@looop-games/cli 0.1.6 → 0.1.8

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.
@@ -78,6 +78,23 @@ function writeLocal(projectDir, engineVersion, placed) {
78
78
  writeFileSync(p, JSON.stringify({ engineVersion, placed }, null, 2) + '\n');
79
79
  }
80
80
 
81
+ // Removing the last SKILL.md from a skill folder must remove the folder too —
82
+ // an empty `.claude/skills/handbook/` still reads to an agent as a skill that
83
+ // exists and is broken. Walks up, stopping at .claude/skills (never the repo).
84
+ function pruneEmptyDirs(projectDir, dest) {
85
+ let dir = dirname(join(projectDir, dest));
86
+ const stop = join(projectDir, '.claude', 'skills');
87
+ while (dir.startsWith(stop) && dir !== stop) {
88
+ try {
89
+ if (readdirSync(dir).length) return;
90
+ rmSync(dir, { recursive: true, force: true });
91
+ } catch {
92
+ return;
93
+ }
94
+ dir = dirname(dir);
95
+ }
96
+ }
97
+
81
98
  // Splice the artifact's managed block into the repo's AGENTS.md, preserving
82
99
  // everything outside the markers. Returns null when the creator's file has no
83
100
  // markers — they rewrote Layer 0 themselves, so it is theirs now.
