@looop-games/cli 0.1.5 → 0.1.7
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 +294 -0
- package/lib/create.mjs +7 -0
- package/lib/update.mjs +74 -15
- package/package.json +1 -1
- package/template/.claude/skills/build/SKILL.md +27 -1
- package/template/.claude/skills/engine/SKILL.md +42 -1
- package/template/.claude/skills/handbook/SKILL.md +142 -0
- package/template/.claude/skills/qa/SKILL.md +1 -1
- package/template/.claude/skills/update-looop/SKILL.md +74 -0
- package/template/AGENTS.md +14 -7
- package/template/handbook/design.md +3 -2
- package/template/handbook/feel.md +1 -1
- package/template/handbook/qa.md +1 -1
- package/template/.claude/skills/update-handbook/SKILL.md +0 -69
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// The agent surface — skills + the Layer-0 managed block — and how an update
|
|
2
|
+
// reaches a repo that already exists (creator-harness Slice 2, decision Q2).
|
|
3
|
+
//
|
|
4
|
+
// The surface ships INSIDE the versioned engine artifact, not the npm CLI: the
|
|
5
|
+
// CLI's template is only a create-time SEED (the engine is login-gated, so a
|
|
6
|
+
// fresh folder can't download one before its first `dev`). From then on the
|
|
7
|
+
// artifact is the source of truth. Without this, a creator's skills are frozen
|
|
8
|
+
// at the version they created the game with — the exact freeze Q2 exists to end.
|
|
9
|
+
//
|
|
10
|
+
// **This runs ONLY under `looop update`** (2026-07-11) — never on `dev`
|
|
11
|
+
// or `publish`. It COULD run there safely (it can only ever converge a repo to
|
|
12
|
+
// the engine version already pinned in its package.json, never a newer one),
|
|
13
|
+
// but a creator who ran `dev` and then found files they never wrote sitting in
|
|
14
|
+
// `git status` would be right to feel we'd gone through their pockets. Updates
|
|
15
|
+
// are something you ASK for. So this module writes nothing on its own schedule,
|
|
16
|
+
// and it narrates nothing either: it returns what it did, and `looop update`
|
|
17
|
+
// tells the creator, who decides whether to commit it.
|
|
18
|
+
//
|
|
19
|
+
// The one contract everything here serves:
|
|
20
|
+
//
|
|
21
|
+
// an update reaches a pre-existing repo without touching anything
|
|
22
|
+
// user-owned.
|
|
23
|
+
//
|
|
24
|
+
// So ownership is tracked explicitly, in `.looop/agent-surface.json` — the
|
|
25
|
+
// hash of every file WE wrote. On each run a file is one of:
|
|
26
|
+
//
|
|
27
|
+
// ours, unmodified (hash matches what we recorded) → replace with the new
|
|
28
|
+
// ours, edited (hash differs) → KEEP theirs, warn
|
|
29
|
+
// never ours (no record, but on disk) → KEEP theirs, warn
|
|
30
|
+
// not on disk → place ours
|
|
31
|
+
// retired upstream → remove, if still ours
|
|
32
|
+
//
|
|
33
|
+
// A skill we never placed is never even enumerated, so a creator's own skills
|
|
34
|
+
// are invisible to this code by construction. `AGENTS.md` is the exception that
|
|
35
|
+
// proves the rule: the managed block between the markers is ours to rewrite;
|
|
36
|
+
// everything outside them (their title, their instructions below the end
|
|
37
|
+
// marker) is theirs and is spliced back untouched.
|
|
38
|
+
import { createHash } from 'node:crypto';
|
|
39
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
40
|
+
import { dirname, join } from 'node:path';
|
|
41
|
+
|
|
42
|
+
export const SURFACE_DIR = 'agent-surface';
|
|
43
|
+
export const LOCAL_MANIFEST = '.looop/agent-surface.json';
|
|
44
|
+
|
|
45
|
+
const START = '<!-- looop:managed:start -->';
|
|
46
|
+
const END = '<!-- looop:managed:end -->';
|
|
47
|
+
|
|
48
|
+
const sha = (buf) => createHash('sha256').update(buf).digest('hex');
|
|
49
|
+
|
|
50
|
+
// Where each artifact path lands in the game repo. `skills/**` is the whole
|
|
51
|
+
// surface today; AGENTS.md is spliced rather than written whole.
|
|
52
|
+
function target(rel) {
|
|
53
|
+
if (rel === 'AGENTS.md') return 'AGENTS.md';
|
|
54
|
+
if (rel.startsWith('skills/')) return join('.claude', rel);
|
|
55
|
+
return null; // unknown surface entry from a newer engine — ignore, don't guess
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function projectName(projectDir) {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(readFileSync(join(projectDir, 'package.json'), 'utf8')).name || '';
|
|
61
|
+
} catch {
|
|
62
|
+
return '';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readLocal(projectDir) {
|
|
67
|
+
try {
|
|
68
|
+
const m = JSON.parse(readFileSync(join(projectDir, LOCAL_MANIFEST), 'utf8'));
|
|
69
|
+
return { engineVersion: m.engineVersion ?? null, placed: m.placed ?? {} };
|
|
70
|
+
} catch {
|
|
71
|
+
return null; // no manifest = a pre-Slice-2 repo; see `adopt` below
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function writeLocal(projectDir, engineVersion, placed) {
|
|
76
|
+
const p = join(projectDir, LOCAL_MANIFEST);
|
|
77
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
78
|
+
writeFileSync(p, JSON.stringify({ engineVersion, placed }, null, 2) + '\n');
|
|
79
|
+
}
|
|
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
|
+
|
|
98
|
+
// Splice the artifact's managed block into the repo's AGENTS.md, preserving
|
|
99
|
+
// everything outside the markers. Returns null when the creator's file has no
|
|
100
|
+
// markers — they rewrote Layer 0 themselves, so it is theirs now.
|
|
101
|
+
function spliceManaged(current, incoming) {
|
|
102
|
+
const cs = current.indexOf(START);
|
|
103
|
+
const ce = current.indexOf(END);
|
|
104
|
+
if (cs === -1 || ce === -1 || ce < cs) return null;
|
|
105
|
+
|
|
106
|
+
const is = incoming.indexOf(START);
|
|
107
|
+
const ie = incoming.indexOf(END);
|
|
108
|
+
if (is === -1 || ie === -1) return null;
|
|
109
|
+
|
|
110
|
+
const block = incoming.slice(is, ie + END.length);
|
|
111
|
+
return current.slice(0, cs) + block + current.slice(ce + END.length);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Reconcile the game repo's agent surface against the installed engine artifact.
|
|
116
|
+
* Never throws: a malformed or absent surface must not block `looop dev`.
|
|
117
|
+
*/
|
|
118
|
+
export function reconcileAgentSurface(projectDir, engineDir, { log = console.log } = {}) {
|
|
119
|
+
const result = {
|
|
120
|
+
placed: [], removed: [], shadowed: [], backups: [],
|
|
121
|
+
adopted: false, skipped: false, engineVersion: null, backupDir: null,
|
|
122
|
+
};
|
|
123
|
+
const surface = join(engineDir, SURFACE_DIR);
|
|
124
|
+
|
|
125
|
+
let manifest;
|
|
126
|
+
try {
|
|
127
|
+
if (!existsSync(join(surface, 'manifest.json'))) {
|
|
128
|
+
result.skipped = true; // engine older than Slice 2 — it carries no surface
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
manifest = JSON.parse(readFileSync(join(surface, 'manifest.json'), 'utf8'));
|
|
132
|
+
} catch (err) {
|
|
133
|
+
log(`⚠️ Could not read the engine's agent surface (${err.message}) — skills left as they are.`);
|
|
134
|
+
result.skipped = true;
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const name = projectName(projectDir);
|
|
140
|
+
const render = (buf) => Buffer.from(buf.toString('utf8').replaceAll('{{name}}', name), 'utf8');
|
|
141
|
+
|
|
142
|
+
const prior = readLocal(projectDir);
|
|
143
|
+
// Adoption: a repo scaffolded before this mechanism existed has our files on
|
|
144
|
+
// disk but no record of them. We take ownership so it can ever be updated —
|
|
145
|
+
// and back up anything whose bytes diverge, so an edit we can't distinguish
|
|
146
|
+
// from ours is preserved rather than destroyed.
|
|
147
|
+
const adopt = prior === null;
|
|
148
|
+
result.adopted = adopt;
|
|
149
|
+
const placed = adopt ? {} : { ...prior.placed };
|
|
150
|
+
|
|
151
|
+
const backupRoot = join(projectDir, '.looop', `backup-${manifest.engineVersion}`);
|
|
152
|
+
const backups = [];
|
|
153
|
+
const backup = (rel) => {
|
|
154
|
+
const from = join(projectDir, rel);
|
|
155
|
+
if (!existsSync(from)) return;
|
|
156
|
+
const to = join(backupRoot, rel);
|
|
157
|
+
mkdirSync(dirname(to), { recursive: true });
|
|
158
|
+
cpSync(from, to, { recursive: true });
|
|
159
|
+
backups.push(rel);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// ── retired entries: remove what upstream dropped. This is the half that
|
|
163
|
+
// makes a RENAME possible on a repo that already exists — without it a
|
|
164
|
+
// replaced skill would linger next to its replacement forever. ─────────
|
|
165
|
+
for (const rel of manifest.retired ?? []) {
|
|
166
|
+
const dest = target(rel);
|
|
167
|
+
if (!dest) continue;
|
|
168
|
+
const abs = join(projectDir, dest);
|
|
169
|
+
if (!existsSync(abs)) continue;
|
|
170
|
+
|
|
171
|
+
const owned = Object.keys(placed).some((p) => p === dest || p.startsWith(`${dest}/`));
|
|
172
|
+
if (!owned && !adopt) {
|
|
173
|
+
result.shadowed.push(dest);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (adopt) backup(dest);
|
|
177
|
+
rmSync(abs, { recursive: true, force: true });
|
|
178
|
+
for (const p of Object.keys(placed)) {
|
|
179
|
+
if (p === dest || p.startsWith(`${dest}/`)) delete placed[p];
|
|
180
|
+
}
|
|
181
|
+
result.removed.push(dest);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── the live surface ────────────────────────────────────────────────────
|
|
185
|
+
for (const rel of Object.keys(manifest.files ?? {})) {
|
|
186
|
+
const dest = target(rel);
|
|
187
|
+
if (!dest) continue;
|
|
188
|
+
const src = join(surface, rel);
|
|
189
|
+
if (!existsSync(src)) continue;
|
|
190
|
+
|
|
191
|
+
const abs = join(projectDir, dest);
|
|
192
|
+
const desired = render(readFileSync(src));
|
|
193
|
+
const exists = existsSync(abs);
|
|
194
|
+
const current = exists ? readFileSync(abs) : null;
|
|
195
|
+
const recorded = placed[dest];
|
|
196
|
+
|
|
197
|
+
if (dest === 'AGENTS.md') {
|
|
198
|
+
// The markers ARE the contract: inside is ours, outside is theirs.
|
|
199
|
+
const next = exists
|
|
200
|
+
? spliceManaged(current.toString('utf8'), desired.toString('utf8'))
|
|
201
|
+
: desired.toString('utf8');
|
|
202
|
+
if (next === null) {
|
|
203
|
+
result.shadowed.push(dest);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (exists && current.toString('utf8') === next) continue;
|
|
207
|
+
writeFileSync(abs, next);
|
|
208
|
+
placed[dest] = sha(Buffer.from(next, 'utf8'));
|
|
209
|
+
result.placed.push(dest);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (exists) {
|
|
214
|
+
const now = sha(current);
|
|
215
|
+
if (now === recorded) {
|
|
216
|
+
if (now === sha(desired)) continue; // already current
|
|
217
|
+
} else if (recorded === undefined && adopt) {
|
|
218
|
+
if (now !== sha(desired)) backup(dest);
|
|
219
|
+
} else {
|
|
220
|
+
// Either they edited ours, or it was never ours. Both are theirs.
|
|
221
|
+
result.shadowed.push(dest);
|
|
222
|
+
placed[dest] = recorded ?? now;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
228
|
+
writeFileSync(abs, desired);
|
|
229
|
+
placed[dest] = sha(desired);
|
|
230
|
+
result.placed.push(dest);
|
|
231
|
+
}
|
|
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
|
+
|
|
259
|
+
writeLocal(projectDir, manifest.engineVersion, placed);
|
|
260
|
+
result.engineVersion = manifest.engineVersion;
|
|
261
|
+
result.backups = backups;
|
|
262
|
+
result.backupDir = backups.length
|
|
263
|
+
? `${LOCAL_MANIFEST.replace('agent-surface.json', '')}backup-${manifest.engineVersion}/`
|
|
264
|
+
: null;
|
|
265
|
+
} catch (err) {
|
|
266
|
+
// A surface bug must never be the reason a creator can't run their game.
|
|
267
|
+
log(`⚠️ Could not sync the agent skills (${err.message}) — continuing; your game is unaffected.`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
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
|
@@ -19,6 +19,7 @@ import { join, relative, dirname } from 'node:path';
|
|
|
19
19
|
import { getApiBase } from './config.mjs';
|
|
20
20
|
import { DEFAULT_API_BASE } from './llm-shim.mjs';
|
|
21
21
|
import { runNpm } from './npm.mjs';
|
|
22
|
+
import { recordSeededSurface } from './agent-surface.mjs';
|
|
22
23
|
|
|
23
24
|
export const TEMPLATE_DIR = join(import.meta.dirname, '..', 'template');
|
|
24
25
|
|
|
@@ -127,6 +128,12 @@ export async function create({
|
|
|
127
128
|
) + '\n',
|
|
128
129
|
);
|
|
129
130
|
|
|
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
|
+
|
|
130
137
|
if (install) {
|
|
131
138
|
log(`Installing the CLI (npm install in ${name}/)…`);
|
|
132
139
|
runNpm(['install', '--no-audit', '--no-fund'], { cwd: dir, stdio: 'pipe' });
|
package/lib/update.mjs
CHANGED
|
@@ -1,15 +1,53 @@
|
|
|
1
|
-
// `looop update` — move this game to the latest engine
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// (
|
|
6
|
-
//
|
|
7
|
-
|
|
1
|
+
// `looop update` — move this game to the latest Looop: the engine bundle AND
|
|
2
|
+
// the agent surface (skills + the Layer-0 managed block) that rides it.
|
|
3
|
+
//
|
|
4
|
+
// This is the ONLY command that writes to `.claude/skills/` or `AGENTS.md`
|
|
5
|
+
// (creator-harness Slice 2 / Q2, scoped to update-only, 2026-07-11).
|
|
6
|
+
// `dev` and `publish` never touch them: an update is something a creator asks
|
|
7
|
+
// for, not something that happens to them while they were doing something else.
|
|
8
|
+
//
|
|
9
|
+
// Because it writes into files the creator has in git, it OWES them a full
|
|
10
|
+
// account of what it changed — every path, and whose it was. The `/update-looop`
|
|
11
|
+
// skill wraps this to walk them through the diff and offer the commit.
|
|
12
|
+
import { findProject, resolveEngine } from './project.mjs';
|
|
8
13
|
import { ensureEngine, readEnginePin, writeEnginePin } from './engine.mjs';
|
|
14
|
+
import { reconcileAgentSurface } from './agent-surface.mjs';
|
|
9
15
|
import { getToken, getApiBase } from './config.mjs';
|
|
10
16
|
import { login } from './login.mjs';
|
|
11
17
|
import { DEFAULT_API_BASE } from './llm-shim.mjs';
|
|
12
18
|
|
|
19
|
+
// The report is the point. A creator reading this must be able to answer, with
|
|
20
|
+
// no further digging: what changed, was any of it mine, and what do I do now.
|
|
21
|
+
function report(log, engineVersion, surface) {
|
|
22
|
+
const { placed = [], removed = [], shadowed = [], backups = [], backupDir } = surface;
|
|
23
|
+
if (!placed.length && !removed.length && !shadowed.length) {
|
|
24
|
+
log(' Your skills and instructions were already current.');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
log('');
|
|
29
|
+
log(`Looop also updated this game's skills and instructions to match engine ${engineVersion}:`);
|
|
30
|
+
for (const f of placed) log(` updated ${f}`);
|
|
31
|
+
for (const f of removed) log(` removed ${f} (retired by Looop)`);
|
|
32
|
+
for (const f of shadowed) log(` KEPT YOURS ${f} (you changed it — we left it alone)`);
|
|
33
|
+
|
|
34
|
+
if (backups.length) {
|
|
35
|
+
// On adoption we cannot tell an untouched old platform file from one the
|
|
36
|
+
// creator edited — there is no ownership record to compare against, which
|
|
37
|
+
// is precisely why we're adopting. Most of these are just the previous
|
|
38
|
+
// version's files. Claiming "you edited these" would be a lie that alarms;
|
|
39
|
+
// saying nothing would risk a silent overwrite. State only what is true.
|
|
40
|
+
log('');
|
|
41
|
+
log(` This game predates Looop's skill updates, so those files had no`);
|
|
42
|
+
log(` ownership record. Their previous contents are saved in ${backupDir}`);
|
|
43
|
+
log(` — if you had edited any of them, your version is in there.`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
log('');
|
|
47
|
+
log(" These are Looop's files inside your repo, so they show up in git as");
|
|
48
|
+
log(" changes you didn't make. Review them and save them with your next commit.");
|
|
49
|
+
}
|
|
50
|
+
|
|
13
51
|
export async function update({
|
|
14
52
|
cwd = process.cwd(),
|
|
15
53
|
apiBase = getApiBase(DEFAULT_API_BASE),
|
|
@@ -17,6 +55,7 @@ export async function update({
|
|
|
17
55
|
fetchImpl = fetch,
|
|
18
56
|
loginFn = login,
|
|
19
57
|
ensure = ensureEngine,
|
|
58
|
+
reconcile = reconcileAgentSurface,
|
|
20
59
|
} = {}) {
|
|
21
60
|
const project = findProject(cwd);
|
|
22
61
|
const from = readEnginePin(project.dir);
|
|
@@ -36,16 +75,36 @@ export async function update({
|
|
|
36
75
|
const { latest } = await res.json();
|
|
37
76
|
if (!latest) throw new Error('the platform has no downloadable engine releases yet.');
|
|
38
77
|
|
|
78
|
+
let engineDir = null;
|
|
79
|
+
let updated = false;
|
|
80
|
+
|
|
39
81
|
if (from === latest) {
|
|
40
82
|
log(`✅ Engine ${latest} — already up to date.`);
|
|
41
|
-
|
|
83
|
+
// Reconcile anyway. A repo can sit on the latest engine and STILL have an
|
|
84
|
+
// out-of-date surface: one scaffolded before this mechanism existed has
|
|
85
|
+
// never had its skills adopted, and would otherwise wait forever for a
|
|
86
|
+
// release it already has.
|
|
87
|
+
try {
|
|
88
|
+
engineDir = resolveEngine(project.dir).dir;
|
|
89
|
+
} catch {
|
|
90
|
+
engineDir = null; // engine not installed yet — the next `dev` fetches it
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
// Rewrite the pin first; ensureEngine honors it (download → install → pin).
|
|
94
|
+
writeEnginePin(project.dir, latest);
|
|
95
|
+
const engine = await ensure(project.dir, { apiBase, log, fetchImpl });
|
|
96
|
+
engineDir = engine.dir ?? null;
|
|
97
|
+
updated = true;
|
|
98
|
+
log('');
|
|
99
|
+
log(`✅ Engine updated: ${from ?? '(none)'} → ${engine.version}`);
|
|
42
100
|
}
|
|
43
101
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
102
|
+
const surface = engineDir ? reconcile(project.dir, engineDir, { log }) : { skipped: true };
|
|
103
|
+
if (!surface.skipped) report(log, surface.engineVersion ?? latest, surface);
|
|
104
|
+
|
|
105
|
+
if (updated) {
|
|
106
|
+
log('');
|
|
107
|
+
log(' Republish (`npx looop publish`) when you want the live game on it.');
|
|
108
|
+
}
|
|
109
|
+
return { from, to: latest, updated, surface };
|
|
51
110
|
}
|
package/package.json
CHANGED
|
@@ -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 `/
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: engine
|
|
3
|
-
description: Discover what the Looop engine already provides before building something from scratch —
|
|
3
|
+
description: Discover what the Looop engine already provides before building something from scratch — its component library, craft docs, and how to customize engine behaviour. Read the index rather than assuming what exists. Use when the creator asks "can it do X?", when you're about to write a system a game engine would normally provide, or when tuning game feel.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# /engine — what Looop already gives you
|
|
@@ -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.
|
|
@@ -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 `/
|
|
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
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: update-looop
|
|
3
|
+
description: Update Looop itself — the engine, and the skills and instructions this repo runs on. Use when the creator asks for the latest Looop ("is there an update?", "update looop", "get the newest version"), when a Looop feature they've heard about is missing, or when something looks like a Looop bug that may already be fixed. Not for updating their GAME — that's just building.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /update-looop — get the latest Looop, and explain what changed
|
|
7
|
+
|
|
8
|
+
`npx looop update` moves this game to the newest Looop release. It is the **only**
|
|
9
|
+
command that writes to Looop's files inside this repo — `dev` and `publish` never
|
|
10
|
+
do. That is deliberate: an update is something the creator *asks* for.
|
|
11
|
+
|
|
12
|
+
But it does write into **their** repo. After it runs, `git status` shows changes
|
|
13
|
+
they did not make. **Your job is to make sure that is never a surprise.**
|
|
14
|
+
|
|
15
|
+
## Before
|
|
16
|
+
|
|
17
|
+
Tell them what an update touches, in one or two sentences:
|
|
18
|
+
|
|
19
|
+
- **The engine** — the game code they import from (`/shared/...`). Their game
|
|
20
|
+
keeps running on the old engine until they publish again, so an update is safe
|
|
21
|
+
to take at any time.
|
|
22
|
+
- **The skills and instructions** — the files in `.claude/skills/` and the Looop
|
|
23
|
+
section of `AGENTS.md`. These are Looop's, living in their repo.
|
|
24
|
+
- **Never their game.** Not `index.html`, not `game.js`, not `handbook/`, not
|
|
25
|
+
`notes/`. And never a skill they wrote, or an edit they made to one of ours —
|
|
26
|
+
where they've changed something, their version wins and the update says so.
|
|
27
|
+
|
|
28
|
+
If they have uncommitted work, say so first — the update's changes will land
|
|
29
|
+
alongside it, and a mixed diff is harder to read. Offer to save theirs first.
|
|
30
|
+
|
|
31
|
+
## Run it
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx looop update
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
It prints exactly what it did: every file updated, every file removed, and every
|
|
38
|
+
file it **kept because the creator had changed it**. Read that output — it is the
|
|
39
|
+
source of truth for the next step, not your assumptions about what a release
|
|
40
|
+
contains.
|
|
41
|
+
|
|
42
|
+
## After — the part that matters
|
|
43
|
+
|
|
44
|
+
**Report the change in their terms, not ours.** Don't paste the file list back at
|
|
45
|
+
them; tell them what is now different about what you and they can do:
|
|
46
|
+
|
|
47
|
+
> *"Looop updated to engine 0.1.8. The `/update-handbook` skill was replaced by a
|
|
48
|
+
> broader `/handbook` — same job, plus it now keeps your game's vision and design
|
|
49
|
+
> pillars. Your own `/my-thing` skill was left alone."*
|
|
50
|
+
|
|
51
|
+
Then **offer the commit** — never run it unasked:
|
|
52
|
+
|
|
53
|
+
> *"That changed 3 of Looop's files in your repo. Want me to save them?
|
|
54
|
+
> (`git add -A && git commit -m "update Looop to engine 0.1.8"`)"*
|
|
55
|
+
|
|
56
|
+
Two things to volunteer without being asked:
|
|
57
|
+
|
|
58
|
+
- **If they'd edited a Looop file**, the update kept *their* version and did not
|
|
59
|
+
apply ours. Say which file, and that they may want to re-apply their change on
|
|
60
|
+
top of the new one — otherwise they'll silently miss the update to that file
|
|
61
|
+
forever.
|
|
62
|
+
- **If the update mentions a backup folder** (`.looop/backup-<version>/`), that
|
|
63
|
+
is the pre-update copy of files whose history we couldn't be sure of. Tell them
|
|
64
|
+
it exists. They can delete it once they're happy.
|
|
65
|
+
|
|
66
|
+
## Rules
|
|
67
|
+
|
|
68
|
+
- **Never run `looop update` as a side effect of something else.** It is its own
|
|
69
|
+
act, with its own consent. If a bug looks like it might already be fixed
|
|
70
|
+
upstream, *propose* the update — don't just do it mid-build.
|
|
71
|
+
- **Never commit on their behalf** without asking, even when the only changes are
|
|
72
|
+
Looop's own files. It is their repo and their history.
|
|
73
|
+
- The live game at `play.looop.games` does **not** change until they run
|
|
74
|
+
`npx looop publish`. Say so — creators reasonably assume an update is live.
|
package/template/AGENTS.md
CHANGED
|
@@ -19,7 +19,7 @@ applies. This table routes only what has no skill:
|
|
|
19
19
|
| The creator wants… | Do this |
|
|
20
20
|
|---|---|
|
|
21
21
|
| To "save my work" | `git add -A && git commit` (and push, if the repo has a remote). Saving is local; it is **not** publishing. |
|
|
22
|
-
| To update Looop / the engine |
|
|
22
|
+
| To update Looop / the engine | `/update-looop`. It is the ONLY thing that writes Looop's files into this repo (`.claude/skills/`, the managed block of this file) — `dev` and `publish` never do, so an update is never a surprise in their git. The live game changes only on the next publish. |
|
|
23
23
|
| Better game feel (tuning speeds, jumps, timings) | The engine's **tweaks** library (`node_modules/@looop-games/engine/shared/ui/tweaks/`) + `shared/practices/feel.md`. |
|
|
24
24
|
| To change how an engine file behaves | Copy it to `overrides/shared/<same path>` and edit the copy — dev and publish serve your override instead of the official module. Imports stay `/shared/...`; never rewrite them. An override is a fork: that file stops receiving engine updates until you re-port it. |
|
|
25
25
|
|
|
@@ -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
|
|
52
|
-
engine at `node_modules/@looop-games/engine`
|
|
53
|
-
`looop dev` downloads the version pinned in
|
|
54
|
-
and reinstalls it if an `npm install` prunes
|
|
55
|
-
`node_modules` — use `overrides/shared/` (see the
|
|
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 `/
|
|
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
|
-
|
|
4
|
-
|
|
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. `/
|
|
6
|
+
locked. `/handbook` adds entries.
|
|
7
7
|
|
|
8
8
|
_Nothing locked yet._
|
package/template/handbook/qa.md
CHANGED
|
@@ -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; `/
|
|
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.
|