@looop-games/cli 0.1.8 → 0.1.9
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/lib/agent-surface.mjs +14 -32
- package/lib/create.mjs +132 -86
- package/package.json +1 -2
- package/template/.claude/skills/build/SKILL.md +0 -266
- package/template/.claude/skills/engine/SKILL.md +0 -100
- package/template/.claude/skills/feedback/SKILL.md +0 -125
- package/template/.claude/skills/handbook/SKILL.md +0 -142
- package/template/.claude/skills/qa/SKILL.md +0 -53
- package/template/.claude/skills/todo/SKILL.md +0 -54
- package/template/.claude/skills/update-looop/SKILL.md +0 -74
- package/template/AGENTS.md +0 -93
- package/template/CLAUDE.md +0 -4
- package/template/GEMINI.md +0 -4
- package/template/boot.smoke.mjs +0 -21
- package/template/game.js +0 -53
- package/template/gitignore +0 -2
- package/template/handbook/design.md +0 -8
- package/template/handbook/feel.md +0 -8
- package/template/handbook/qa.md +0 -9
- package/template/index.html +0 -26
package/lib/agent-surface.mjs
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
// The agent surface — skills + the Layer-0 managed block — and how an update
|
|
2
2
|
// reaches a repo that already exists (creator-harness Slice 2, decision Q2).
|
|
3
3
|
//
|
|
4
|
-
// The surface ships INSIDE the versioned engine artifact,
|
|
5
|
-
// CLI
|
|
6
|
-
//
|
|
7
|
-
// artifact
|
|
8
|
-
//
|
|
4
|
+
// The surface ships INSIDE the versioned engine artifact, and ONLY there — the
|
|
5
|
+
// npm CLI carries no copy of it. That is what makes the ownership record below
|
|
6
|
+
// trustworthy: `create` places the surface by calling straight into this module
|
|
7
|
+
// (against the artifact it just installed), so the bytes on disk and the hashes
|
|
8
|
+
// we record come from the same place, and a fresh repo's first `update` is a
|
|
9
|
+
// genuine no-op. The CLI used to seed the surface from its own bundled
|
|
10
|
+
// `template/`, stamped with the ENGINE's version — a record that was false the
|
|
11
|
+
// moment npm and the engine registry shipped out of step, and that made the
|
|
12
|
+
// first `update` overwrite (or DELETE, via the orphan prune below) skills on a
|
|
13
|
+
// repo minutes old.
|
|
9
14
|
//
|
|
10
|
-
// **This runs
|
|
11
|
-
// or `publish`. It COULD run there safely (it can only ever converge a
|
|
12
|
-
// the engine version already pinned in its package.json, never a newer one),
|
|
15
|
+
// **This runs under `looop update` — and under `looop create`** — never on
|
|
16
|
+
// `dev` or `publish`. It COULD run there safely (it can only ever converge a
|
|
17
|
+
// repo to the engine version already pinned in its package.json, never a newer one),
|
|
13
18
|
// but a creator who ran `dev` and then found files they never wrote sitting in
|
|
14
19
|
// `git status` would be right to feel we'd gone through their pockets. Updates
|
|
15
20
|
// are something you ASK for. So this module writes nothing on its own schedule,
|
|
@@ -36,7 +41,7 @@
|
|
|
36
41
|
// everything outside them (their title, their instructions below the end
|
|
37
42
|
// marker) is theirs and is spliced back untouched.
|
|
38
43
|
import { createHash } from 'node:crypto';
|
|
39
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync,
|
|
44
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
40
45
|
import { dirname, join } from 'node:path';
|
|
41
46
|
|
|
42
47
|
export const SURFACE_DIR = 'agent-surface';
|
|
@@ -269,26 +274,3 @@ export function reconcileAgentSurface(projectDir, engineDir, { log = console.log
|
|
|
269
274
|
|
|
270
275
|
return result;
|
|
271
276
|
}
|
|
272
|
-
|
|
273
|
-
// Used by `looop create`: the template seed IS the surface for a brand-new
|
|
274
|
-
// repo, so record it as ours immediately. Without this the first `dev` would
|
|
275
|
-
// treat a two-minute-old scaffold as an unknown pre-existing repo and adopt it.
|
|
276
|
-
export function recordSeededSurface(projectDir, engineVersion) {
|
|
277
|
-
const placed = {};
|
|
278
|
-
const add = (rel) => {
|
|
279
|
-
const abs = join(projectDir, rel);
|
|
280
|
-
if (existsSync(abs)) placed[rel] = sha(readFileSync(abs));
|
|
281
|
-
};
|
|
282
|
-
const walk = (rel) => {
|
|
283
|
-
const abs = join(projectDir, rel);
|
|
284
|
-
if (!existsSync(abs)) return;
|
|
285
|
-
for (const entry of readdirSync(abs)) {
|
|
286
|
-
const child = join(rel, entry);
|
|
287
|
-
if (statSync(join(projectDir, child)).isDirectory()) walk(child);
|
|
288
|
-
else add(child);
|
|
289
|
-
}
|
|
290
|
-
};
|
|
291
|
-
walk(join('.claude', 'skills'));
|
|
292
|
-
add('AGENTS.md');
|
|
293
|
-
writeLocal(projectDir, engineVersion ?? null, placed);
|
|
294
|
-
}
|
package/lib/create.mjs
CHANGED
|
@@ -2,26 +2,45 @@
|
|
|
2
2
|
//
|
|
3
3
|
// The create-next-app gesture: one command → a complete folder that is its
|
|
4
4
|
// own repo, where `looop dev` immediately serves a WORKING multiplayer game
|
|
5
|
-
// (the platform is not optional — the
|
|
5
|
+
// (the platform is not optional — the scaffold joins a room on line one, no
|
|
6
6
|
// "add multiplayer later" tier). The folder name is the slug.
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
8
|
+
// ── Where the creator's files come from ─────────────────────────────────────
|
|
9
|
+
//
|
|
10
|
+
// EVERYTHING a creator receives comes from the versioned engine artifact: the
|
|
11
|
+
// starter game (`scaffold/`) and the agent surface (`agent-surface/` — skills
|
|
12
|
+
// plus the Layer-0 managed block). This npm package ships CODE ONLY.
|
|
13
|
+
//
|
|
14
|
+
// It used to ship a `template/` folder, and that was a bug with a long fuse.
|
|
15
|
+
// npm and the engine registry are two independent, immutable release lanes, so
|
|
16
|
+
// the two copies of the surface drifted the moment one shipped without the
|
|
17
|
+
// other — and `create` seeded from the CLI while stamping the ownership record
|
|
18
|
+
// with the ENGINE's version, so that record was a lie the day it was written.
|
|
19
|
+
// The creator paid for it on their first `looop update`: skills they had never
|
|
20
|
+
// touched changed under them, and a skill the CLI shipped but the engine didn't
|
|
21
|
+
// was silently DELETED (agent-surface.mjs prunes files it owns that the
|
|
22
|
+
// artifact no longer lists). On a repo minutes old.
|
|
23
|
+
//
|
|
24
|
+
// One authored copy, one release lane. A skill or scaffold change now ships
|
|
25
|
+
// with an engine release alone; the CLI is released only when the CLI's own
|
|
26
|
+
// code changes.
|
|
27
|
+
//
|
|
28
|
+
// The consequence, and it is deliberate: `create` needs the engine, and the
|
|
29
|
+
// engine download is login-gated — so CREATE SIGNS YOU IN. That gate existed
|
|
30
|
+
// anyway, one command later, at first `dev`. This moves it to where the creator
|
|
31
|
+
// is already waiting and leaves `dev` instant.
|
|
15
32
|
import { execFileSync } from 'node:child_process';
|
|
16
|
-
import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync, symlinkSync } from 'node:fs';
|
|
33
|
+
import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync, rmSync, symlinkSync } from 'node:fs';
|
|
17
34
|
import { createRequire } from 'node:module';
|
|
18
35
|
import { join, relative, dirname } from 'node:path';
|
|
19
36
|
import { getApiBase } from './config.mjs';
|
|
20
37
|
import { DEFAULT_API_BASE } from './llm-shim.mjs';
|
|
21
38
|
import { runNpm } from './npm.mjs';
|
|
22
|
-
import {
|
|
39
|
+
import { ensureEngine } from './engine.mjs';
|
|
40
|
+
import { reconcileAgentSurface } from './agent-surface.mjs';
|
|
23
41
|
|
|
24
|
-
|
|
42
|
+
// The starter game, inside the installed engine artifact.
|
|
43
|
+
export const SCAFFOLD_DIR = 'scaffold';
|
|
25
44
|
|
|
26
45
|
const NAME_OK = /^[a-z0-9][a-z0-9-]{0,40}$/;
|
|
27
46
|
|
|
@@ -48,69 +67,37 @@ const ALLOW_SCRIPTS = { esbuild: true, workerd: true, fsevents: true };
|
|
|
48
67
|
// what create-time provisioning exists to prevent.
|
|
49
68
|
const PLAYWRIGHT_SPEC = '^1.61.1';
|
|
50
69
|
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
log(`⚠️ Could not resolve the engine version (${err.message}) — the first \`looop dev\` will pin it instead.`);
|
|
67
|
-
return null;
|
|
70
|
+
// Copy the starter game out of the artifact. One rename: `gitignore` →
|
|
71
|
+
// `.gitignore` (npm pack silently strips .gitignore files from a package, so
|
|
72
|
+
// the artifact stores it dot-less).
|
|
73
|
+
function copyScaffold(engineDir, dir, name, version) {
|
|
74
|
+
const root = join(engineDir, SCAFFOLD_DIR);
|
|
75
|
+
if (!existsSync(root)) {
|
|
76
|
+
// RELEASE ORDER: the engine must ship the scaffold BEFORE a CLI that expects
|
|
77
|
+
// it. If this CLI ever reaches a creator ahead of that engine, `create`
|
|
78
|
+
// resolves the latest engine, finds no starter game, and every create on
|
|
79
|
+
// earth fails — so say exactly what is wrong, and never blame the creator's
|
|
80
|
+
// setup for our release ordering.
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Looop engine ${version} ships no starter game — it predates engine-owned scaffolding. ` +
|
|
83
|
+
'This is a platform-side issue, not something you did: please report it (`npx looop feedback`) or retry shortly.',
|
|
84
|
+
);
|
|
68
85
|
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const entries = readdirSync(TEMPLATE_DIR, { withFileTypes: true, recursive: true }).filter((e) => e.isFile());
|
|
73
|
-
for (const entry of entries) {
|
|
74
|
-
const rel = relative(TEMPLATE_DIR, join(entry.parentPath, entry.name));
|
|
86
|
+
for (const entry of readdirSync(root, { withFileTypes: true, recursive: true })) {
|
|
87
|
+
if (!entry.isFile()) continue;
|
|
88
|
+
const rel = relative(root, join(entry.parentPath, entry.name));
|
|
75
89
|
const dest = join(dir, rel === 'gitignore' ? '.gitignore' : rel);
|
|
76
90
|
mkdirSync(dirname(dest), { recursive: true });
|
|
77
|
-
writeFileSync(dest, readFileSync(join(
|
|
91
|
+
writeFileSync(dest, readFileSync(join(root, rel), 'utf8').replaceAll('{{name}}', name));
|
|
78
92
|
}
|
|
79
93
|
}
|
|
80
94
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
log = console.log,
|
|
88
|
-
} = {}) {
|
|
89
|
-
if (!name || !NAME_OK.test(name)) {
|
|
90
|
-
throw new Error(
|
|
91
|
-
`"${name ?? ''}" won't work as a game name — use lowercase letters, digits, and dashes (it becomes the URL: play.looop.games/g/<name>).`,
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
const dir = join(cwd, name);
|
|
95
|
-
if (existsSync(dir)) throw new Error(`${dir} already exists — pick another name or remove it first.`);
|
|
96
|
-
|
|
97
|
-
mkdirSync(dir, { recursive: true });
|
|
98
|
-
copyTemplate(dir, name);
|
|
99
|
-
// Non-Claude agent CLIs (Codex, Gemini/Antigravity, Cursor, opencode)
|
|
100
|
-
// discover project skills at .agents/skills/ — alias it to the same files.
|
|
101
|
-
// Created here, not shipped in the template: npm pack can't carry symlinks.
|
|
102
|
-
try {
|
|
103
|
-
mkdirSync(join(dir, '.agents'), { recursive: true });
|
|
104
|
-
symlinkSync(join('..', '.claude', 'skills'), join(dir, '.agents', 'skills'), 'dir');
|
|
105
|
-
} catch {
|
|
106
|
-
// e.g. Windows without symlink rights — Claude Code (and any tool that
|
|
107
|
-
// reads .claude/skills/ directly) still works.
|
|
108
|
-
}
|
|
109
|
-
// The engine is deliberately NOT a dependency (Q4 revision): it installs
|
|
110
|
-
// from the platform's login-gated registry on first `looop dev`. The
|
|
111
|
-
// VERSION is pinned here at create time (public lookup, warn-fallback) so
|
|
112
|
-
// the initial commit already carries it — see resolveEnginePin above.
|
|
113
|
-
const enginePin = await resolveEnginePin(apiBase, log);
|
|
95
|
+
async function scaffold({ dir, name, install, cliSpec, apiBase, log, ensure, reconcile }) {
|
|
96
|
+
// Written FIRST, and without the engine pin: npm needs a package.json to
|
|
97
|
+
// install into, and `ensureEngine` writes the pin itself once it knows which
|
|
98
|
+
// version it actually installed. ONE writer for the pin — the old code had
|
|
99
|
+
// `create` resolve it and `ensureEngine` resolve it again, which is precisely
|
|
100
|
+
// how they came to disagree.
|
|
114
101
|
writeFileSync(
|
|
115
102
|
join(dir, 'package.json'),
|
|
116
103
|
JSON.stringify(
|
|
@@ -121,28 +108,50 @@ export async function create({
|
|
|
121
108
|
scripts: { dev: 'looop dev', publish: 'looop publish', test: 'looop test' },
|
|
122
109
|
devDependencies: { '@looop-games/cli': cliSpec, playwright: PLAYWRIGHT_SPEC },
|
|
123
110
|
allowScripts: ALLOW_SCRIPTS,
|
|
124
|
-
...(enginePin ? { looop: { engine: enginePin } } : {}),
|
|
125
111
|
},
|
|
126
112
|
null,
|
|
127
113
|
2,
|
|
128
114
|
) + '\n',
|
|
129
115
|
);
|
|
130
116
|
|
|
131
|
-
// The template we just copied IS this repo's agent surface, so record it as
|
|
132
|
-
// ours now (Slice 2 / Q2). Without this, the first `looop dev` would meet a
|
|
133
|
-
// two-minute-old scaffold with no ownership record, treat it as a legacy
|
|
134
|
-
// pre-Slice-2 repo, and noisily "adopt" it. From here the artifact drives it.
|
|
135
|
-
recordSeededSurface(dir, enginePin);
|
|
136
|
-
|
|
137
117
|
if (install) {
|
|
138
118
|
log(`Installing the CLI (npm install in ${name}/)…`);
|
|
139
119
|
runNpm(['install', '--no-audit', '--no-fund'], { cwd: dir, stdio: 'pipe' });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The engine — and with it the starter game and the agent skills. Signs the
|
|
123
|
+
// creator in via the device flow if this machine has no token yet, and writes
|
|
124
|
+
// the `looop.engine` pin.
|
|
125
|
+
const engine = await ensure(dir, { apiBase, log });
|
|
126
|
+
|
|
127
|
+
copyScaffold(engine.dir, dir, name, engine.version);
|
|
128
|
+
|
|
129
|
+
// The agent surface is PLATFORM-owned and reconciled on every `looop update`,
|
|
130
|
+
// so it is placed here by the very code that will later update it — reading
|
|
131
|
+
// the artifact, recording the real hash of what landed. That identity is the
|
|
132
|
+
// whole point: a fresh repo's first `update` is now a genuine no-op, because
|
|
133
|
+
// the bytes on disk and the bytes in the ownership record came from the same
|
|
134
|
+
// artifact.
|
|
135
|
+
reconcile(dir, engine.dir, { log });
|
|
136
|
+
|
|
137
|
+
// Non-Claude agent CLIs (Codex, Gemini/Antigravity, Cursor, opencode)
|
|
138
|
+
// discover project skills at .agents/skills/ — alias it to the same files.
|
|
139
|
+
// After reconcile, so the link is never dangling.
|
|
140
|
+
try {
|
|
141
|
+
mkdirSync(join(dir, '.agents'), { recursive: true });
|
|
142
|
+
symlinkSync(join('..', '.claude', 'skills'), join(dir, '.agents', 'skills'), 'dir');
|
|
143
|
+
} catch {
|
|
144
|
+
// e.g. Windows without symlink rights — Claude Code (and any tool that
|
|
145
|
+
// reads .claude/skills/ directly) still works.
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (install) {
|
|
140
149
|
// Fetch the smoke-test browser NOW (cached machine-wide per version) so
|
|
141
150
|
// the first `looop test` never stalls on a surprise download mid-build.
|
|
142
151
|
// Resolved + run via node directly — NEVER a nested `npx`: when create
|
|
143
|
-
// itself runs under `npm exec --package=<tarball>` (the stranger
|
|
144
|
-
//
|
|
145
|
-
//
|
|
152
|
+
// itself runs under `npm exec --package=<tarball>` (the stranger gesture),
|
|
153
|
+
// the inherited npm_config_* env makes an inner npx try to re-install the
|
|
154
|
+
// tarball and cancel (caught live, 2026-07-10).
|
|
146
155
|
try {
|
|
147
156
|
log('Fetching the test browser (Chromium — one-time download, cached for every game)…');
|
|
148
157
|
const req = createRequire(join(dir, 'package.json'));
|
|
@@ -152,12 +161,14 @@ export async function create({
|
|
|
152
161
|
log('⚠️ Could not fetch Chromium (offline?) — `looop test` will fetch it when first needed.');
|
|
153
162
|
}
|
|
154
163
|
}
|
|
164
|
+
|
|
155
165
|
try {
|
|
156
166
|
execFileSync('git', ['init', '-q'], { cwd: dir, stdio: 'pipe' });
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
167
|
+
// The initial commit is the first revert baseline (rejecting milestone 1
|
|
168
|
+
// resets to it) and what lets `git worktree add` work — on a never-committed
|
|
169
|
+
// repo it silently creates an empty orphan lane instead. It carries the
|
|
170
|
+
// engine pin AND the true agent surface, so a reset to baseline cannot
|
|
171
|
+
// silently change either.
|
|
161
172
|
execFileSync('git', ['add', '-A'], { cwd: dir, stdio: 'pipe' });
|
|
162
173
|
const msg = ['commit', '-q', '-m', 'initial scaffold (looop create)'];
|
|
163
174
|
try {
|
|
@@ -175,8 +186,43 @@ export async function create({
|
|
|
175
186
|
}
|
|
176
187
|
|
|
177
188
|
log('');
|
|
178
|
-
log(`✅ ${name} is ready.`);
|
|
179
|
-
log(` cd ${
|
|
180
|
-
|
|
181
|
-
|
|
189
|
+
log(`✅ ${name} is ready (Looop engine ${engine.version}).`);
|
|
190
|
+
log(` cd ${name} && npx looop dev`);
|
|
191
|
+
return { dir, engineVersion: engine.version };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function create({
|
|
195
|
+
name,
|
|
196
|
+
cwd = process.cwd(),
|
|
197
|
+
install = true,
|
|
198
|
+
cliSpec = process.env.LOOOP_CREATE_CLI_SPEC || '^0.1.0',
|
|
199
|
+
apiBase = getApiBase(DEFAULT_API_BASE),
|
|
200
|
+
log = console.log,
|
|
201
|
+
// Seams, mirroring `looop update` — the tests drive a real artifact on disk
|
|
202
|
+
// through the real reconciler, with no network and no account.
|
|
203
|
+
ensure = ensureEngine,
|
|
204
|
+
reconcile = reconcileAgentSurface,
|
|
205
|
+
} = {}) {
|
|
206
|
+
if (!name || !NAME_OK.test(name)) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`"${name ?? ''}" won't work as a game name — use lowercase letters, digits, and dashes (it becomes the URL: play.looop.games/g/<name>).`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
const dir = join(cwd, name);
|
|
212
|
+
if (existsSync(dir)) throw new Error(`${dir} already exists — pick another name or remove it first.`);
|
|
213
|
+
|
|
214
|
+
mkdirSync(dir, { recursive: true });
|
|
215
|
+
try {
|
|
216
|
+
return await scaffold({ dir, name, install, cliSpec, apiBase, log, ensure, reconcile });
|
|
217
|
+
} catch (err) {
|
|
218
|
+
// ATOMIC. Everything a creator receives now arrives over the network behind
|
|
219
|
+
// a login, so a failure anywhere leaves a folder with no game, no skills and
|
|
220
|
+
// no initial commit — one that `looop dev` cannot run, and that `create`
|
|
221
|
+
// would refuse to retry into ("already exists"). A folder that looks like a
|
|
222
|
+
// game and isn't one is worse than no folder at all: it fails later,
|
|
223
|
+
// somewhere more confusing. Take it back, and say so.
|
|
224
|
+
rmSync(dir, { recursive: true, force: true });
|
|
225
|
+
log(`\n⚠️ Could not finish creating ${name} — removed the incomplete folder so you can retry cleanly.`);
|
|
226
|
+
throw err;
|
|
227
|
+
}
|
|
182
228
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@looop-games/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
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",
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"bin",
|
|
12
12
|
"lib",
|
|
13
|
-
"template",
|
|
14
13
|
"!lib/**/*.test.mjs"
|
|
15
14
|
],
|
|
16
15
|
"engines": {
|
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: build
|
|
3
|
-
description: The Looop build loop — the one way game work happens in this repo. Use when the creator wants to make their game, add or change a feature, start something new, or continue where a previous session left off ("let's build", "keep going", "add X", "make it so Y"). Runs from a plan in notes/plans/ — continues the plan if one exists, creates one if not.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# /build — the loop that builds this game
|
|
7
|
-
|
|
8
|
-
You are building for a creator who is probably not a developer. They bring the
|
|
9
|
-
vision and the judgment calls; you bring everything else. This skill is the
|
|
10
|
-
process — one repo, one game, one loop:
|
|
11
|
-
|
|
12
|
-
> **plan → discuss → build in milestones of auto-verified steps → the creator
|
|
13
|
-
> playtests → offer publish**
|
|
14
|
-
|
|
15
|
-
Two words carry this loop, sized by the words themselves:
|
|
16
|
-
|
|
17
|
-
- A **milestone** is a big, vertical increment the creator can actually play —
|
|
18
|
-
the loop's unit of *their* attention. **The creator playtests at every
|
|
19
|
-
milestone.**
|
|
20
|
-
- A **step** is a small build increment inside a milestone. **Every step is
|
|
21
|
-
auto-verified by you** — the creator is never asked to approve steps.
|
|
22
|
-
|
|
23
|
-
## When to collapse the ceremony
|
|
24
|
-
|
|
25
|
-
A trivial direct request ("make the ball a bit faster") doesn't need the full
|
|
26
|
-
loop: make the change, verify it, one line in the plan's log if a plan exists.
|
|
27
|
-
The loop is for real increments — new mechanics, features, session-scale work.
|
|
28
|
-
The tell: if the creator will want to *play* the result to judge it, it's a
|
|
29
|
-
milestone and it runs through the loop.
|
|
30
|
-
|
|
31
|
-
**Collapse never applies when there is no plan yet.** Starting something new
|
|
32
|
-
runs through the §2 discussion — the plan records the creator's answers, so
|
|
33
|
-
it cannot exist before they've answered.
|
|
34
|
-
|
|
35
|
-
## 1. Anchor on the plan
|
|
36
|
-
|
|
37
|
-
Every build runs from a **plan** — a file in `notes/plans/`. A build without a
|
|
38
|
-
plan doesn't exist; the plan is the loop's spine and the next session's recap.
|
|
39
|
-
|
|
40
|
-
- **A plan exists** (look in `notes/plans/`) → CONTINUE it. Read it fully —
|
|
41
|
-
vision, decisions, the milestone checklist, the log — and pick up exactly
|
|
42
|
-
where it left off. Don't make the creator repeat themselves.
|
|
43
|
-
- **No plan** → START one (create `notes/plans/<YYYY-MM-DD-HHMM>-<short-name>.md`
|
|
44
|
-
— creation datetime first, always, so plans list in order and the newest is
|
|
45
|
-
findable at a glance), through the discussion below. A `/todo` being
|
|
46
|
-
promoted becomes a plan the same way.
|
|
47
|
-
|
|
48
|
-
Plan shape (keep it lean — decisions and why, never pasted code):
|
|
49
|
-
|
|
50
|
-
```markdown
|
|
51
|
-
---
|
|
52
|
-
status: in_progress # in_progress | done | paused | abandoned
|
|
53
|
-
created: YYYY-MM-DD
|
|
54
|
-
updated: YYYY-MM-DD
|
|
55
|
-
---
|
|
56
|
-
# <What we're building>
|
|
57
|
-
|
|
58
|
-
## Vision
|
|
59
|
-
What this wants to be at its best, in the creator's words.
|
|
60
|
-
|
|
61
|
-
## Decisions
|
|
62
|
-
- **<choice>** — what was decided and why (one entry per real decision).
|
|
63
|
-
|
|
64
|
-
## Out of scope
|
|
65
|
-
- **<cut>** — why it's out (one line per cut). As load-bearing as Decisions:
|
|
66
|
-
every "we could also…" that got trimmed lands here, so it stays trimmed.
|
|
67
|
-
|
|
68
|
-
## Milestones
|
|
69
|
-
### Milestone 1 — <name> _(playtest: what the creator checks)_
|
|
70
|
-
- [x] step
|
|
71
|
-
- [ ] step
|
|
72
|
-
|
|
73
|
-
## Log
|
|
74
|
-
- YYYY-MM-DD — what happened, in a few lines. Newest first.
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
## 2. Discuss before building
|
|
78
|
-
|
|
79
|
-
Before code, get the shape right with the creator — and record it in the plan.
|
|
80
|
-
|
|
81
|
-
**Building (§3) runs only from a plan whose decisions are settled — and a
|
|
82
|
-
decision is settled by the creator answering, never by you choosing for
|
|
83
|
-
them.** A new build request therefore starts as a conversation, one decision
|
|
84
|
-
at a time:
|
|
85
|
-
|
|
86
|
-
- **One decision per turn.** Pose it, resolve it fully, record it in the
|
|
87
|
-
plan, then move to the next. Never a batch of questions — and never a
|
|
88
|
-
finished plan (or code, or tests) as your opening move.
|
|
89
|
-
- **Explain the problem before the options.** The creator can't choose
|
|
90
|
-
between options they don't have the picture for. If they ask "what is X?",
|
|
91
|
-
stop and explain X cleanly; the decision waits. If they say "I don't
|
|
92
|
-
understand", don't restate the options louder — back up and re-explain the
|
|
93
|
-
underlying problem from scratch, with concrete examples.
|
|
94
|
-
- **2–3 options with honest pros/cons, and your lean stated** — make it easy
|
|
95
|
-
to say "do that" or override you. Don't stack the deck, and don't fake
|
|
96
|
-
confidence on a genuine 50/50: phrase the case for the other option clearly
|
|
97
|
-
enough that the creator can pick it up if they disagree.
|
|
98
|
-
- **Lock it in visibly** — say "Locking in: <choice>" and write it into the
|
|
99
|
-
plan's Decisions as it lands, so the record never lags the conversation.
|
|
100
|
-
|
|
101
|
-
**Every decision that shapes the game reaches the creator, framed by its
|
|
102
|
-
consequences** — the way an engineer briefs a product manager. Not "authority
|
|
103
|
-
vs. client prediction?" but "if X, joining mid-game is instant but scores can
|
|
104
|
-
briefly disagree; with Y it's the reverse — which matters more here?" Give 2–3
|
|
105
|
-
options with honest trade-offs and your lean. The technical detail stays
|
|
106
|
-
available underneath for whoever pulls the thread.
|
|
107
|
-
|
|
108
|
-
**Silently decide only what is consequence-free for the creator** — naming,
|
|
109
|
-
file layout, code structure. The test is *"is the creator the right person to
|
|
110
|
-
answer this?"*, not "is this technical?" A technical fork with a consequence
|
|
111
|
-
they'd care about (feel, fairness, what can break) goes to them; one with no
|
|
112
|
-
creator-visible consequence does not. Never hide a real fork to keep things
|
|
113
|
-
simple — there is always a non-confusing way to ask it.
|
|
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
|
-
|
|
141
|
-
**Default to the smallest build that meets the creator's ask — never inflate
|
|
142
|
-
scope.** Every "we could also…" and every richer-than-asked option is scope
|
|
143
|
-
YOU are injecting; the creator can't push back on over-building they didn't
|
|
144
|
-
ask for. Surface extras as explicitly optional, record what was ruled out in
|
|
145
|
-
the plan (an "Out of scope" line per cut), and when a discussion has stacked
|
|
146
|
-
several decisions, recap the accumulated size so the creator can trim.
|
|
147
|
-
|
|
148
|
-
Then cut the work into **milestones**: each one vertical, end-to-end, playable
|
|
149
|
-
on its own. The FIRST milestone is the thinnest playable thing that proves the
|
|
150
|
-
riskiest idea — if the core bet is wrong, the creator learns it after one
|
|
151
|
-
milestone, not after the whole build. Write the milestones into the plan, each
|
|
152
|
-
with its `_(playtest: …)_` note.
|
|
153
|
-
|
|
154
|
-
## 3. Build a milestone
|
|
155
|
-
|
|
156
|
-
Run the steps **autonomously — no approval gates between steps**, just brief
|
|
157
|
-
progress notes:
|
|
158
|
-
|
|
159
|
-
1. Consult what's already known before writing new systems: `handbook/`
|
|
160
|
-
(this game's locked truths), the engine practices
|
|
161
|
-
(`node_modules/@looop-games/engine/shared/practices/`), and `/engine` for
|
|
162
|
-
components that already exist — don't reinvent a library.
|
|
163
|
-
2. Where a behaviour has a real contract, write the failing test/smoke first,
|
|
164
|
-
then the code.
|
|
165
|
-
3. **After every step, verify it yourself — run `/qa` on what the step
|
|
166
|
-
changed.** The steps themselves live in the engine's master QA doc
|
|
167
|
-
(`node_modules/@looop-games/engine/shared/practices/qa.md`); `/qa` reads
|
|
168
|
-
and runs them — **including the review rows: fresh sub-agent reviewers
|
|
169
|
-
(code-quality + test-thoroughness) on the step's diff, findings resolved
|
|
170
|
-
red→green before the step counts as done.** Every automated step runs at
|
|
171
|
-
step cadence; only the playtest is per-milestone. You are the only tester
|
|
172
|
-
inside a milestone — act like it.
|
|
173
|
-
4. **If a fix doesn't land on the first re-test, diagnose — don't
|
|
174
|
-
second-guess.** Revert it, add instrumentation, find the actual cause;
|
|
175
|
-
never ship a second guess stacked on the first.
|
|
176
|
-
5. **A real fork discovered mid-step goes to the creator**, framed by its
|
|
177
|
-
consequences like every other game-shaping decision — never silently
|
|
178
|
-
resolved just because the build is rolling.
|
|
179
|
-
6. **Never narrow scope silently.** Build the full milestone that was agreed;
|
|
180
|
-
if a constraint forces something smaller, surface it at the gate — don't
|
|
181
|
-
quietly ship the lesser version.
|
|
182
|
-
7. Tick the step off in the plan as it lands.
|
|
183
|
-
|
|
184
|
-
## 3½. Close the milestone — before the creator ever sees it
|
|
185
|
-
|
|
186
|
-
Every step already passed its own `/qa` — including the per-step review
|
|
187
|
-
sub-agents. The milestone close is the whole-greater-than-parts check before
|
|
188
|
-
the ONE manual gate: run the full **`npx looop test`** suite (everything, not
|
|
189
|
-
just what the last step touched), re-drive the milestone's headline behaviour
|
|
190
|
-
end-to-end yourself, and for anything LLM-authored/generative run the
|
|
191
|
-
**verdict-mix** row (judged scenarios, never a green check). Anything found
|
|
192
|
-
here is fixed **red→green** — the failing test first, then the fix. Only then
|
|
193
|
-
hand back.
|
|
194
|
-
|
|
195
|
-
## 4. The playtest gate
|
|
196
|
-
|
|
197
|
-
When the milestone's steps are done and verified, hand it to the creator —
|
|
198
|
-
this is the one place the loop stops for a human:
|
|
199
|
-
|
|
200
|
-
- **The game is already RUNNING when you hand back — you run the server,
|
|
201
|
-
never the creator.** If the dev stack isn't up, start it yourself
|
|
202
|
-
(`npx looop dev`, backgrounded) and confirm the game URL answers before
|
|
203
|
-
handing over; if one is already running, reuse it. The creator gets a
|
|
204
|
-
**link to click** — `http://localhost:8000/games/<name>/index.html` (plus
|
|
205
|
-
the phone/LAN URL when touch is part of the playtest) — never a command
|
|
206
|
-
to paste. Asking them to run a server is asking them to learn infra.
|
|
207
|
-
- **A minimal checklist of genuinely human-judgment items** — feel, look,
|
|
208
|
-
"does this play the way you imagined". Never hand them a check you could
|
|
209
|
-
have run yourself; you already ran those, report the results instead.
|
|
210
|
-
|
|
211
|
-
Outcomes:
|
|
212
|
-
|
|
213
|
-
- **It lands** → save it (a commit — `milestone: <name>`), check it off in
|
|
214
|
-
the plan, update the log, move on. The saves at accepted milestones are the
|
|
215
|
-
game's restore points.
|
|
216
|
-
- **Close, needs iteration** → adjust within the milestone and hand back.
|
|
217
|
-
- **Wrong direction** → offer the revert plainly, no sunk-cost defence:
|
|
218
|
-
back to the last save (`git reset --hard` — nothing was saved mid-milestone,
|
|
219
|
-
so that's the last accepted state).
|
|
220
|
-
- **The playtest caught a defect you missed** → that's a hole in the
|
|
221
|
-
verification, not just a bug. Fix it, then run `/handbook` so this
|
|
222
|
-
class of defect gets an automated check and never reaches a playtest again.
|
|
223
|
-
|
|
224
|
-
## 5. Offer publish — never publish on your own
|
|
225
|
-
|
|
226
|
-
After a milestone lands, offer it: `npx looop publish` puts the game live at
|
|
227
|
-
`play.looop.games/g/<name>`. Publishing is **always the creator's explicit
|
|
228
|
-
call** — `/build` never runs it unprompted. For a variant the creator wants to
|
|
229
|
-
compare against the live game, `npx looop publish --slug <alt>` publishes an
|
|
230
|
-
isolated A/B copy (own URL, own room) without touching the real one.
|
|
231
|
-
|
|
232
|
-
## Parallel lanes (advanced)
|
|
233
|
-
|
|
234
|
-
A creator can run several `/build` experiments on one game at once. A lane is
|
|
235
|
-
just this loop in a git worktree:
|
|
236
|
-
|
|
237
|
-
```bash
|
|
238
|
-
# in the main checkout. git worktree needs the repo to have at least one
|
|
239
|
-
# commit (on a never-saved repo it silently creates an EMPTY orphan lane) —
|
|
240
|
-
# this saves ONLY in that case, and is a no-op otherwise:
|
|
241
|
-
git rev-parse HEAD >/dev/null 2>&1 || { git add -A && git commit -m "save: initial"; }
|
|
242
|
-
|
|
243
|
-
git worktree add ../<name>-<lane> # one folder per experiment
|
|
244
|
-
cd ../<name>-<lane> && npm install # node_modules is gitignored — without
|
|
245
|
-
# this, npx falls through to a WRONG
|
|
246
|
-
# public 'looop' package and crashes
|
|
247
|
-
# with a misleading error
|
|
248
|
-
npx looop dev --port 8010 # DISTINCT --port per lane (8010, 8020, …)
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
- **The lane serves `/games/<worktree-folder-name>/…`** — the folder name, not
|
|
252
|
-
the original game slug. Use the URL the dev banner prints. The folder name
|
|
253
|
-
also keys the lane's multiplayer room.
|
|
254
|
-
- Every lane needs its own `--port`: the port shifts the whole stack together
|
|
255
|
-
— game, multiplayer, services — so lanes are fully isolated (distinct
|
|
256
|
-
server processes per lane; `looop test` inside one lane picks its own free
|
|
257
|
-
ports and doesn't disturb the others).
|
|
258
|
-
- Each lane keeps its own plan progress; save/playtest semantics are
|
|
259
|
-
unchanged inside a lane.
|
|
260
|
-
- Compare lanes live with `publish --slug <lane>` variants.
|
|
261
|
-
- Land the winner: save it in the lane (`git add -A && git commit`), then from
|
|
262
|
-
the main checkout `git merge <lane-branch>`, then
|
|
263
|
-
`git worktree remove ../<name>-<lane>` and delete the lane branch.
|
|
264
|
-
- **If the merge conflicts, stop — never auto-resolve.** Show the creator
|
|
265
|
-
what collided (in game terms: "both lanes changed how jumping feels") and
|
|
266
|
-
resolve it with them.
|