@@ -213,6 +230,32 @@ export function reconcileAgentSurface(projectDir, engineDir, { log = console.log
213
230
  result.placed.push(dest);
214
231
  }
215
232
 
233
+ // ── orphans: anything WE placed that the artifact no longer ships ────────
234
+ // Deleting a skill upstream is enough to delete it here — no declaration,
235
+ // nothing to remember. The manifest already knows what we put on disk, so a
236
+ // file of ours that vanished from the artifact has been retired, full stop.
237
+ // (`retired` above still earns its keep: on ADOPTION there is no manifest,
238
+ // so an orphan is indistinguishable from a skill the creator wrote.)
239
+ const shipped = new Set(
240
+ Object.keys(manifest.files ?? {})
241
+ .map(target)
242
+ .filter(Boolean),
243
+ );
244
+ for (const dest of Object.keys(placed)) {
245
+ if (shipped.has(dest)) continue;
246
+ const abs = join(projectDir, dest);
247
+ if (existsSync(abs)) {
248
+ if (sha(readFileSync(abs)) !== placed[dest]) {
249
+ result.shadowed.push(dest); // they changed it — it's theirs now
250
+ continue;
251
+ }
252
+ rmSync(abs, { force: true });
253
+ pruneEmptyDirs(projectDir, dest);
254
+ result.removed.push(dest);
255
+ }
256
+ delete placed[dest];
257
+ }
258
+
216
259
  writeLocal(projectDir, manifest.engineVersion, placed);
217
260
  result.engineVersion = manifest.engineVersion;
218
261
  result.backups = backups;
package/lib/feedback.mjs CHANGED
@@ -8,18 +8,117 @@
8
8
  // looop.engine pin, and this CLI's version. A delivered file is stamped
9
9
  // `sent:` + `id:` in its frontmatter so it never ships twice; a failed send
10
10
  // leaves the file unstamped and is retried on the next run.
11
- import { readdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
12
- import { basename, join } from 'node:path';
11
+ import { readdirSync, readFileSync, writeFileSync, existsSync, statSync, realpathSync } from 'node:fs';
12
+ import { basename, join, resolve, relative, isAbsolute, sep } from 'node:path';
13
13
  import { findProject } from './project.mjs';
14
14
  import { getToken, getApiBase } from './config.mjs';
15
15
  import { DEFAULT_API_BASE } from './llm-shim.mjs';
16
16
 
17
17
  const CLI_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
18
18
 
19
+ // Attachment caps. The riven report cited game.js:56 and riven-render.js:302 —
20
+ // source files, not screenshots — so attachments are TEXT. Kept well under the
21
+ // intake's own ceiling so an oversized set is caught HERE, with a readable
22
+ // message: Cloudflare replaces a Pages Function's 5xx body with its own HTML
23
+ // page, which would reach the creator as an unreadable wall.
24
+ export const MAX_ATTACHMENTS = 20;
25
+ export const MAX_ATTACHMENT_BYTES = 256 * 1024; // per file
26
+ export const MAX_ATTACHMENTS_BYTES = 1024 * 1024; // all of them together
27
+
28
+ const frontmatter = (text) => /^---\n([\s\S]*?)\n---/.exec(text)?.[1] ?? '';
29
+
19
30
  // A report is "sent" when its frontmatter carries a `sent:` stamp.
20
31
  function isSent(text) {
21
- const fm = /^---\n([\s\S]*?)\n---/.exec(text);
22
- return fm ? /^sent:\s*\S/m.test(fm[1]) : false;
32
+ return /^sent:\s*\S/m.test(frontmatter(text));
33
+ }
34
+
35
+ // `attachments:` in the report's frontmatter, in either YAML shape agents write:
36
+ //
37
+ // attachments: attachments: [game.js, src/render.js]
38
+ // - game.js
39
+ // - src/render.js
40
+ export function parseAttachments(text) {
41
+ const fm = frontmatter(text);
42
+ const inline = /^attachments:\s*\[(.*)\]\s*$/m.exec(fm);
43
+ if (inline) {
44
+ return inline[1]
45
+ .split(',')
46
+ .map((s) => s.trim().replace(/^['"]|['"]$/g, ''))
47
+ .filter(Boolean);
48
+ }
49
+ const block = /^attachments:\s*$/m.exec(fm);
50
+ if (!block) return [];
51
+ const out = [];
52
+ for (const line of fm.slice(block.index).split('\n').slice(1)) {
53
+ const item = /^\s*-\s*(.+?)\s*$/.exec(line);
54
+ if (!item) break; // the list ends at the first non-item line
55
+ out.push(item[1].replace(/^['"]|['"]$/g, ''));
56
+ }
57
+ return out;
58
+ }
59
+
60
+ // The agent chooses this list, so it is arbitrary filesystem read access over a
61
+ // network. Every path must land inside the game folder — resolved, not merely
62
+ // inspected, because a symlink's string looks innocent. An escape is a hard
63
+ // error, never a skipped file.
64
+ function readAttachments(projectDir, paths, reportName) {
65
+ if (paths.length > MAX_ATTACHMENTS) {
66
+ throw new Error(
67
+ `${reportName} lists ${paths.length} attachments (max ${MAX_ATTACHMENTS}) — attach only the files the diagnosis rests on.`,
68
+ );
69
+ }
70
+
71
+ const root = realpathSync(projectDir);
72
+ const inside = (candidate) => candidate === root || candidate.startsWith(root + sep);
73
+ const escaped = (p, resolved) => {
74
+ throw new Error(
75
+ `${reportName} attaches "${p}", which resolves outside the game folder (${resolved}). ` +
76
+ `Feedback may only attach files from this game.`,
77
+ );
78
+ };
79
+
80
+ const files = [];
81
+ let total = 0;
82
+
83
+ for (const p of paths) {
84
+ // Containment is checked BEFORE existence, and on the lexical path first:
85
+ // "../../.ssh/id_rsa" is refused for what it asks for, not for whether it
86
+ // happens to be there. Then again after realpath — a symlink's string looks
87
+ // perfectly innocent.
88
+ const full = resolve(root, p);
89
+ if (isAbsolute(p) || !inside(full)) escaped(p, full);
90
+
91
+ let real;
92
+ try {
93
+ real = realpathSync(full);
94
+ } catch {
95
+ throw new Error(`${reportName} attaches "${p}", which does not exist in the game folder.`);
96
+ }
97
+ if (!inside(real)) escaped(p, real);
98
+ if (!statSync(real).isFile()) throw new Error(`${reportName} attaches "${p}", which is not a file.`);
99
+
100
+ const bytes = statSync(real).size;
101
+ if (bytes > MAX_ATTACHMENT_BYTES) {
102
+ throw new Error(
103
+ `${reportName} attaches "${p}" (${Math.round(bytes / 1024)} KiB) — too large, the per-file cap is ${MAX_ATTACHMENT_BYTES / 1024} KiB.`,
104
+ );
105
+ }
106
+ total += bytes;
107
+ if (total > MAX_ATTACHMENTS_BYTES) {
108
+ throw new Error(
109
+ `${reportName}'s attachments are too large together (over ${MAX_ATTACHMENTS_BYTES / 1024} KiB) — attach fewer files.`,
110
+ );
111
+ }
112
+
113
+ const content = readFileSync(real, 'utf8');
114
+ if (content.includes('\0')) {
115
+ throw new Error(`${reportName} attaches "${p}", which is not a text file. Attach source, logs, or config.`);
116
+ }
117
+ // Report the path the creator wrote (project-relative), not the real one —
118
+ // it is what the report's prose cites.
119
+ files.push({ path: relative(root, real).split(sep).join('/'), content });
120
+ }
121
+ return files;
23
122
  }
24
123
 
25
124
  function titleOf(text, path) {
@@ -61,12 +160,16 @@ export async function sendFeedback({
61
160
  const sent = [];
62
161
  for (const path of unsent) {
63
162
  const body = readFileSync(path, 'utf8');
163
+ // Read + guard the attachments BEFORE the request: a bad path must fail the
164
+ // send outright, leaving the report unstamped and retryable.
165
+ const files = readAttachments(project.dir, parseAttachments(body), basename(path));
64
166
  const report = {
65
167
  title: titleOf(body, path),
66
168
  body,
67
169
  slug: project.slug,
68
170
  engine: project.pkg?.looop?.engine,
69
171
  cli: CLI_VERSION,
172
+ ...(files.length ? { files } : {}),
70
173
  };
71
174
  const res = await fetch(`${apiBase}/api/creator/feedback`, {
72
175
  method: 'POST',
@@ -79,8 +182,9 @@ export async function sendFeedback({
79
182
  }
80
183
  const { id } = await res.json();
81
184
  writeFileSync(path, stamp(body, id));
82
- log(`✅ Sent ${basename(path)} ${id}`);
83
- sent.push({ file: path, id });
185
+ const withFiles = files.length ? ` (+${files.length} file${files.length === 1 ? '' : 's'})` : '';
186
+ log(`✅ Sent ${basename(path)}${withFiles} → ${id}`);
187
+ sent.push({ file: path, id, files: files.map((f) => f.path) });
84
188
  }
85
189
  log(`${sent.length} report${sent.length === 1 ? '' : 's'} delivered to the Looop team.`);
86
190
  return { sent };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@looop-games/cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Looop game development CLI — dev server, login, and publishing for standalone Looop games.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -112,6 +112,32 @@ they'd care about (feel, fairness, what can break) goes to them; one with no
112
112
  creator-visible consequence does not. Never hide a real fork to keep things
113
113
  simple — there is always a non-confusing way to ask it.
114
114
 
115
+ **The shape decisions are ALWAYS the creator's** — never a default you carry in
116
+ on their behalf, and never something you mention in passing as already settled:
117
+
118
+ - **Camera and perspective** — top-down, side-on, isometric, first-person, 3D.
119
+ There is no house camera; see `/engine`'s render paths.
120
+ - **Art direction and look** — the Looop iso kit is *available*, not mandatory.
121
+ - **The core verb** — what the player actually does, moment to moment.
122
+ - **How conflict works** — combat, avoidance, puzzles, none of the above.
123
+ - **Single-player vs. co-op vs. competitive** feel (the *plumbing* is always
124
+ multiplayer-first; what the game is *about* is theirs).
125
+
126
+ **NEVER invent a platform constraint.** Do not tell the creator "the engine
127
+ only does X", "Looop doesn't support Y", or "that would take months" unless you
128
+ have just checked and can point at what says so. A Looop game is a web page;
129
+ the library is a bag of conveniences, not a fence (`/engine` — "The library is
130
+ NOT the limit"). This failure is worse than a silent default: a creator can
131
+ argue with "that's a lot of work", but they cannot argue with "the platform
132
+ can't", so a fabricated limit kills their idea and looks like physics while
133
+ doing it. If a thing is genuinely unbuilt, that is a **cost to price honestly**
134
+ and hand them — never a "no" you issue on Looop's behalf.
135
+
136
+ Watch for this specifically when scope pressure and a gap in the library point
137
+ the same way: **"default to the smallest build" is never a licence to narrow
138
+ the creator's vision** — that's the scope-narrowing this skill forbids two
139
+ paragraphs down, wearing a technical disguise.
140
+
115
141
  **Default to the smallest build that meets the creator's ask — never inflate
116
142
  scope.** Every "we could also…" and every richer-than-asked option is scope
117
143
  YOU are injecting; the creator can't push back on over-building they didn't
@@ -192,7 +218,7 @@ Outcomes:
192
218
  back to the last save (`git reset --hard` — nothing was saved mid-milestone,
193
219
  so that's the last accepted state).
194
220
  - **The playtest caught a defect you missed** → that's a hole in the
195
- verification, not just a bug. Fix it, then run `/update-handbook` so this
221
+ verification, not just a bug. Fix it, then run `/handbook` so this
196
222
  class of defect gets an automated check and never reaches a playtest again.
197
223
 
198
224
  ## 5. Offer publish — never publish on your own
@@ -11,6 +11,42 @@ system from scratch — movement, audio, UI, chat, NPCs, scoring — check wheth
11
11
  the engine already has it. Reinventing a library is the most common way a game
12
12
  gets worse.
13
13
 
14
+ **But the library is a floor, not a ceiling — read the next section before you
15
+ ever tell the creator something isn't possible.**
16
+
17
+ ## The library is NOT the limit of what the game can be
18
+
19
+ A Looop game is **a web page**. Anything the browser can do, the game can do.
20
+ The engine saves you work; it does not bound the game. The catalog is what
21
+ Looop has *already built for you* — never mistake it for the list of things a
22
+ Looop game is allowed to be.
23
+
24
+ So there are only ever two answers to "can Looop do X?":
25
+
26
+ 1. **The library has it** → use it (don't reinvent it).
27
+ 2. **The library doesn't** → **then you build it, in the game folder.** That is
28
+ the normal, expected path — it is how the library got its entries in the
29
+ first place. Price it honestly and let the creator decide.
30
+
31
+ **Never say "the engine doesn't support that" as if it settled the question.**
32
+ It is the most damaging sentence you can say to a creator: it retires their
33
+ idea without their consent, and it is almost always false. If you catch
34
+ yourself about to say it — stop, come back here, and check. What you *may* say
35
+ is an honest **cost**: "nothing in the kit does this, so we'd build it; here's
36
+ roughly what that means." Price it from what actually exists, never from a
37
+ worst case you imagined.
38
+
39
+ ## Render paths (the camera is the creator's decision, not yours)
40
+
41
+ There is no house camera. Pick with the creator, framed by consequences:
42
+
43
+ | path | what it is |
44
+ | --- | --- |
45
+ | **plain 2D canvas** | `getContext('2d')` — what the scaffold ships with. Top-down, side-on, whatever you draw. Lightest. |
46
+ | **iso 2D** (`shared/ui/iso` + `iso-style-looop`) | Isometric 3/4 canvas drawing with the Looop world kit. The best-supported look. Depth is a painter-algorithm sort, so large occluders can glitch. |
47
+ | **iso-3d** (`shared/ui/iso-3d`) | Real WebGL/three.js geometry with per-pixel depth-buffer occlusion. Same iso look, no depth-sort bugs. |
48
+ | **anything else** | First-person, over-the-shoulder, free 3D camera, 2.5D — all buildable on three.js in the game folder. Less kit support, not less possible. |
49
+
14
50
  ## Discover
15
51
 
16
52
  1. **Start at the index:**
@@ -57,3 +93,8 @@ when the behaviour truly must change inside the engine module.
57
93
  game would want** → that's platform feedback: `/feedback`.
58
94
  - The need is **specific to this game** → build it game-local (or override),
59
95
  and note in `handbook/design.md` if it's a pillar.
96
+ - The engine has **nothing at all** for something many games would want (a
97
+ whole render path, a genre's core system) → do **both**: build it game-local
98
+ so the creator is never blocked waiting on Looop, *and* `/feedback` it so
99
+ Looop can make it first-class. Never let a gap in the library become a "no"
100
+ to the creator.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: feedback
3
- description: File feedback to the Looop team when something in Looop ITSELF seems broken or missing — the engine, a shared component, the looop CLI, login, publishing, the platform. Use the moment you or the creator notice "this looks like a Looop bug, not our game" — offer to file it right away, don't wait to be asked.
3
+ description: File feedback to the Looop team when something in Looop ITSELF is broken or missing — the engine, a shared component, the looop CLI, login, publishing, the platform. Use the moment you or the creator think "this looks like a Looop bug, not our game" — AND, just as importantly, whenever you discover your own game breaking a Looop rule, because Looop's own instructions may be what put it there. Offer to file it right away; don't wait to be asked.
4
4
  ---
5
5
 
6
6
  # /feedback — tell Looop when Looop is the problem
@@ -13,6 +13,34 @@ that lied, a missing capability every game would want — **offer to file it**:
13
13
 
14
14
  This game's own bugs are NOT feedback — they're `/todo`s or just fixes.
15
15
 
16
+ ## The defect you will not notice
17
+
18
+ There is one class of Looop bug the reflex above is blind to, and it is the most
19
+ valuable class there is: **a defect Looop's own guidance caused.**
20
+
21
+ It looks like this. You discover your game violating a Looop rule — it's routing
22
+ state the wrong way, using an API it shouldn't. The symptom is in *your* code, so
23
+ the conclusion writes itself: *"our bug, not the engine's."* You fix your game and
24
+ move on, and Looop never hears about it.
25
+
26
+ That inference is locally correct and globally wrong. **Looop's instructions are
27
+ not a fixed point — they are a suspect.** They contradict each other sometimes.
28
+ The mandatory ones (this repo's `AGENTS.md`, injected into every session) can be
29
+ wrong while an opt-in practices doc is right, and you will have followed the
30
+ mandatory one without ever noticing there was a choice.
31
+
32
+ So, the rule:
33
+
34
+ > **When you find your game violating a Looop rule, work out what led you there
35
+ > BEFORE you fix it.**
36
+
37
+ Retrace it honestly. Did a Looop-authored source — `AGENTS.md`, a
38
+ `shared/practices/` doc, a library README, a skill — point you this way? If yes,
39
+ **that is a Looop defect and it gets filed**, even though your game is also wrong.
40
+ Both are true at the same time; fix your game *and* file the report. If you'd have
41
+ made the same mistake with no guidance at all, it's just your bug — fix it and
42
+ move on.
43
+
16
44
  ## What to file
17
45
 
18
46
  Write the report to `notes/feedback/<YYYY-MM-DD-HHMM>-<short-slug>.md`
@@ -26,6 +54,9 @@ detail you leave out is a detail they'll have to guess. Include:
26
54
  created: YYYY-MM-DD
27
55
  engine: <the looop.engine pin from package.json>
28
56
  cli: <@looop-games/cli version from package.json devDependencies>
57
+ attachments:
58
+ - game.js
59
+ - src/net.js
29
60
  ---
30
61
  # <One line: what's wrong>
31
62
 
@@ -40,6 +71,10 @@ custom port, offline, …).
40
71
  The smallest steps/snippet that shows it. Exact commands **with their full
41
72
  output pasted verbatim** — never summarize an error message.
42
73
 
74
+ ## What led us here (if a Looop source did)
75
+ The file and line that pointed you wrong, quoted, and the doc that contradicts
76
+ it. This is the section that gets the guidance itself fixed.
77
+
43
78
  ## Transcript
44
79
  The conversation that surfaced this, quoted verbatim — from the creator's
45
80
  request that first hit the problem through your diagnosis: what they asked,
@@ -54,18 +89,37 @@ Trim game-specific noise, never evidence: cut what's irrelevant to the
54
89
  defect, keep everything that shows it. (The intake caps a report at 256 KiB —
55
90
  if the transcript pushes past that, cut it down to the relevant exchange.)
56
91
 
92
+ ## Attach the files you're talking about
93
+
94
+ A report that cites `game.js:56` is only as good as the reader's ability to see
95
+ line 56. **List the files your diagnosis rests on in the `attachments:`
96
+ frontmatter** — they ship with the report, and the team reads them beside it.
97
+
98
+ This applies to **any** report, whatever its reason: a bug, a missing capability,
99
+ a rule that led you wrong, a module you had to fork. Whatever your evidence is,
100
+ attach it.
101
+
102
+ - **You pick the files** — you wrote the diagnosis, so you know what it rests on.
103
+ - Paths are **relative to the game folder** and must stay inside it. Anything
104
+ pointing outside is refused outright.
105
+ - **Text only** (source, logs, config) — up to 20 files, 256 KiB each, 1 MiB total.
106
+ - Attach what the report *argues from*: the file with the bug, the module you
107
+ worked around, the config that reproduces it. Not the whole game.
108
+
57
109
  ## Send it
58
110
 
59
111
  1. Run **`npx looop feedback`** — it delivers every unsent report under
60
112
  `notes/feedback/` to the Looop team (login required; it's the same account
61
- as publishing) and stamps each delivered file with `sent:` + `id:` in its
62
- frontmatter so it never ships twice.
63
- 2. If the send fails (offline, not logged in), the file stays unstamped —
64
- just run `npx looop feedback` again later; it retries everything unsent.
113
+ as publishing), attachments included, and stamps each delivered file with
114
+ `sent:` + `id:` in its frontmatter so it never ships twice.
115
+ 2. If the send fails (offline, not logged in, a bad attachment path), the file
116
+ stays unstamped — fix what it says and run `npx looop feedback` again; it
117
+ retries everything unsent.
65
118
 
66
119
  ## Why bother
67
120
 
68
121
  An override that patches an engine bug is a fork that stops receiving updates;
69
- feedback is how the fix lands upstream so the fork can be deleted. And a
70
- missing capability filed from a real game is exactly how the engine decides
71
- what to build next.
122
+ feedback is how the fix lands upstream so the fork can be deleted. A missing
123
+ capability filed from a real game is exactly how the engine decides what to build
124
+ next. And a rule that led you wrong will lead **every** creator wrong, in every
125
+ game, until somebody says so.
@@ -0,0 +1,142 @@
1
+ ---
2
+ name: handbook
3
+ description: The entry point to this game's handbook — its durable truth. Use when you need to know what this game already believes (before building, before proposing anything), and when something durable emerges that belongs in it — a design pillar, a blessed feel value, or a playtest catch that needs converting into an automated check. Also use when durable truth appears that has no chapter yet (the game's vision, its world, its cast, its economy) and one should be started.
4
+ ---
5
+
6
+ # /handbook — what this game knows about itself
7
+
8
+ `handbook/` is this game's **durable truth**: the things a future session must
9
+ not violate, and must not have to re-derive. It is the game's own layer on top
10
+ of the engine's read-only craft docs — where they touch the same topic, the
11
+ handbook is *this game's* answer.
12
+
13
+ Three places knowledge lives here; keep them straight:
14
+
15
+ | | Holds | Lifespan |
16
+ |---|---|---|
17
+ | **`handbook/`** | what this game IS and has decided | durable — outlives every plan |
18
+ | **`notes/`** | what we're building or might build (`plans/`, `todos/`) | transient — closed when the work is |
19
+ | the engine's `shared/practices/` | how Looop games are built in general | read-only, ships with the engine |
20
+
21
+ The test for whether something belongs here: **would a future session need to
22
+ *not violate* this?** → handbook. *Is it something we're doing, or might do?* →
23
+ `notes/`.
24
+
25
+ ## The chapters
26
+
27
+ ```
28
+ handbook/
29
+ design.md the pillars — what this game IS, checked against new ideas
30
+ feel.md locked feel values the creator has blessed
31
+ qa.md checks this game has earned (mostly from playtests that caught something)
32
+ ```
33
+
34
+ **The set is open, and these three are only where it starts.** They ship with
35
+ every game because every game accumulates them. They are not the limit, and they
36
+ are not a template to fill in — what else a handbook holds depends entirely on
37
+ what this game turns out to need. See *Starting a new chapter*.
38
+
39
+ ## Reading it — do this before you build
40
+
41
+ **`ls handbook/` and read what's relevant before writing game code or proposing
42
+ a direction.** Every chapter opens with a line saying what it's for, so the
43
+ folder listing plus the first two lines of each file is a cheap orientation.
44
+ Reading is free; contradicting the handbook and being caught later is not.
45
+
46
+ If what you're about to propose **contradicts something the handbook already
47
+ says**, that is not a detail to smooth over — stop and say so. Either the idea is
48
+ wrong, or what's written is out of date and the creator needs to say so out loud.
49
+ Never quietly build against it.
50
+
51
+ An empty chapter is honest — an early game hasn't decided much yet, and a
52
+ speculative pillar nobody has lived is worse than none.
53
+
54
+ ## Growing it
55
+
56
+ **Every handbook write needs the creator's approval first.** The handbook is
57
+ *their* game's truth — propose the exact entry ("I'd like to record: …") and
58
+ write it only after they say yes. Never slip an entry in as a side effect of
59
+ other work. (Smoke/test FILES don't need this gate — they're regression tests,
60
+ not blessed truth; only `handbook/` writes do.)
61
+
62
+ ### A design principle emerged → `handbook/design.md`
63
+
64
+ When a decision reveals what this game IS ("never text tutorials — the world
65
+ teaches", "death must always be the player's fault"), write the pillar down.
66
+ Future builds check new ideas against these.
67
+
68
+ ### A playtest caught a defect → an automated check
69
+
70
+ The premise (from the engine's `shared/practices/qa.md`): **a human catching a
71
+ defect means an automated check was missing.** Convert the *class* of defect,
72
+ not the instance:
73
+
74
+ 1. Name the miss precisely — not "the door was broken" but "doors can lose
75
+ their collision when the room resets, and nothing checks collision after a
76
+ reset."
77
+ 2. Prefer an **executable check**: write a `<aspect>.smoke.mjs` (or
78
+ `*.test.mjs`) that reproduces the defect — confirm it fails RED on the
79
+ broken state, then goes green on the fix. `npx looop test` discovers it
80
+ forever after; a guard you never saw fail is a guard you can't trust.
81
+ 3. **If the check needs to SEE the game's internals** (collision boxes, depth
82
+ order, hit areas) and the game has no debug overlay yet, **build one as part
83
+ of the conversion** — a keyboard-toggled draw of the real boxes/order. It's
84
+ a small one-time cost, and every later screenshot-verify reuses it (master
85
+ list row R2).
86
+ 4. Only if it truly can't be executed (needs human perception), add it as a
87
+ procedural step in **`handbook/qa.md`** — `/qa` runs those by hand each time.
88
+
89
+ ## Starting a new chapter
90
+
91
+ The four standing chapters won't fit everything. When durable truth appears
92
+ that belongs in none of them, **start a chapter** — that is the handbook
93
+ working as intended, not a special case.
94
+
95
+ A subject earns a chapter when it is: **durable** (it outlives the current
96
+ plan), **referred back to** (future sessions need it to stay consistent), and
97
+ **not a fit** for an existing chapter. It can be anything this game actually
98
+ needs: `vision.md` (what the game is *for*, once the creator has said it out
99
+ loud), `world.md` (a setting an agent must not contradict), `characters.md`,
100
+ `economy.md` (numbers that have to balance), `controls.md`. Don't shop from that
101
+ list — reach for whatever this game keeps needing to remember.
102
+
103
+ To start one:
104
+
105
+ 1. **Propose it** — the name, and the exact first entry. Same approval gate as
106
+ any handbook write; creating a chapter is a bigger act than adding a line,
107
+ not a smaller one.
108
+ 2. Create `handbook/<subject>.md` with the standard shape:
109
+
110
+ ```markdown
111
+ # <Subject> — <what this chapter is for, in half a line>
112
+
113
+ <One or two sentences: what belongs here, what doesn't.>
114
+
115
+ ## YYYY-MM-DD — <the entry>
116
+ <The truth itself. Short. Actionable cold, by someone who wasn't there.>
117
+ ```
118
+
119
+ 3. Don't pre-create chapters "in case", and don't propose one because the game
120
+ "ought to" have it. An empty speculative chapter is a trap — it invites
121
+ invented content, and a creator filling in a template is not the same as a
122
+ creator telling you something true. A chapter starts the day it has something
123
+ true to hold.
124
+
125
+ ## The upstream half
126
+
127
+ Before writing, ask: **is this lesson specific to this game, or would every
128
+ Looop game want it?** A generic hole (an engine component that breaks a
129
+ universal expectation, a check every game should run) belongs in the engine's
130
+ master list, not just this repo — offer `/feedback` so it lands upstream for
131
+ everyone. Do both when in doubt: the handbook entry protects this game now; the
132
+ feedback fixes it everywhere later.
133
+
134
+ ## Rules
135
+
136
+ - **One lesson per invocation, converted fully** — an entry someone can act on
137
+ cold, not a vague reminder.
138
+ - **Date entries.** When a later decision supersedes one, update it **in place**
139
+ rather than stacking contradictions — a handbook that argues with itself is
140
+ worse than no handbook, because a future session will pick the wrong side.
141
+ - **Short.** Every line in here is read by every future session. It earns its
142
+ place or it goes.
@@ -40,7 +40,7 @@ creator's list.
40
40
  URL to open, never a command to run). If everything was automatable, say
41
41
  so plainly; "couldn't test it" must never read as "tested and fine".
42
42
  5. **Close the ratchet.** If the creator's pass catches something your run
43
- missed, that's a hole in the checks — run `/update-handbook` to convert
43
+ missed, that's a hole in the checks — run `/handbook` to convert
44
44
  that defect class into an automated check.
45
45
 
46
46
  ## Notes
@@ -48,11 +48,18 @@ applies. This table routes only what has no skill:
48
48
  scores, world objects) rides the room's authority — `room.update(...)`,
49
49
  `room.send('input', ...)` — never local mutation only one client sees.
50
50
  Verify multiplayer behaviour with TWO browser contexts, not one.
51
- 2. **The engine is read-only.** `/shared/...` imports come from the installed
52
- engine at `node_modules/@looop-games/engine` (not an npm dependency —
53
- `looop dev` downloads the version pinned in `package.json`'s `looop.engine`
54
- and reinstalls it if an `npm install` prunes it). Never edit files in
55
- `node_modules` — use `overrides/shared/` (see the table above).
51
+ 2. **The engine is read-only and it is not the ceiling.** `/shared/...`
52
+ imports come from the installed engine at `node_modules/@looop-games/engine`
53
+ (not an npm dependency — `looop dev` downloads the version pinned in
54
+ `package.json`'s `looop.engine` and reinstalls it if an `npm install` prunes
55
+ it). Never edit files in `node_modules` — use `overrides/shared/` (see the
56
+ table above). But read-only means *don't edit it*, **not** *don't exceed
57
+ it*: a Looop game is a web page, so **anything the browser can do, this game
58
+ can do.** The library is a bag of conveniences you draw from, not a fence
59
+ around what the game may be — what it doesn't have, you build, here in the
60
+ game folder. **Never tell the creator that Looop "can't" do something**
61
+ (see `/engine` — it is almost always false, and it retires their idea
62
+ without their consent).
56
63
  3. **Never hand-roll a server.** Only `looop dev` (or `looop test`) serves
57
64
  this game: they alias `/shared/...` and inject the platform layer. A plain
58
65
  static server 404s every engine import and the game silently never boots.
@@ -75,7 +82,7 @@ applies. This table routes only what has no skill:
75
82
 
76
83
  - **`handbook/`** — durable truth about THIS game: `qa.md` (its checks),
77
84
  `feel.md` (locked feel values), `design.md` (its pillars). Consult it before
78
- working; grow it with `/update-handbook`.
85
+ working; grow it with `/handbook`.
79
86
  - **`notes/`** — work tracking: `notes/plans/` (what's being built — `/build`
80
87
  runs from these), `notes/todos/` (captured bugs/ideas), and
81
88
  `notes/feedback/` (reports for the Looop team, written by `/feedback`).
@@ -1,7 +1,8 @@
1
1
  # Design — the pillars
2
2
 
3
- What this game IS: the principles new ideas get checked against. Written as
4
- they emerge from real decisions (via `/update-handbook`), not invented up
3
+ The rules that follow from the vision: the principles new ideas get checked
4
+ against. Written as
5
+ they emerge from real decisions (via `/handbook`), not invented up
5
6
  front — an empty file is honest; a speculative pillar is a trap.
6
7
 
7
8
  _No pillars yet._
@@ -3,6 +3,6 @@
3
3
  Feel calls the creator has blessed: the value, where it lives, and why it's
4
4
  right — so no future session "improves" them away. General feel craft lives in
5
5
  the engine's `shared/practices/feel.md`; this file is only what THIS game has
6
- locked. `/update-handbook` adds entries.
6
+ locked. `/handbook` adds entries.
7
7
 
8
8
  _Nothing locked yet._
@@ -2,7 +2,7 @@
2
2
 
3
3
  Checks THIS game has earned, on top of the engine's master list
4
4
  (`node_modules/@looop-games/engine/shared/practices/qa.md`) and the executable
5
- tests `npx looop test` discovers. `/qa` runs all three sources; `/update-handbook`
5
+ tests `npx looop test` discovers. `/qa` runs all three sources; `/handbook`
6
6
  adds entries here when a playtest catches something no automated check saw —
7
7
  but prefer writing a `*.smoke.mjs` when the check can be executed.
8
8
 
@@ -1,69 +0,0 @@
1
- ---
2
- name: update-handbook
3
- description: Record something durable this game just taught us — convert a playtest catch into an automated check, lock in a blessed feel value, or write down a design pillar. Use right after a creator playtest catches a defect the automated checks missed, when a feel value gets approved ("that jump is perfect — keep it"), or when a design principle emerges.
4
- ---
5
-
6
- # /update-handbook — keep what the game taught us
7
-
8
- `handbook/` is this game's durable truth — the layer that AUGMENTS the
9
- engine's read-only practices with what THIS game has learned. This skill is
10
- how it grows.
11
-
12
- **Every handbook write needs the creator's approval first.** The handbook is
13
- *their* game's truth — propose the exact entry ("I'd like to record: …"),
14
- and write it only after they say yes. Never slip an entry in as a side effect
15
- of other work. (Smoke/test FILES don't need this gate — they're regression
16
- tests, not blessed truth; only `handbook/` writes do.)
17
-
18
- Three kinds of lesson, three destinations:
19
-
20
- ## 1. A playtest caught a defect → an automated check
21
-
22
- The premise (from the engine's `shared/practices/qa.md`): **a human catching a
23
- defect means an automated check was missing.** Convert the *class* of defect,
24
- not the instance:
25
-
26
- 1. Name the miss precisely — not "the door was broken" but "doors can lose
27
- their collision when the room resets, and nothing checks collision after a
28
- reset."
29
- 2. Prefer an **executable check**: write a `<aspect>.smoke.mjs` (or
30
- `*.test.mjs`) that reproduces the defect — confirm it fails RED on the
31
- broken state, then goes green on the fix. `npx looop test` discovers it
32
- forever after; a guard you never saw fail is a guard you can't trust.
33
- 3. **If the check needs to SEE the game's internals** (collision boxes, depth
34
- order, hit areas) and the game has no debug overlay yet, **build one as
35
- part of the conversion** — a keyboard-toggled draw of the real boxes/order.
36
- It's a small one-time cost, and every later screenshot-verify reuses it
37
- (master list row R2).
38
- 4. Only if it truly can't be executed (needs human perception), add it as a
39
- procedural step in **`handbook/qa.md`** — `/qa` runs those by hand each
40
- time.
41
-
42
- ## 2. A feel value got blessed → `handbook/feel.md`
43
-
44
- When the creator locks a feel call ("that speed is exactly right"), record the
45
- value, where it lives, and WHY it's right — so no future session "improves" it
46
- away. If it's worth defending, pin it with a smoke too.
47
-
48
- ## 3. A design principle emerged → `handbook/design.md`
49
-
50
- When a decision reveals what this game IS ("never text tutorials — the world
51
- teaches", "death must always be the player's fault"), write the pillar down.
52
- Future builds check new ideas against these.
53
-
54
- ## The upstream half
55
-
56
- Before writing, ask: **is this lesson specific to this game, or would every
57
- Looop game want it?** A generic hole (an engine component that breaks a
58
- universal expectation, a check every game should run) belongs in the engine's
59
- master list, not just this repo — offer `/feedback` so it lands upstream for
60
- everyone. Do both when in doubt: the handbook entry protects this game now;
61
- the feedback fixes it everywhere later.
62
-
63
- ## Rules
64
-
65
- - One lesson per invocation, converted fully — an entry someone can act on
66
- cold, not a vague reminder.
67
- - Handbook entries are durable truth: date them, keep them short, and when a
68
- later decision supersedes one, update it in place rather than stacking
69
- contradictions.