@open-tomato/rafa 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/dist/bundled/skills/dev-planner/SKILL.md +11 -0
- package/dist/cli.js +3607 -1715
- package/dist/{describe-66gth329.js → describe-vbwqxj1f.js} +3 -3
- package/dist/effort/store/index.js +4 -4
- package/dist/{index-zzvc0gkb.js → index-2yd8bber.js} +1292 -799
- package/dist/{index-4fwgqveh.js → index-4j1e3h26.js} +1 -1
- package/dist/{index-gt9acb1y.js → index-59x3zsh4.js} +2869 -2259
- package/dist/{index-7qgng1rf.js → index-azptzvp3.js} +945 -193
- package/dist/{index-21qkxk63.js → index-p1265xgj.js} +4 -33
- package/dist/{index-twbp38kz.js → index-rv7y8y1j.js} +36 -2
- package/dist/{index-pmkgvtfe.js → index-sf2brh4w.js} +7 -1
- package/dist/{index-w1dnwja1.js → index-xwr54rzt.js} +2 -2
- package/dist/index.js +16 -16
- package/dist/plan/index.js +3 -3
- package/package.json +1 -1
|
@@ -1,31 +1,37 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
CONFIG_DEFAULTS,
|
|
4
|
+
CONFIG_FILE,
|
|
4
5
|
FINDING_DOMAINS,
|
|
5
6
|
FINDING_KINDS,
|
|
6
7
|
FINDING_SIGNALS,
|
|
7
8
|
activeOutput,
|
|
8
9
|
activeOutputMode,
|
|
10
|
+
configFilePath,
|
|
9
11
|
createTextOutput,
|
|
12
|
+
isStampableStub,
|
|
10
13
|
readRafaBlocks
|
|
11
|
-
} from "./index-
|
|
14
|
+
} from "./index-rv7y8y1j.js";
|
|
12
15
|
import {
|
|
13
16
|
bless,
|
|
14
17
|
merge
|
|
15
18
|
} from "./index-d6d0942q.js";
|
|
16
19
|
import {
|
|
17
20
|
commandSpelling
|
|
18
|
-
} from "./index-
|
|
21
|
+
} from "./index-4j1e3h26.js";
|
|
19
22
|
import {
|
|
23
|
+
BUSY_TIMEOUT_MAX_MS,
|
|
24
|
+
BUSY_TIMEOUT_MIN_MS,
|
|
20
25
|
RAFA_INSTINCTS_PATH,
|
|
21
26
|
STORE_BACKENDS,
|
|
22
27
|
bundledBinDirectory,
|
|
23
28
|
describeValue,
|
|
29
|
+
isBusyTimeoutMs,
|
|
24
30
|
isMapping,
|
|
25
31
|
messageOf,
|
|
26
32
|
resolveInstinctScopes,
|
|
27
33
|
tierExists
|
|
28
|
-
} from "./index-
|
|
34
|
+
} from "./index-sf2brh4w.js";
|
|
29
35
|
import {
|
|
30
36
|
actionHash,
|
|
31
37
|
triggerKey
|
|
@@ -34,6 +40,9 @@ import {
|
|
|
34
40
|
// src/adapters/registry.ts
|
|
35
41
|
import { homedir } from "os";
|
|
36
42
|
|
|
43
|
+
// src/effort/store/ndjson.ts
|
|
44
|
+
import { join as join3 } from "path";
|
|
45
|
+
|
|
37
46
|
// src/effort/store.ts
|
|
38
47
|
import {
|
|
39
48
|
appendFileSync,
|
|
@@ -53,8 +62,8 @@ function asObject(value) {
|
|
|
53
62
|
function normaliseKey(key) {
|
|
54
63
|
return typeof key === "string" && key.length > 0 ? key : null;
|
|
55
64
|
}
|
|
56
|
-
function
|
|
57
|
-
return
|
|
65
|
+
function effortStoreFileName(kind) {
|
|
66
|
+
return STORE_FILE_NAMES[kind];
|
|
58
67
|
}
|
|
59
68
|
function readStoreText(path) {
|
|
60
69
|
return existsSync(path) ? readFileSync(path, "utf8") : null;
|
|
@@ -141,6 +150,54 @@ function appendRows(path, rows, keyOf) {
|
|
|
141
150
|
};
|
|
142
151
|
}
|
|
143
152
|
|
|
153
|
+
// src/effort/store/location.ts
|
|
154
|
+
import { existsSync as existsSync2, realpathSync } from "fs";
|
|
155
|
+
import { tmpdir } from "os";
|
|
156
|
+
import { isAbsolute, join as join2, relative, resolve, sep } from "path";
|
|
157
|
+
var EFFORT_DIR_VARIABLE = "RAFA_EFFORT_DIR";
|
|
158
|
+
var TEST_PROCESS_VARIABLE = "RAFA_TEST";
|
|
159
|
+
var TEST_ENTRY_SUFFIX = ".test.ts";
|
|
160
|
+
function canonical(path) {
|
|
161
|
+
const resolved = resolve(path);
|
|
162
|
+
return existsSync2(resolved) ? realpathSync(resolved) : resolved;
|
|
163
|
+
}
|
|
164
|
+
function isWithin(path, dir) {
|
|
165
|
+
const between = relative(resolve(dir), resolve(path));
|
|
166
|
+
const climbsOut = between === ".." || between.startsWith(`..${sep}`);
|
|
167
|
+
return !climbsOut && !isAbsolute(between);
|
|
168
|
+
}
|
|
169
|
+
function readEffortDirOverride(repoRoot, env = process.env) {
|
|
170
|
+
const value = env[EFFORT_DIR_VARIABLE];
|
|
171
|
+
if (value === undefined || value === "")
|
|
172
|
+
return null;
|
|
173
|
+
if (!isAbsolute(value)) {
|
|
174
|
+
throw new Error(`effort store: ${EFFORT_DIR_VARIABLE} is not an absolute path (${value}); run this command again` + ` with ${EFFORT_DIR_VARIABLE} set to the absolute path 'rafa effort copy' printed.`);
|
|
175
|
+
}
|
|
176
|
+
if (canonical(value) === canonical(join2(repoRoot, EFFORT_STORE_DIR))) {
|
|
177
|
+
throw new Error(`effort store: ${EFFORT_DIR_VARIABLE} names this project's own store (${value}); it points a command` + " at a copy only. Copy the store with 'rafa effort copy' and run this command with" + ` ${EFFORT_DIR_VARIABLE}=<the copy>.`);
|
|
178
|
+
}
|
|
179
|
+
return value;
|
|
180
|
+
}
|
|
181
|
+
function effortStoreDir(repoRoot, env = process.env) {
|
|
182
|
+
return readEffortDirOverride(repoRoot, env) ?? join2(repoRoot, EFFORT_STORE_DIR);
|
|
183
|
+
}
|
|
184
|
+
function isTestProcess(main = Bun.main, env = process.env) {
|
|
185
|
+
return main.endsWith(TEST_ENTRY_SUFFIX) || env[TEST_PROCESS_VARIABLE] === "1";
|
|
186
|
+
}
|
|
187
|
+
function isUnderTempDir(path, tempDir = tmpdir()) {
|
|
188
|
+
return isWithin(path, tempDir) || isWithin(path, canonical(tempDir));
|
|
189
|
+
}
|
|
190
|
+
function currentProbe() {
|
|
191
|
+
return { main: Bun.main, env: process.env, tempDir: tmpdir() };
|
|
192
|
+
}
|
|
193
|
+
function guardTestProcess(path, probe = currentProbe()) {
|
|
194
|
+
if (!isTestProcess(probe.main, probe.env))
|
|
195
|
+
return;
|
|
196
|
+
if (isUnderTempDir(path, probe.tempDir))
|
|
197
|
+
return;
|
|
198
|
+
throw new Error(`effort store: a test opened ${resolve(path)}, outside the temp directory ${probe.tempDir};` + " a test opens stores under tmpdir() only");
|
|
199
|
+
}
|
|
200
|
+
|
|
144
201
|
// src/effort/store/types.ts
|
|
145
202
|
function keyOrNull(value) {
|
|
146
203
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
@@ -151,15 +208,23 @@ var EFFORT_KEY_PROJECTIONS = {
|
|
|
151
208
|
};
|
|
152
209
|
|
|
153
210
|
// src/effort/store/ndjson.ts
|
|
211
|
+
function kindPath(repoRoot, kind) {
|
|
212
|
+
return join3(effortStoreDir(repoRoot), effortStoreFileName(kind));
|
|
213
|
+
}
|
|
214
|
+
function openedKindPath(repoRoot, kind) {
|
|
215
|
+
const path = kindPath(repoRoot, kind);
|
|
216
|
+
guardTestProcess(path);
|
|
217
|
+
return path;
|
|
218
|
+
}
|
|
154
219
|
function readKind(repoRoot, kind) {
|
|
155
|
-
return readStoreRows(
|
|
220
|
+
return readStoreRows(openedKindPath(repoRoot, kind));
|
|
156
221
|
}
|
|
157
222
|
function keysOfKind(repoRoot, kind) {
|
|
158
|
-
const path =
|
|
223
|
+
const path = openedKindPath(repoRoot, kind);
|
|
159
224
|
return collectedKeys(path, EFFORT_KEY_PROJECTIONS[kind]);
|
|
160
225
|
}
|
|
161
226
|
function appendKind(repoRoot, kind, rows) {
|
|
162
|
-
const path =
|
|
227
|
+
const path = openedKindPath(repoRoot, kind);
|
|
163
228
|
return appendRows(path, rows, EFFORT_KEY_PROJECTIONS[kind]);
|
|
164
229
|
}
|
|
165
230
|
function openNdjsonStore(repoRoot) {
|
|
@@ -167,19 +232,461 @@ function openNdjsonStore(repoRoot) {
|
|
|
167
232
|
append: (kind, rows) => appendKind(repoRoot, kind, rows),
|
|
168
233
|
keys: (kind) => keysOfKind(repoRoot, kind),
|
|
169
234
|
read: (kind) => readKind(repoRoot, kind).rows,
|
|
170
|
-
path: (kind) =>
|
|
235
|
+
path: (kind) => kindPath(repoRoot, kind),
|
|
171
236
|
readRows: (kind) => readKind(repoRoot, kind)
|
|
172
237
|
};
|
|
173
238
|
}
|
|
174
239
|
|
|
175
240
|
// src/effort/store/sqlite.ts
|
|
176
|
-
import { existsSync as
|
|
177
|
-
import { dirname as
|
|
241
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
242
|
+
import { dirname as dirname6, join as join7 } from "path";
|
|
178
243
|
import { Database } from "bun:sqlite";
|
|
179
244
|
|
|
245
|
+
// package.json
|
|
246
|
+
var name = "@open-tomato/rafa";
|
|
247
|
+
var version = "0.26.0";
|
|
248
|
+
|
|
249
|
+
// src/cli/version.ts
|
|
250
|
+
var RAFA_VERSION = version;
|
|
251
|
+
function versionLine() {
|
|
252
|
+
return `rafa ${RAFA_VERSION}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/effort/store/development-build.ts
|
|
256
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
257
|
+
import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute3 } from "path";
|
|
258
|
+
|
|
259
|
+
// src/loop/sessions.ts
|
|
260
|
+
import {
|
|
261
|
+
linkSync,
|
|
262
|
+
mkdirSync as mkdirSync2,
|
|
263
|
+
readdirSync,
|
|
264
|
+
readFileSync as readFileSync2,
|
|
265
|
+
renameSync,
|
|
266
|
+
rmSync,
|
|
267
|
+
writeFileSync
|
|
268
|
+
} from "fs";
|
|
269
|
+
import { basename, dirname as dirname3, join as join5 } from "path";
|
|
270
|
+
|
|
271
|
+
// src/project/scope.ts
|
|
272
|
+
import { existsSync as existsSync3, realpathSync as realpathSync2 } from "fs";
|
|
273
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join4 } from "path";
|
|
274
|
+
var SCOPE_DIR = dirname2(CONFIG_FILE);
|
|
275
|
+
var INIT_COMMAND = "rafa init";
|
|
276
|
+
var DISK_FILE_SYSTEM = Object.freeze({
|
|
277
|
+
exists: (path) => existsSync3(path),
|
|
278
|
+
realpath: (path) => realpathSync2(path)
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
class ScopeError extends Error {
|
|
282
|
+
constructor(message, options) {
|
|
283
|
+
super(`rafa scope: ${message}`, options);
|
|
284
|
+
this.name = "ScopeError";
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function scopeAt(base) {
|
|
288
|
+
return { dir: join4(base, SCOPE_DIR), configFile: configFilePath(base) };
|
|
289
|
+
}
|
|
290
|
+
function initHint(start, passedUserConfig = null) {
|
|
291
|
+
const passed = passedUserConfig === null ? [] : [`${passedUserConfig} is the user scope's config and marks no project`];
|
|
292
|
+
return [
|
|
293
|
+
`not inside a rafa project: no ${CONFIG_FILE} in ${start} or any directory above it`,
|
|
294
|
+
...passed,
|
|
295
|
+
`run \`${INIT_COMMAND}\` to set one up`
|
|
296
|
+
].join(`
|
|
297
|
+
`);
|
|
298
|
+
}
|
|
299
|
+
function requireAbsolute(label, path) {
|
|
300
|
+
if (!isAbsolute2(path)) {
|
|
301
|
+
throw new ScopeError(`${label} is ${describeValue(path)}, expected an absolute path`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function realStartOf(start, fs) {
|
|
305
|
+
try {
|
|
306
|
+
return fs.realpath(start);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
const message = `start directory ${start} does not resolve (${messageOf(error)})`;
|
|
309
|
+
throw new ScopeError(message, { cause: error });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function realHomeOf(home, fs) {
|
|
313
|
+
try {
|
|
314
|
+
return fs.realpath(home);
|
|
315
|
+
} catch {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function selfAndAncestors(path) {
|
|
320
|
+
const parent = dirname2(path);
|
|
321
|
+
return parent === path ? [path] : [path, ...selfAndAncestors(parent)];
|
|
322
|
+
}
|
|
323
|
+
function walkUp(realStart, realHome, fs) {
|
|
324
|
+
let passedUserConfig = false;
|
|
325
|
+
for (const dir of selfAndAncestors(realStart)) {
|
|
326
|
+
if (!fs.exists(configFilePath(dir)))
|
|
327
|
+
continue;
|
|
328
|
+
if (dir !== realHome)
|
|
329
|
+
return { root: dir, passedUserConfig };
|
|
330
|
+
passedUserConfig = true;
|
|
331
|
+
}
|
|
332
|
+
return { root: null, passedUserConfig };
|
|
333
|
+
}
|
|
334
|
+
function resolveScope(start, seams) {
|
|
335
|
+
const { home, fs = DISK_FILE_SYSTEM } = seams;
|
|
336
|
+
requireAbsolute("start directory", start);
|
|
337
|
+
requireAbsolute("home directory", home);
|
|
338
|
+
const user = scopeAt(home);
|
|
339
|
+
const walk = walkUp(realStartOf(start, fs), realHomeOf(home, fs), fs);
|
|
340
|
+
if (walk.root === null) {
|
|
341
|
+
const passed = walk.passedUserConfig ? user.configFile : null;
|
|
342
|
+
return { found: false, start, home, user, hint: initHint(start, passed) };
|
|
343
|
+
}
|
|
344
|
+
return { found: true, root: walk.root, home, project: scopeAt(walk.root), user };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/loop/sessions.ts
|
|
348
|
+
var SESSION_STATES = Object.freeze(["running", "paused", "stopped", "done"]);
|
|
349
|
+
var RUNS_DIR = "runs";
|
|
350
|
+
var RECORD_EXTENSION = ".json";
|
|
351
|
+
var SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
352
|
+
|
|
353
|
+
class SessionRecordError extends Error {
|
|
354
|
+
file;
|
|
355
|
+
constructor(file, problem) {
|
|
356
|
+
super(`session record ${file}: ${problem}`);
|
|
357
|
+
this.name = "SessionRecordError";
|
|
358
|
+
this.file = file;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
class SessionConflictError extends Error {
|
|
363
|
+
conflicts;
|
|
364
|
+
constructor(conflicts) {
|
|
365
|
+
const named = conflicts.map((conflict) => `${conflict.record.sessionId} (${conflict.reason})`);
|
|
366
|
+
super(`the plan's sessions refuse the run: ${named.join(", ")}`);
|
|
367
|
+
this.name = "SessionConflictError";
|
|
368
|
+
this.conflicts = conflicts;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
class SessionStateError extends Error {
|
|
373
|
+
file;
|
|
374
|
+
state;
|
|
375
|
+
constructor(file, state, onlyFrom) {
|
|
376
|
+
const acted = onlyFrom.length === 0 ? "no state" : onlyFrom.join(" or ");
|
|
377
|
+
super(`session record ${file}: stores ${state}, and the change acts on ${acted} alone`);
|
|
378
|
+
this.name = "SessionStateError";
|
|
379
|
+
this.file = file;
|
|
380
|
+
this.state = state;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function isSessionId(value) {
|
|
384
|
+
return SESSION_ID_PATTERN.test(value);
|
|
385
|
+
}
|
|
386
|
+
function runsDir(root) {
|
|
387
|
+
return join5(scopeAt(root).dir, RUNS_DIR);
|
|
388
|
+
}
|
|
389
|
+
function sessionFilePath(root, sessionId) {
|
|
390
|
+
if (!isSessionId(sessionId)) {
|
|
391
|
+
throw new Error(`session record: unusable session id ${JSON.stringify(sessionId)}`);
|
|
392
|
+
}
|
|
393
|
+
return join5(runsDir(root), `${sessionId}${RECORD_EXTENSION}`);
|
|
394
|
+
}
|
|
395
|
+
function describeValue2(value) {
|
|
396
|
+
return value === undefined ? "missing" : JSON.stringify(value) ?? String(value);
|
|
397
|
+
}
|
|
398
|
+
function field(fields, key) {
|
|
399
|
+
return Object.hasOwn(fields, key) ? fields[key] : undefined;
|
|
400
|
+
}
|
|
401
|
+
function isObject(value) {
|
|
402
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
403
|
+
}
|
|
404
|
+
function isPositiveWhole(value) {
|
|
405
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
406
|
+
}
|
|
407
|
+
function isText(value) {
|
|
408
|
+
return typeof value === "string" && value.trim() !== "";
|
|
409
|
+
}
|
|
410
|
+
function taskProblem(task) {
|
|
411
|
+
if (task === null)
|
|
412
|
+
return null;
|
|
413
|
+
if (!isObject(task))
|
|
414
|
+
return `task is ${describeValue2(task)}, expected null or an object`;
|
|
415
|
+
if (!isPositiveWhole(field(task, "line"))) {
|
|
416
|
+
return `task.line is ${describeValue2(field(task, "line"))}, expected a whole number from 1`;
|
|
417
|
+
}
|
|
418
|
+
return isText(field(task, "text")) ? null : `task.text is ${describeValue2(field(task, "text"))}, expected a non-empty string`;
|
|
419
|
+
}
|
|
420
|
+
function sessionIdProblem(sessionId, file) {
|
|
421
|
+
if (typeof sessionId !== "string" || !isSessionId(sessionId)) {
|
|
422
|
+
return `sessionId is ${describeValue2(sessionId)}, expected a plain file name`;
|
|
423
|
+
}
|
|
424
|
+
return basename(file) === `${sessionId}${RECORD_EXTENSION}` ? null : `sessionId ${JSON.stringify(sessionId)} is not the file's name`;
|
|
425
|
+
}
|
|
426
|
+
function planStubProblem(planStub) {
|
|
427
|
+
return planStub === null || typeof planStub === "string" && isStampableStub(planStub) ? null : `planStub is ${describeValue2(planStub)}, expected null or a plan stub`;
|
|
428
|
+
}
|
|
429
|
+
function textProblems(fields) {
|
|
430
|
+
return ["plan", "branch"].filter((key) => !isText(field(fields, key))).map((key) => `${key} is ${describeValue2(field(fields, key))}, expected a non-empty string`);
|
|
431
|
+
}
|
|
432
|
+
function recordProblems(fields, file) {
|
|
433
|
+
const pid = field(fields, "pid");
|
|
434
|
+
const startedAt = field(fields, "startedAt");
|
|
435
|
+
const state = field(fields, "state");
|
|
436
|
+
const problems = [
|
|
437
|
+
sessionIdProblem(field(fields, "sessionId"), file),
|
|
438
|
+
planStubProblem(field(fields, "planStub")),
|
|
439
|
+
...textProblems(fields),
|
|
440
|
+
isPositiveWhole(pid) ? null : `pid is ${describeValue2(pid)}, expected a whole number from 1`,
|
|
441
|
+
typeof startedAt === "string" && !Number.isNaN(Date.parse(startedAt)) ? null : `startedAt is ${describeValue2(startedAt)}, expected a timestamp`,
|
|
442
|
+
SESSION_STATES.includes(state) ? null : `state is ${describeValue2(state)}, expected one of ${SESSION_STATES.join(", ")}`,
|
|
443
|
+
taskProblem(field(fields, "task"))
|
|
444
|
+
];
|
|
445
|
+
return problems.filter((problem) => problem !== null);
|
|
446
|
+
}
|
|
447
|
+
function freezeRecord(fields) {
|
|
448
|
+
const task = field(fields, "task");
|
|
449
|
+
return Object.freeze({
|
|
450
|
+
sessionId: field(fields, "sessionId"),
|
|
451
|
+
planStub: field(fields, "planStub"),
|
|
452
|
+
plan: field(fields, "plan"),
|
|
453
|
+
branch: field(fields, "branch"),
|
|
454
|
+
pid: field(fields, "pid"),
|
|
455
|
+
startedAt: field(fields, "startedAt"),
|
|
456
|
+
state: field(fields, "state"),
|
|
457
|
+
task: isObject(task) ? Object.freeze({ line: field(task, "line"), text: field(task, "text") }) : null
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
function parseSessionRecord(text, file) {
|
|
461
|
+
let parsed;
|
|
462
|
+
try {
|
|
463
|
+
parsed = JSON.parse(text);
|
|
464
|
+
} catch (error) {
|
|
465
|
+
throw new SessionRecordError(file, `holds no JSON: ${messageOf(error)}`);
|
|
466
|
+
}
|
|
467
|
+
if (!isObject(parsed))
|
|
468
|
+
throw new SessionRecordError(file, "holds no JSON object");
|
|
469
|
+
const problems = recordProblems(parsed, file);
|
|
470
|
+
if (problems.length > 0)
|
|
471
|
+
throw new SessionRecordError(file, problems.join("; "));
|
|
472
|
+
return freezeRecord(parsed);
|
|
473
|
+
}
|
|
474
|
+
function errorCode(error) {
|
|
475
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : null;
|
|
476
|
+
}
|
|
477
|
+
function isPidAlive(pid) {
|
|
478
|
+
if (!isPositiveWhole(pid))
|
|
479
|
+
return false;
|
|
480
|
+
try {
|
|
481
|
+
process.kill(pid, 0);
|
|
482
|
+
return true;
|
|
483
|
+
} catch (error) {
|
|
484
|
+
return errorCode(error) === "EPERM";
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function readState(record, isAlive = isPidAlive) {
|
|
488
|
+
if (record.state === "stopped" || record.state === "done")
|
|
489
|
+
return record.state;
|
|
490
|
+
return isAlive(record.pid) ? record.state : "stopped";
|
|
491
|
+
}
|
|
492
|
+
function readRecordFile(file) {
|
|
493
|
+
let text;
|
|
494
|
+
try {
|
|
495
|
+
text = readFileSync2(file, "utf8");
|
|
496
|
+
} catch (error) {
|
|
497
|
+
throw new SessionRecordError(file, `cannot be read: ${messageOf(error)}`);
|
|
498
|
+
}
|
|
499
|
+
return parseSessionRecord(text, file);
|
|
500
|
+
}
|
|
501
|
+
function recordNames(dir) {
|
|
502
|
+
try {
|
|
503
|
+
return readdirSync(dir);
|
|
504
|
+
} catch (error) {
|
|
505
|
+
if (errorCode(error) === "ENOENT")
|
|
506
|
+
return [];
|
|
507
|
+
throw new SessionRecordError(dir, `cannot be listed: ${messageOf(error)}`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function byStart(a, b) {
|
|
511
|
+
return Date.parse(a.startedAt) - Date.parse(b.startedAt) || a.sessionId.localeCompare(b.sessionId);
|
|
512
|
+
}
|
|
513
|
+
function readSessions(root, seams = {}) {
|
|
514
|
+
const dir = runsDir(root);
|
|
515
|
+
const isAlive = seams.isAlive ?? isPidAlive;
|
|
516
|
+
const records = recordNames(dir).filter((name2) => name2.endsWith(RECORD_EXTENSION)).map((name2) => readRecordFile(join5(dir, name2))).map((record) => Object.freeze({ ...record, state: readState(record, isAlive) }));
|
|
517
|
+
return Object.freeze(records.sort(byStart));
|
|
518
|
+
}
|
|
519
|
+
function readSession(root, sessionId, seams = {}) {
|
|
520
|
+
const record = readRecordFile(sessionFilePath(root, sessionId));
|
|
521
|
+
return Object.freeze({ ...record, state: readState(record, seams.isAlive ?? isPidAlive) });
|
|
522
|
+
}
|
|
523
|
+
function samePlan(a, b) {
|
|
524
|
+
if (a.planStub !== null || b.planStub !== null)
|
|
525
|
+
return a.planStub === b.planStub;
|
|
526
|
+
return a.plan === b.plan;
|
|
527
|
+
}
|
|
528
|
+
function sessionConflicts(records, candidate) {
|
|
529
|
+
return records.filter((record) => samePlan(record, candidate)).flatMap((record) => {
|
|
530
|
+
if (record.branch !== candidate.branch)
|
|
531
|
+
return [{ reason: "branch", record }];
|
|
532
|
+
return record.state === "running" || record.state === "paused" ? [{ reason: "live", record }] : [];
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
function checkRecord(record, file) {
|
|
536
|
+
const problems = recordProblems(record, file);
|
|
537
|
+
if (problems.length > 0)
|
|
538
|
+
throw new SessionRecordError(file, `not written: ${problems.join("; ")}`);
|
|
539
|
+
}
|
|
540
|
+
function writeRecordFile(file, record, isNew) {
|
|
541
|
+
checkRecord(record, file);
|
|
542
|
+
mkdirSync2(dirname3(file), { recursive: true });
|
|
543
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
544
|
+
writeFileSync(temporary, `${JSON.stringify(record, null, 2)}
|
|
545
|
+
`);
|
|
546
|
+
try {
|
|
547
|
+
if (isNew)
|
|
548
|
+
linkSync(temporary, file);
|
|
549
|
+
else
|
|
550
|
+
renameSync(temporary, file);
|
|
551
|
+
} finally {
|
|
552
|
+
rmSync(temporary, { force: true });
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function beginSession(root, draft, seams = {}) {
|
|
556
|
+
const file = sessionFilePath(root, draft.sessionId);
|
|
557
|
+
const record = freezeRecord({ ...draft, state: "running", task: null });
|
|
558
|
+
const conflicts = sessionConflicts(readSessions(root, seams), record);
|
|
559
|
+
if (conflicts.length > 0)
|
|
560
|
+
throw new SessionConflictError(conflicts);
|
|
561
|
+
writeRecordFile(file, record, true);
|
|
562
|
+
return record;
|
|
563
|
+
}
|
|
564
|
+
function updateSession(root, sessionId, change) {
|
|
565
|
+
const file = sessionFilePath(root, sessionId);
|
|
566
|
+
const stored = readRecordFile(file);
|
|
567
|
+
if (change.onlyFrom !== undefined && !change.onlyFrom.includes(stored.state)) {
|
|
568
|
+
throw new SessionStateError(file, stored.state, change.onlyFrom);
|
|
569
|
+
}
|
|
570
|
+
const record = freezeRecord({
|
|
571
|
+
...stored,
|
|
572
|
+
state: change.state ?? stored.state,
|
|
573
|
+
task: change.task === undefined ? stored.task : change.task
|
|
574
|
+
});
|
|
575
|
+
writeRecordFile(file, record, false);
|
|
576
|
+
return record;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/runtime/identity.ts
|
|
580
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, realpathSync as realpathSync3 } from "fs";
|
|
581
|
+
import { dirname as dirname4, join as join6, resolve as resolve2 } from "path";
|
|
582
|
+
var MANIFEST = "package.json";
|
|
583
|
+
var CHECKOUT_ENTRY = join6("src", "rafa.ts");
|
|
584
|
+
function realOrResolved(path) {
|
|
585
|
+
const resolved = resolve2(path);
|
|
586
|
+
return existsSync4(resolved) ? realpathSync3(resolved) : resolved;
|
|
587
|
+
}
|
|
588
|
+
function manifestName(file) {
|
|
589
|
+
let parsed;
|
|
590
|
+
try {
|
|
591
|
+
parsed = JSON.parse(readFileSync3(file, "utf8"));
|
|
592
|
+
} catch {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
596
|
+
return null;
|
|
597
|
+
const name2 = parsed["name"];
|
|
598
|
+
return typeof name2 === "string" ? name2 : null;
|
|
599
|
+
}
|
|
600
|
+
function nearestRafaPackage(start) {
|
|
601
|
+
let dir = start;
|
|
602
|
+
for (;; ) {
|
|
603
|
+
const manifest = join6(dir, MANIFEST);
|
|
604
|
+
if (existsSync4(manifest) && manifestName(manifest) === name)
|
|
605
|
+
return dir;
|
|
606
|
+
const parent = dirname4(dir);
|
|
607
|
+
if (parent === dir)
|
|
608
|
+
return null;
|
|
609
|
+
dir = parent;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function readRuntimeIdentity(entry = Bun.main) {
|
|
613
|
+
const real = realOrResolved(entry);
|
|
614
|
+
const packageDir = nearestRafaPackage(dirname4(real));
|
|
615
|
+
if (packageDir !== null && existsSync4(join6(packageDir, CHECKOUT_ENTRY))) {
|
|
616
|
+
return { kind: "development", entry: real, checkout: packageDir };
|
|
617
|
+
}
|
|
618
|
+
return { kind: "installed", entry: real };
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/effort/store/development-build.ts
|
|
622
|
+
var DEVELOPMENT_NEXT_STEP = "rafa effort copy";
|
|
623
|
+
var ADOPTION_NAME = "schema_migrations";
|
|
624
|
+
var EFFORT_DIR_NAME = "effort";
|
|
625
|
+
var DEVELOPMENT_MARK = "+dev:";
|
|
626
|
+
function appliedByName(identity) {
|
|
627
|
+
return identity.kind === "development" ? `${RAFA_VERSION}${DEVELOPMENT_MARK}${identity.checkout}` : RAFA_VERSION;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
class DevelopmentBuildRefusedError extends Error {
|
|
631
|
+
path;
|
|
632
|
+
nextStep;
|
|
633
|
+
constructor(path, message) {
|
|
634
|
+
super(message);
|
|
635
|
+
this.name = "DevelopmentBuildRefusedError";
|
|
636
|
+
this.path = path;
|
|
637
|
+
this.nextStep = DEVELOPMENT_NEXT_STEP;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function storeProjectRoot(path) {
|
|
641
|
+
const storeDir = dirname5(path);
|
|
642
|
+
const scopeDir = dirname5(storeDir);
|
|
643
|
+
if (basename2(storeDir) !== EFFORT_DIR_NAME || basename2(scopeDir) !== SCOPE_DIR)
|
|
644
|
+
return null;
|
|
645
|
+
return dirname5(scopeDir);
|
|
646
|
+
}
|
|
647
|
+
function isOwnedStore(path, env, tempDir) {
|
|
648
|
+
if (isUnderTempDir(path, tempDir))
|
|
649
|
+
return true;
|
|
650
|
+
const override = env[EFFORT_DIR_VARIABLE];
|
|
651
|
+
if (override === undefined || override === "" || !isAbsolute3(override))
|
|
652
|
+
return false;
|
|
653
|
+
return storeProjectRoot(path) === null && isUnderTempDir(path, override);
|
|
654
|
+
}
|
|
655
|
+
function loopSentence(record) {
|
|
656
|
+
return ` Loop ${record.sessionId} (pid ${String(record.pid)}, plan ${record.planStub ?? record.plan}) is running on this store.`;
|
|
657
|
+
}
|
|
658
|
+
function liveLoopSentences(path, isAlive) {
|
|
659
|
+
const root = storeProjectRoot(path);
|
|
660
|
+
if (root === null)
|
|
661
|
+
return "";
|
|
662
|
+
try {
|
|
663
|
+
return readSessions(root, { isAlive }).filter((record) => record.state === "running" || record.state === "paused").map(loopSentence).join("");
|
|
664
|
+
} catch (error) {
|
|
665
|
+
return ` The loop records under ${runsDir(root)} could not be read (${messageOf(error)}).`;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function developmentRefusalText(path, needs, checkout, loops) {
|
|
669
|
+
return `effort store: ${path} needs migration ${needs.join(", ")} and this rafa is a development build (${checkout});` + ` a development build migrates only a store under the temp directory or ${EFFORT_DIR_VARIABLE}.` + ` Copy it with '${DEVELOPMENT_NEXT_STEP}' and run this command with ${EFFORT_DIR_VARIABLE}=<the copy>.` + loops;
|
|
670
|
+
}
|
|
671
|
+
function refuseUnownedDevelopmentWrite(path, needs, probe = {}) {
|
|
672
|
+
const identity = probe.identity ?? readRuntimeIdentity();
|
|
673
|
+
if (identity.kind !== "development")
|
|
674
|
+
return;
|
|
675
|
+
if (isOwnedStore(path, probe.env ?? process.env, probe.tempDir ?? tmpdir2()))
|
|
676
|
+
return;
|
|
677
|
+
const loops = liveLoopSentences(path, probe.isAlive ?? isPidAlive);
|
|
678
|
+
throw new DevelopmentBuildRefusedError(path, developmentRefusalText(path, needs, identity.checkout, loops));
|
|
679
|
+
}
|
|
680
|
+
|
|
180
681
|
// src/effort/store/migrations.ts
|
|
682
|
+
import { createHash } from "crypto";
|
|
683
|
+
var LEGACY_GATE_OPEN = 13;
|
|
684
|
+
var LEGACY_GATE_CLOSED = 1000;
|
|
181
685
|
var SQLITE_MIGRATIONS = [
|
|
182
|
-
|
|
686
|
+
{
|
|
687
|
+
id: "kind-tables",
|
|
688
|
+
breaks: [],
|
|
689
|
+
sql: `
|
|
183
690
|
CREATE TABLE sessions (
|
|
184
691
|
seq INTEGER PRIMARY KEY,
|
|
185
692
|
session_id TEXT NOT NULL UNIQUE CHECK (session_id <> ''),
|
|
@@ -191,8 +698,12 @@ var SQLITE_MIGRATIONS = [
|
|
|
191
698
|
sha TEXT NOT NULL UNIQUE CHECK (sha <> ''),
|
|
192
699
|
row_json TEXT NOT NULL
|
|
193
700
|
);
|
|
194
|
-
`,
|
|
195
701
|
`
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
id: "findings",
|
|
705
|
+
breaks: [],
|
|
706
|
+
sql: `
|
|
196
707
|
CREATE TABLE findings (
|
|
197
708
|
seq INTEGER PRIMARY KEY,
|
|
198
709
|
id TEXT NOT NULL UNIQUE CHECK (id <> ''),
|
|
@@ -220,8 +731,12 @@ var SQLITE_MIGRATIONS = [
|
|
|
220
731
|
CREATE UNIQUE INDEX findings_by_trigger_what
|
|
221
732
|
ON findings (session_id, trigger, what)
|
|
222
733
|
WHERE artifact IS NULL;
|
|
223
|
-
`,
|
|
224
734
|
`
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
id: "blockers-and-bugs",
|
|
738
|
+
breaks: [],
|
|
739
|
+
sql: `
|
|
225
740
|
CREATE TABLE blockers (
|
|
226
741
|
seq INTEGER PRIMARY KEY,
|
|
227
742
|
id TEXT NOT NULL UNIQUE CHECK (id <> ''),
|
|
@@ -252,8 +767,12 @@ var SQLITE_MIGRATIONS = [
|
|
|
252
767
|
|
|
253
768
|
CREATE UNIQUE INDEX out_of_scope_bugs_by_entry
|
|
254
769
|
ON out_of_scope_bugs (session_id, what, ifnull(artifact, ''), ifnull(security, -1));
|
|
255
|
-
`,
|
|
256
770
|
`
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
id: "report-absences",
|
|
774
|
+
breaks: [],
|
|
775
|
+
sql: `
|
|
257
776
|
CREATE TABLE report_absences (
|
|
258
777
|
seq INTEGER PRIMARY KEY,
|
|
259
778
|
id TEXT NOT NULL UNIQUE CHECK (id <> ''),
|
|
@@ -266,8 +785,12 @@ var SQLITE_MIGRATIONS = [
|
|
|
266
785
|
outcome TEXT NOT NULL,
|
|
267
786
|
collected_at TEXT NOT NULL
|
|
268
787
|
);
|
|
269
|
-
`,
|
|
270
788
|
`
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
id: "task-reports",
|
|
792
|
+
breaks: [],
|
|
793
|
+
sql: `
|
|
271
794
|
CREATE TABLE task_reports (
|
|
272
795
|
seq INTEGER PRIMARY KEY,
|
|
273
796
|
id TEXT NOT NULL UNIQUE CHECK (id <> ''),
|
|
@@ -278,8 +801,12 @@ var SQLITE_MIGRATIONS = [
|
|
|
278
801
|
outcome TEXT NOT NULL,
|
|
279
802
|
collected_at TEXT NOT NULL
|
|
280
803
|
);
|
|
281
|
-
`,
|
|
282
804
|
`
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
id: "preflight",
|
|
808
|
+
breaks: [],
|
|
809
|
+
sql: `
|
|
283
810
|
CREATE TABLE preflight (
|
|
284
811
|
seq INTEGER PRIMARY KEY,
|
|
285
812
|
run_id TEXT NOT NULL CHECK (run_id <> ''),
|
|
@@ -295,8 +822,12 @@ var SQLITE_MIGRATIONS = [
|
|
|
295
822
|
UNIQUE (run_id, position),
|
|
296
823
|
CHECK ((outcome = 'pass') = (failure IS NULL))
|
|
297
824
|
);
|
|
298
|
-
`,
|
|
299
825
|
`
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
id: "dispatches",
|
|
829
|
+
breaks: [],
|
|
830
|
+
sql: `
|
|
300
831
|
CREATE TABLE dispatches (
|
|
301
832
|
seq INTEGER PRIMARY KEY,
|
|
302
833
|
session_id TEXT NOT NULL UNIQUE CHECK (session_id <> ''),
|
|
@@ -311,8 +842,12 @@ var SQLITE_MIGRATIONS = [
|
|
|
311
842
|
flags TEXT NOT NULL,
|
|
312
843
|
collected_at TEXT NOT NULL
|
|
313
844
|
);
|
|
314
|
-
`,
|
|
315
845
|
`
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
id: "changes",
|
|
849
|
+
breaks: [],
|
|
850
|
+
sql: `
|
|
316
851
|
CREATE TABLE changes (
|
|
317
852
|
seq INTEGER PRIMARY KEY,
|
|
318
853
|
id TEXT NOT NULL UNIQUE CHECK (id <> ''),
|
|
@@ -328,20 +863,32 @@ var SQLITE_MIGRATIONS = [
|
|
|
328
863
|
|
|
329
864
|
CREATE UNIQUE INDEX changes_by_entry
|
|
330
865
|
ON changes (session_id, level, ifnull(area, ''), summary);
|
|
331
|
-
`,
|
|
332
866
|
`
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
id: "out-of-scope-bug-scope",
|
|
870
|
+
breaks: [],
|
|
871
|
+
sql: `
|
|
333
872
|
ALTER TABLE out_of_scope_bugs
|
|
334
873
|
ADD COLUMN scope TEXT CHECK (scope IN ('machine', 'rafa'));
|
|
335
|
-
`,
|
|
336
874
|
`
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
id: "dispatch-skills",
|
|
878
|
+
breaks: [],
|
|
879
|
+
sql: `
|
|
337
880
|
ALTER TABLE dispatches
|
|
338
881
|
ADD COLUMN resolver TEXT CHECK (resolver IN ('planner', 'tag', 'none'));
|
|
339
882
|
ALTER TABLE dispatches
|
|
340
883
|
ADD COLUMN skills_offered TEXT CHECK (skills_offered IS NULL OR json_type(skills_offered) = 'array');
|
|
341
884
|
ALTER TABLE dispatches
|
|
342
885
|
ADD COLUMN lessons_offered TEXT CHECK (lessons_offered IS NULL OR json_type(lessons_offered) = 'array');
|
|
343
|
-
`,
|
|
344
886
|
`
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
id: "skill-invocations",
|
|
890
|
+
breaks: [],
|
|
891
|
+
sql: `
|
|
345
892
|
CREATE TABLE skill_invocations (
|
|
346
893
|
seq INTEGER PRIMARY KEY,
|
|
347
894
|
session_id TEXT NOT NULL CHECK (session_id <> ''),
|
|
@@ -353,12 +900,20 @@ var SQLITE_MIGRATIONS = [
|
|
|
353
900
|
|
|
354
901
|
CREATE UNIQUE INDEX skill_invocations_by_use
|
|
355
902
|
ON skill_invocations (session_id, ifnull(name, ''), ifnull(sidechain, -1));
|
|
356
|
-
`,
|
|
357
903
|
`
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
id: "task-report-skills",
|
|
907
|
+
breaks: [],
|
|
908
|
+
sql: `
|
|
358
909
|
ALTER TABLE task_reports
|
|
359
910
|
ADD COLUMN skills_used TEXT CHECK (skills_used IS NULL OR json_type(skills_used) = 'array');
|
|
360
|
-
`,
|
|
361
911
|
`
|
|
912
|
+
},
|
|
913
|
+
{
|
|
914
|
+
id: "plan-ci",
|
|
915
|
+
breaks: [],
|
|
916
|
+
sql: `
|
|
362
917
|
CREATE TABLE plan_ci (
|
|
363
918
|
seq INTEGER PRIMARY KEY,
|
|
364
919
|
plan_stub TEXT NOT NULL CHECK (plan_stub <> ''),
|
|
@@ -371,13 +926,231 @@ var SQLITE_MIGRATIONS = [
|
|
|
371
926
|
CHECK ((verdict = 'red') = (json_array_length(failing) > 0))
|
|
372
927
|
);
|
|
373
928
|
`
|
|
929
|
+
}
|
|
374
930
|
];
|
|
375
931
|
var SQLITE_SCHEMA_VERSION = SQLITE_MIGRATIONS.length;
|
|
932
|
+
function legacyGate(applied) {
|
|
933
|
+
return applied.every(({ breaks }) => breaks.length === 0) ? LEGACY_GATE_OPEN : LEGACY_GATE_CLOSED;
|
|
934
|
+
}
|
|
935
|
+
function migrationChecksum(migration) {
|
|
936
|
+
return createHash("sha256").update(migration.sql, "utf8").digest("hex");
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// src/effort/store/schema-plan.ts
|
|
940
|
+
var FIX_SCHEMA_STEP = "rafa effort fix-schema --dry-run";
|
|
941
|
+
function sqliteCatalogue(migrations) {
|
|
942
|
+
return migrations.map(({ sql, ...spec }) => ({ ...spec, sha256: migrationChecksum({ sql }) }));
|
|
943
|
+
}
|
|
944
|
+
function planSchema(store, catalogue, access, caller) {
|
|
945
|
+
const { path, userVersion, log } = store;
|
|
946
|
+
if (log === null && (userVersion < 0 || userVersion > LEGACY_GATE_OPEN)) {
|
|
947
|
+
return refuse("pre-log-unreleased", FIX_SCHEMA_STEP, `effort store: ${path} is at schema version` + ` ${String(userVersion)} with no migration log; no released rafa wrote that.` + " Refusing to read or write it.");
|
|
948
|
+
}
|
|
949
|
+
if (log !== null && userVersion !== LEGACY_GATE_OPEN && userVersion !== LEGACY_GATE_CLOSED) {
|
|
950
|
+
return refuse("gate-mismatch", FIX_SCHEMA_STEP, `effort store: ${path} has a migration log and` + ` schema version ${String(userVersion)}; a build older than the log migrated it after the` + " log was made. Refusing to read or write it.");
|
|
951
|
+
}
|
|
952
|
+
const adopted = log === null ? adoptedEntries(catalogue, userVersion) : [];
|
|
953
|
+
const held = log ?? [];
|
|
954
|
+
const logRefusal = judgeLog(path, held, catalogue);
|
|
955
|
+
if (logRefusal !== null)
|
|
956
|
+
return logRefusal;
|
|
957
|
+
const heldIds = new Set([...adopted, ...held].map(({ id }) => id));
|
|
958
|
+
const pending = catalogue.filter(({ id }) => !heldIds.has(id));
|
|
959
|
+
const unknown = held.filter(({ id }) => !catalogue.some((entry) => entry.id === id));
|
|
960
|
+
const writes = access === "write" || pending.length > 0;
|
|
961
|
+
const writersBroken = unknown.find(({ breaks }) => breaks.includes("writers"));
|
|
962
|
+
if (writes && writersBroken !== undefined) {
|
|
963
|
+
return refuseUnknown("unknown-breaks-writers", path, writersBroken);
|
|
964
|
+
}
|
|
965
|
+
const orderRefusal = judgePending(path, catalogue, heldIds, pending, caller);
|
|
966
|
+
if (orderRefusal !== null)
|
|
967
|
+
return orderRefusal;
|
|
968
|
+
return {
|
|
969
|
+
verdict: "use",
|
|
970
|
+
adopted,
|
|
971
|
+
pending,
|
|
972
|
+
unknown,
|
|
973
|
+
gate: legacyGate([...adopted, ...held, ...pending])
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
function adoptedEntries(catalogue, count) {
|
|
977
|
+
const adopted = catalogue.slice(0, count);
|
|
978
|
+
if (adopted.length < count) {
|
|
979
|
+
throw new Error(`schema plan: the catalogue holds ${String(catalogue.length)} migrations,` + ` fewer than the ${String(count)} legacy entries a store at that version holds`);
|
|
980
|
+
}
|
|
981
|
+
return adopted;
|
|
982
|
+
}
|
|
983
|
+
function judgeLog(path, log, catalogue) {
|
|
984
|
+
const byId = new Map(catalogue.map((entry) => [entry.id, entry]));
|
|
985
|
+
for (const row of log) {
|
|
986
|
+
const known = byId.get(row.id);
|
|
987
|
+
if (known !== undefined && known.sha256 !== row.sha256) {
|
|
988
|
+
return refuse("edited", FIX_SCHEMA_STEP, `effort store: ${path} recorded migration ${row.id}` + ` as sha256 ${row.sha256}, and this rafa holds it as ${known.sha256}; one was edited` + " after it ran. Refusing to read or write the store.");
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
const readersBroken = log.find(({ id, breaks }) => !byId.has(id) && breaks.some((word) => word !== "writers"));
|
|
992
|
+
return readersBroken === undefined ? null : refuseUnknown("unknown-breaks-readers", path, readersBroken);
|
|
993
|
+
}
|
|
994
|
+
function judgePending(path, catalogue, heldIds, pending, caller) {
|
|
995
|
+
const breaking = pending.filter(({ breaks }) => breaks.length > 0);
|
|
996
|
+
for (const entry of breaking) {
|
|
997
|
+
const later = catalogue.slice(catalogue.indexOf(entry) + 1).find(({ id }) => heldIds.has(id));
|
|
998
|
+
if (later !== undefined) {
|
|
999
|
+
return refuse("breaking-out-of-order", "rafa effort schema", `effort store: ${path} needs` + ` migration ${entry.id} ahead of ${later.id}, which is already applied; a migration that` + " breaks older runtimes applies only in order. Refusing to read or write the store.");
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
const [first] = breaking;
|
|
1003
|
+
if (first === undefined || caller === "migrate")
|
|
1004
|
+
return null;
|
|
1005
|
+
return refuse("breaking-pending", "rafa effort migrate --dry-run", `effort store: ${path} needs` + ` migration ${first.id}, which breaks older runtimes and is applied only by` + " 'rafa effort migrate'; refusing to read or write the store until then.");
|
|
1006
|
+
}
|
|
1007
|
+
function refuseUnknown(reason, path, row) {
|
|
1008
|
+
const ending = reason === "unknown-breaks-readers" ? "which breaks older readers; refusing to read or write it." : "which breaks older writers; refusing to write it.";
|
|
1009
|
+
return refuse(reason, `install a rafa that knows ${row.id}, or ${FIX_SCHEMA_STEP}`, "effort store:" + ` ${path} holds migration ${row.id} (applied by ${row.appliedBy} on ${dateOf(row.appliedAt)}),` + ` which this rafa does not know and ${ending}`);
|
|
1010
|
+
}
|
|
1011
|
+
function dateOf(appliedAt) {
|
|
1012
|
+
return /^\d{4}-\d{2}-\d{2}/.test(appliedAt) ? appliedAt.slice(0, 10) : appliedAt;
|
|
1013
|
+
}
|
|
1014
|
+
function refuse(reason, nextStep, text) {
|
|
1015
|
+
return { verdict: "refuse", reason, message: `${text} Next safe step: ${nextStep}`, nextStep };
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// src/effort/store/bring-forward.ts
|
|
1019
|
+
var MIGRATION_LOG_TABLE = "schema_migrations";
|
|
1020
|
+
var CREATE_MIGRATION_LOG = `
|
|
1021
|
+
CREATE TABLE IF NOT EXISTS ${MIGRATION_LOG_TABLE} (
|
|
1022
|
+
seq INTEGER PRIMARY KEY,
|
|
1023
|
+
id TEXT NOT NULL UNIQUE CHECK (id <> ''),
|
|
1024
|
+
sha256 TEXT NOT NULL CHECK (length(sha256) = 64),
|
|
1025
|
+
breaks TEXT NOT NULL,
|
|
1026
|
+
applied_at TEXT NOT NULL,
|
|
1027
|
+
applied_by TEXT NOT NULL CHECK (applied_by <> '')
|
|
1028
|
+
);
|
|
1029
|
+
`;
|
|
1030
|
+
|
|
1031
|
+
class SchemaRefusedError extends Error {
|
|
1032
|
+
reason;
|
|
1033
|
+
nextStep;
|
|
1034
|
+
constructor(refusal) {
|
|
1035
|
+
super(refusal.message);
|
|
1036
|
+
this.name = "SchemaRefusedError";
|
|
1037
|
+
this.reason = refusal.reason;
|
|
1038
|
+
this.nextStep = refusal.nextStep;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
function bringForward(db, path, access, caller, options = {}) {
|
|
1042
|
+
const migrations = options.migrations ?? SQLITE_MIGRATIONS;
|
|
1043
|
+
const catalogue = sqliteCatalogue(migrations);
|
|
1044
|
+
const first = usablePlan(readStoreSchema(db, path), catalogue, access, caller);
|
|
1045
|
+
if (!hasWork(first))
|
|
1046
|
+
return nothingWritten(db);
|
|
1047
|
+
if (options.builtAside !== true)
|
|
1048
|
+
refuseUnownedDevelopmentWrite(path, writeNames(first), options);
|
|
1049
|
+
const apply = db.transaction(() => {
|
|
1050
|
+
const store = readStoreSchema(db, path);
|
|
1051
|
+
const plan = usablePlan(store, catalogue, access, caller);
|
|
1052
|
+
if (!hasWork(plan))
|
|
1053
|
+
return nothingWritten(db);
|
|
1054
|
+
const stamp = {
|
|
1055
|
+
appliedAt: (options.now ?? (() => new Date))().toISOString(),
|
|
1056
|
+
appliedBy: options.appliedBy ?? RAFA_VERSION
|
|
1057
|
+
};
|
|
1058
|
+
db.run(CREATE_MIGRATION_LOG);
|
|
1059
|
+
for (const entry of plan.adopted)
|
|
1060
|
+
logMigration(db, entry, stamp);
|
|
1061
|
+
for (const entry of plan.pending) {
|
|
1062
|
+
db.run(bodyOf(migrations, entry.id));
|
|
1063
|
+
logMigration(db, entry, stamp);
|
|
1064
|
+
}
|
|
1065
|
+
db.run(`PRAGMA user_version = ${String(plan.gate)}`);
|
|
1066
|
+
return {
|
|
1067
|
+
adopted: plan.adopted.map(({ id }) => id),
|
|
1068
|
+
applied: plan.pending.map(({ id }) => id),
|
|
1069
|
+
userVersion: plan.gate
|
|
1070
|
+
};
|
|
1071
|
+
});
|
|
1072
|
+
return apply.immediate();
|
|
1073
|
+
}
|
|
1074
|
+
function readStoreSchema(db, path) {
|
|
1075
|
+
return { path, userVersion: readUserVersion(db), log: readMigrationLog(db, path) };
|
|
1076
|
+
}
|
|
1077
|
+
function usablePlan(store, catalogue, access, caller) {
|
|
1078
|
+
const plan = planSchema(store, catalogue, access, caller);
|
|
1079
|
+
if (plan.verdict === "refuse")
|
|
1080
|
+
throw new SchemaRefusedError(plan);
|
|
1081
|
+
return plan;
|
|
1082
|
+
}
|
|
1083
|
+
function hasWork(plan) {
|
|
1084
|
+
return plan.adopted.length > 0 || plan.pending.length > 0;
|
|
1085
|
+
}
|
|
1086
|
+
function writeNames(plan) {
|
|
1087
|
+
const adoption = plan.adopted.length > 0 ? [ADOPTION_NAME] : [];
|
|
1088
|
+
return [...adoption, ...plan.pending.map(({ id }) => id)];
|
|
1089
|
+
}
|
|
1090
|
+
function nothingWritten(db) {
|
|
1091
|
+
return { adopted: [], applied: [], userVersion: readUserVersion(db) };
|
|
1092
|
+
}
|
|
1093
|
+
function readUserVersion(db) {
|
|
1094
|
+
const row = db.query("PRAGMA user_version").get();
|
|
1095
|
+
return row?.user_version ?? 0;
|
|
1096
|
+
}
|
|
1097
|
+
function readMigrationLog(db, path) {
|
|
1098
|
+
const table = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(MIGRATION_LOG_TABLE);
|
|
1099
|
+
if (table === null)
|
|
1100
|
+
return null;
|
|
1101
|
+
return db.query(`SELECT id, sha256, breaks, applied_at, applied_by FROM ${MIGRATION_LOG_TABLE} ORDER BY seq`).all().map((row) => ({
|
|
1102
|
+
id: row.id,
|
|
1103
|
+
sha256: row.sha256,
|
|
1104
|
+
breaks: parseBreaks(row.breaks, row.id, path),
|
|
1105
|
+
appliedAt: row.applied_at,
|
|
1106
|
+
appliedBy: row.applied_by
|
|
1107
|
+
}));
|
|
1108
|
+
}
|
|
1109
|
+
function parseBreaks(text, id, path) {
|
|
1110
|
+
const refused = new Error(`effort store: ${path} logs migration ${id} with breaks ${text},` + " which is not a JSON array of words; refusing to read or write it");
|
|
1111
|
+
let parsed;
|
|
1112
|
+
try {
|
|
1113
|
+
parsed = JSON.parse(text);
|
|
1114
|
+
} catch (cause) {
|
|
1115
|
+
throw new Error(refused.message, { cause });
|
|
1116
|
+
}
|
|
1117
|
+
if (!Array.isArray(parsed) || !parsed.every((word) => typeof word === "string"))
|
|
1118
|
+
throw refused;
|
|
1119
|
+
return parsed;
|
|
1120
|
+
}
|
|
1121
|
+
function bodyOf(migrations, id) {
|
|
1122
|
+
const migration = migrations.find((candidate) => candidate.id === id);
|
|
1123
|
+
if (migration === undefined)
|
|
1124
|
+
throw new Error(`bring forward: no migration ${id} in the catalogue it planned from`);
|
|
1125
|
+
return migration.sql;
|
|
1126
|
+
}
|
|
1127
|
+
function logMigration(db, entry, stamp) {
|
|
1128
|
+
db.run(`INSERT INTO ${MIGRATION_LOG_TABLE} (id, sha256, breaks, applied_at, applied_by) VALUES (?, ?, ?, ?, ?)`, [entry.id, entry.sha256, JSON.stringify(entry.breaks), stamp.appliedAt, stamp.appliedBy]);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// src/effort/store/settings.ts
|
|
1132
|
+
var DEFAULT_STORE_SETTINGS = Object.freeze({
|
|
1133
|
+
busyTimeoutMs: CONFIG_DEFAULTS.effortBusyTimeoutMs
|
|
1134
|
+
});
|
|
1135
|
+
var active = DEFAULT_STORE_SETTINGS;
|
|
1136
|
+
function setActiveStoreSettings(settings) {
|
|
1137
|
+
if (settings === null) {
|
|
1138
|
+
active = DEFAULT_STORE_SETTINGS;
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
if (!isBusyTimeoutMs(settings.busyTimeoutMs)) {
|
|
1142
|
+
throw new RangeError(`effort store: busy timeout ${String(settings.busyTimeoutMs)} is not a whole number ` + `of milliseconds from ${String(BUSY_TIMEOUT_MIN_MS)} to ${String(BUSY_TIMEOUT_MAX_MS)}`);
|
|
1143
|
+
}
|
|
1144
|
+
active = Object.freeze({ busyTimeoutMs: settings.busyTimeoutMs });
|
|
1145
|
+
}
|
|
1146
|
+
function activeStoreSettings() {
|
|
1147
|
+
return active;
|
|
1148
|
+
}
|
|
376
1149
|
|
|
377
1150
|
// src/effort/store/sqlite.ts
|
|
378
|
-
var
|
|
1151
|
+
var SQLITE_STORE_FILE_NAME = "effort.sqlite";
|
|
379
1152
|
function sqliteStorePath(repoRoot) {
|
|
380
|
-
return
|
|
1153
|
+
return join7(effortStoreDir(repoRoot), SQLITE_STORE_FILE_NAME);
|
|
381
1154
|
}
|
|
382
1155
|
var KIND_TABLES = {
|
|
383
1156
|
sessions: { name: "sessions", keyColumn: "session_id" },
|
|
@@ -405,35 +1178,14 @@ function batchEntries(kind, rows) {
|
|
|
405
1178
|
}
|
|
406
1179
|
return keyed.map(({ key, row }) => ({ key, body: JSON.stringify(row) }));
|
|
407
1180
|
}
|
|
408
|
-
function
|
|
409
|
-
|
|
410
|
-
return row?.user_version ?? 0;
|
|
411
|
-
}
|
|
412
|
-
function checkedVersion(db, path, latest) {
|
|
413
|
-
const version = userVersion(db);
|
|
414
|
-
if (version > latest) {
|
|
415
|
-
throw new Error(`effort store: ${path} is at schema version ${version}, past the` + ` ${latest} this rafa knows; refusing to read or write it`);
|
|
416
|
-
}
|
|
417
|
-
return version;
|
|
418
|
-
}
|
|
419
|
-
function migrateSchema(db, path, migrations = SQLITE_MIGRATIONS) {
|
|
420
|
-
const latest = migrations.length;
|
|
421
|
-
if (checkedVersion(db, path, latest) === latest)
|
|
422
|
-
return;
|
|
423
|
-
const applyPending = db.transaction(() => {
|
|
424
|
-
const from = checkedVersion(db, path, latest);
|
|
425
|
-
for (const sql of migrations.slice(from))
|
|
426
|
-
db.run(sql);
|
|
427
|
-
db.run(`PRAGMA user_version = ${latest}`);
|
|
428
|
-
});
|
|
429
|
-
applyPending.immediate();
|
|
430
|
-
}
|
|
431
|
-
function withSqliteStore(path, create, use) {
|
|
1181
|
+
function withSqliteStore(path, access, create, use) {
|
|
1182
|
+
guardTestProcess(path);
|
|
432
1183
|
if (create)
|
|
433
|
-
|
|
1184
|
+
mkdirSync3(dirname6(path), { recursive: true });
|
|
434
1185
|
const db = new Database(path, { readwrite: true, create });
|
|
435
1186
|
try {
|
|
436
|
-
|
|
1187
|
+
db.run(`PRAGMA busy_timeout = ${String(activeStoreSettings().busyTimeoutMs)}`);
|
|
1188
|
+
bringForward(db, path, access, "open");
|
|
437
1189
|
return use(db);
|
|
438
1190
|
} finally {
|
|
439
1191
|
db.close();
|
|
@@ -445,11 +1197,11 @@ function insertBatch(db, table, entries) {
|
|
|
445
1197
|
return insertAll.immediate();
|
|
446
1198
|
}
|
|
447
1199
|
function writeSqliteStore(path, rowCount, empty, insert) {
|
|
448
|
-
const exists =
|
|
1200
|
+
const exists = existsSync5(path);
|
|
449
1201
|
if (rowCount > 0)
|
|
450
|
-
return withSqliteStore(path, !exists, insert);
|
|
1202
|
+
return withSqliteStore(path, "write", !exists, insert);
|
|
451
1203
|
if (exists)
|
|
452
|
-
withSqliteStore(path, false, () => {
|
|
1204
|
+
withSqliteStore(path, "write", false, () => {
|
|
453
1205
|
return;
|
|
454
1206
|
});
|
|
455
1207
|
return empty;
|
|
@@ -461,10 +1213,10 @@ function appendKind2(path, kind, rows) {
|
|
|
461
1213
|
return { path, appended, skipped: entries.length - appended };
|
|
462
1214
|
}
|
|
463
1215
|
function keysOfKind2(path, kind) {
|
|
464
|
-
if (!
|
|
1216
|
+
if (!existsSync5(path))
|
|
465
1217
|
return new Set;
|
|
466
|
-
const { name, keyColumn } = KIND_TABLES[kind];
|
|
467
|
-
const keys = withSqliteStore(path, false, (db) => db.query(`SELECT ${keyColumn} AS key FROM ${
|
|
1218
|
+
const { name: name2, keyColumn } = KIND_TABLES[kind];
|
|
1219
|
+
const keys = withSqliteStore(path, "read", false, (db) => db.query(`SELECT ${keyColumn} AS key FROM ${name2} ORDER BY seq`).all());
|
|
468
1220
|
return new Set(keys.map(({ key }) => key));
|
|
469
1221
|
}
|
|
470
1222
|
function parsedObject(body) {
|
|
@@ -483,11 +1235,11 @@ function rowOf(body, where) {
|
|
|
483
1235
|
return value;
|
|
484
1236
|
}
|
|
485
1237
|
function readKind2(path, kind) {
|
|
486
|
-
if (!
|
|
1238
|
+
if (!existsSync5(path))
|
|
487
1239
|
return [];
|
|
488
|
-
const { name } = KIND_TABLES[kind];
|
|
489
|
-
const stored = withSqliteStore(path, false, (db) => db.query(`SELECT seq, row_json AS body FROM ${
|
|
490
|
-
return stored.map(({ seq, body }) => rowOf(body, `${path}: ${
|
|
1240
|
+
const { name: name2 } = KIND_TABLES[kind];
|
|
1241
|
+
const stored = withSqliteStore(path, "read", false, (db) => db.query(`SELECT seq, row_json AS body FROM ${name2} ORDER BY seq`).all());
|
|
1242
|
+
return stored.map(({ seq, body }) => rowOf(body, `${path}: ${name2} row ${seq}`));
|
|
491
1243
|
}
|
|
492
1244
|
function openSqliteStore(repoRoot) {
|
|
493
1245
|
const path = sqliteStorePath(repoRoot);
|
|
@@ -501,15 +1253,15 @@ function openSqliteStore(repoRoot) {
|
|
|
501
1253
|
|
|
502
1254
|
// src/adapters/learning/local.ts
|
|
503
1255
|
import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
504
|
-
import { join as
|
|
1256
|
+
import { join as join10 } from "path";
|
|
505
1257
|
|
|
506
1258
|
// src/commands/instinct/instinct-records.ts
|
|
507
|
-
import { readdirSync as
|
|
508
|
-
import { join as
|
|
1259
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
1260
|
+
import { join as join9 } from "path";
|
|
509
1261
|
|
|
510
1262
|
// src/check/layout.ts
|
|
511
|
-
import { readdirSync, statSync } from "fs";
|
|
512
|
-
import { join as
|
|
1263
|
+
import { readdirSync as readdirSync2, statSync } from "fs";
|
|
1264
|
+
import { join as join8, relative as relative2 } from "path";
|
|
513
1265
|
var SKILL_FILE = "SKILL.md";
|
|
514
1266
|
var MARKDOWN_EXTENSION = ".md";
|
|
515
1267
|
var LAYOUT_SEVERITY = Object.freeze({
|
|
@@ -538,16 +1290,16 @@ function entryKind(path) {
|
|
|
538
1290
|
}
|
|
539
1291
|
}
|
|
540
1292
|
function visibleNames(dir) {
|
|
541
|
-
return
|
|
1293
|
+
return readdirSync2(dir).filter((name2) => !name2.startsWith(".")).sort((left, right) => left.localeCompare(right));
|
|
542
1294
|
}
|
|
543
|
-
function isMarkdown(
|
|
544
|
-
return
|
|
1295
|
+
function isMarkdown(name2) {
|
|
1296
|
+
return name2.endsWith(MARKDOWN_EXTENSION);
|
|
545
1297
|
}
|
|
546
|
-
function markdownStem(
|
|
547
|
-
return
|
|
1298
|
+
function markdownStem(name2) {
|
|
1299
|
+
return name2.slice(0, -MARKDOWN_EXTENSION.length);
|
|
548
1300
|
}
|
|
549
1301
|
function shown(root, path) {
|
|
550
|
-
return
|
|
1302
|
+
return relative2(root, path) || ".";
|
|
551
1303
|
}
|
|
552
1304
|
function dotfileOnlyEntry(kind, root, dir) {
|
|
553
1305
|
return {
|
|
@@ -569,37 +1321,37 @@ function unreadableEntry(kind, root, path) {
|
|
|
569
1321
|
issues: [layoutIssue("unreadable-entry", `${shown(root, path)} cannot be read (a broken symlink, or a path stat refuses)`)]
|
|
570
1322
|
};
|
|
571
1323
|
}
|
|
572
|
-
function skillFileEntry(path,
|
|
573
|
-
return { kind: "skill", path, isFile: true, shape: "skill-file", name, issues: [] };
|
|
1324
|
+
function skillFileEntry(path, name2) {
|
|
1325
|
+
return { kind: "skill", path, isFile: true, shape: "skill-file", name: name2, issues: [] };
|
|
574
1326
|
}
|
|
575
|
-
function flatGroupedEntry(root, path,
|
|
1327
|
+
function flatGroupedEntry(root, path, name2) {
|
|
576
1328
|
return {
|
|
577
1329
|
kind: "skill",
|
|
578
1330
|
path,
|
|
579
1331
|
isFile: true,
|
|
580
1332
|
shape: "flat-grouped",
|
|
581
|
-
name,
|
|
582
|
-
issues: [layoutIssue("flat-grouped-layout", `${shown(root, path)} is the flat grouped layout <dir>/<group>/<name>.md,` + ` which does not register; move it to <dir>/${
|
|
1333
|
+
name: name2,
|
|
1334
|
+
issues: [layoutIssue("flat-grouped-layout", `${shown(root, path)} is the flat grouped layout <dir>/<group>/<name>.md,` + ` which does not register; move it to <dir>/${name2}/${SKILL_FILE}`)]
|
|
583
1335
|
};
|
|
584
1336
|
}
|
|
585
|
-
function nestedGroupEntry(root, path,
|
|
1337
|
+
function nestedGroupEntry(root, path, name2) {
|
|
586
1338
|
return {
|
|
587
1339
|
kind: "skill",
|
|
588
1340
|
path,
|
|
589
1341
|
isFile: true,
|
|
590
1342
|
shape: "nested-group",
|
|
591
|
-
name,
|
|
592
|
-
issues: [layoutIssue("nested-group-layout", `${shown(root, path)} is the nested layout <dir>/<group>/<name>/${SKILL_FILE},` + ` which does not register; move it to <dir>/${
|
|
1343
|
+
name: name2,
|
|
1344
|
+
issues: [layoutIssue("nested-group-layout", `${shown(root, path)} is the nested layout <dir>/<group>/<name>/${SKILL_FILE},` + ` which does not register; move it to <dir>/${name2}/${SKILL_FILE}`)]
|
|
593
1345
|
};
|
|
594
1346
|
}
|
|
595
|
-
function looseFileEntry(root, path,
|
|
1347
|
+
function looseFileEntry(root, path, name2) {
|
|
596
1348
|
return {
|
|
597
1349
|
kind: "skill",
|
|
598
1350
|
path,
|
|
599
1351
|
isFile: true,
|
|
600
1352
|
shape: "loose-file",
|
|
601
|
-
name,
|
|
602
|
-
issues: [layoutIssue("loose-file", `${shown(root, path)} sits directly under the skills directory, which registers` + ` only <dir>/<name>/${SKILL_FILE}; move it to <dir>/${
|
|
1353
|
+
name: name2,
|
|
1354
|
+
issues: [layoutIssue("loose-file", `${shown(root, path)} sits directly under the skills directory, which registers` + ` only <dir>/<name>/${SKILL_FILE}; move it to <dir>/${name2}/${SKILL_FILE}`)]
|
|
603
1355
|
};
|
|
604
1356
|
}
|
|
605
1357
|
function noSkillFileEntry(root, dir) {
|
|
@@ -612,30 +1364,30 @@ function noSkillFileEntry(root, dir) {
|
|
|
612
1364
|
issues: [layoutIssue("missing-skill-file", `${shown(root, dir)}/ holds no ${SKILL_FILE}, so it registers no skill`)]
|
|
613
1365
|
};
|
|
614
1366
|
}
|
|
615
|
-
function nestedInstinctEntry(root, path,
|
|
1367
|
+
function nestedInstinctEntry(root, path, name2) {
|
|
616
1368
|
return {
|
|
617
1369
|
kind: "instinct",
|
|
618
1370
|
path,
|
|
619
1371
|
isFile: true,
|
|
620
1372
|
shape: "nested-instinct",
|
|
621
|
-
name,
|
|
622
|
-
issues: [layoutIssue("nested-instinct-file", `${shown(root, path)} sits below the instincts directory, which is flat;` + ` move it to <dir>/${
|
|
1373
|
+
name: name2,
|
|
1374
|
+
issues: [layoutIssue("nested-instinct-file", `${shown(root, path)} sits below the instincts directory, which is flat;` + ` move it to <dir>/${name2}${MARKDOWN_EXTENSION}`)]
|
|
623
1375
|
};
|
|
624
1376
|
}
|
|
625
|
-
function scanGroupMember(root, dir,
|
|
1377
|
+
function scanGroupMember(root, dir, name2) {
|
|
626
1378
|
const children = visibleNames(dir);
|
|
627
1379
|
if (children.length === 0)
|
|
628
1380
|
return [dotfileOnlyEntry("skill", root, dir)];
|
|
629
|
-
const skillPath =
|
|
1381
|
+
const skillPath = join8(dir, SKILL_FILE);
|
|
630
1382
|
if (children.includes(SKILL_FILE) && entryKind(skillPath) === "file") {
|
|
631
|
-
return [nestedGroupEntry(root, skillPath,
|
|
1383
|
+
return [nestedGroupEntry(root, skillPath, name2)];
|
|
632
1384
|
}
|
|
633
1385
|
return [noSkillFileEntry(root, dir)];
|
|
634
1386
|
}
|
|
635
1387
|
function scanSkillGroup(root, dir, children) {
|
|
636
1388
|
const entries = [];
|
|
637
1389
|
for (const child of children) {
|
|
638
|
-
const path =
|
|
1390
|
+
const path = join8(dir, child);
|
|
639
1391
|
const found = entryKind(path);
|
|
640
1392
|
if (found === null) {
|
|
641
1393
|
entries.push(unreadableEntry("skill", root, path));
|
|
@@ -652,31 +1404,31 @@ function scanSkillGroup(root, dir, children) {
|
|
|
652
1404
|
return [noSkillFileEntry(root, dir)];
|
|
653
1405
|
return entries;
|
|
654
1406
|
}
|
|
655
|
-
function scanSkillDirectory(root, dir,
|
|
1407
|
+
function scanSkillDirectory(root, dir, name2) {
|
|
656
1408
|
const children = visibleNames(dir);
|
|
657
1409
|
if (children.length === 0)
|
|
658
1410
|
return [dotfileOnlyEntry("skill", root, dir)];
|
|
659
|
-
const skillPath =
|
|
1411
|
+
const skillPath = join8(dir, SKILL_FILE);
|
|
660
1412
|
if (children.includes(SKILL_FILE) && entryKind(skillPath) === "file") {
|
|
661
|
-
return [skillFileEntry(skillPath,
|
|
1413
|
+
return [skillFileEntry(skillPath, name2)];
|
|
662
1414
|
}
|
|
663
1415
|
return scanSkillGroup(root, dir, children);
|
|
664
1416
|
}
|
|
665
1417
|
function scanSkillRoot(root) {
|
|
666
1418
|
const entries = [];
|
|
667
|
-
for (const
|
|
668
|
-
const path =
|
|
1419
|
+
for (const name2 of visibleNames(root)) {
|
|
1420
|
+
const path = join8(root, name2);
|
|
669
1421
|
const found = entryKind(path);
|
|
670
1422
|
if (found === null) {
|
|
671
1423
|
entries.push(unreadableEntry("skill", root, path));
|
|
672
1424
|
continue;
|
|
673
1425
|
}
|
|
674
1426
|
if (found === "directory") {
|
|
675
|
-
entries.push(...scanSkillDirectory(root, path,
|
|
1427
|
+
entries.push(...scanSkillDirectory(root, path, name2));
|
|
676
1428
|
continue;
|
|
677
1429
|
}
|
|
678
|
-
if (isMarkdown(
|
|
679
|
-
entries.push(looseFileEntry(root, path, markdownStem(
|
|
1430
|
+
if (isMarkdown(name2))
|
|
1431
|
+
entries.push(looseFileEntry(root, path, markdownStem(name2)));
|
|
680
1432
|
}
|
|
681
1433
|
return entries;
|
|
682
1434
|
}
|
|
@@ -686,7 +1438,7 @@ function scanInstinctSubdirectory(root, dir) {
|
|
|
686
1438
|
return [dotfileOnlyEntry("instinct", root, dir)];
|
|
687
1439
|
const entries = [];
|
|
688
1440
|
for (const child of children) {
|
|
689
|
-
const path =
|
|
1441
|
+
const path = join8(dir, child);
|
|
690
1442
|
const found = entryKind(path);
|
|
691
1443
|
if (found === null) {
|
|
692
1444
|
entries.push(unreadableEntry("instinct", root, path));
|
|
@@ -703,8 +1455,8 @@ function scanInstinctSubdirectory(root, dir) {
|
|
|
703
1455
|
}
|
|
704
1456
|
function scanInstinctRoot(root) {
|
|
705
1457
|
const entries = [];
|
|
706
|
-
for (const
|
|
707
|
-
const path =
|
|
1458
|
+
for (const name2 of visibleNames(root)) {
|
|
1459
|
+
const path = join8(root, name2);
|
|
708
1460
|
const found = entryKind(path);
|
|
709
1461
|
if (found === null) {
|
|
710
1462
|
entries.push(unreadableEntry("instinct", root, path));
|
|
@@ -714,14 +1466,14 @@ function scanInstinctRoot(root) {
|
|
|
714
1466
|
entries.push(...scanInstinctSubdirectory(root, path));
|
|
715
1467
|
continue;
|
|
716
1468
|
}
|
|
717
|
-
if (!isMarkdown(
|
|
1469
|
+
if (!isMarkdown(name2))
|
|
718
1470
|
continue;
|
|
719
1471
|
entries.push({
|
|
720
1472
|
kind: "instinct",
|
|
721
1473
|
path,
|
|
722
1474
|
isFile: true,
|
|
723
1475
|
shape: "instinct-file",
|
|
724
|
-
name: markdownStem(
|
|
1476
|
+
name: markdownStem(name2),
|
|
725
1477
|
issues: []
|
|
726
1478
|
});
|
|
727
1479
|
}
|
|
@@ -741,9 +1493,9 @@ function scanLayout(root, kind) {
|
|
|
741
1493
|
function declaredNameIssue(entry, declared) {
|
|
742
1494
|
if (entry.name === null || declared === null || declared === entry.name)
|
|
743
1495
|
return null;
|
|
744
|
-
const
|
|
1496
|
+
const field2 = entry.kind === "skill" ? "name" : "id";
|
|
745
1497
|
const source = entry.kind === "skill" ? "directory name" : "file name";
|
|
746
|
-
return layoutIssue("name-mismatch", `frontmatter ${
|
|
1498
|
+
return layoutIssue("name-mismatch", `frontmatter ${field2} "${declared}" is not the ${source} "${entry.name}"`);
|
|
747
1499
|
}
|
|
748
1500
|
|
|
749
1501
|
// src/schema/frontmatter.ts
|
|
@@ -889,8 +1641,8 @@ var INSTINCT_KEY_ORDER = [
|
|
|
889
1641
|
"created_at",
|
|
890
1642
|
"updated_at"
|
|
891
1643
|
];
|
|
892
|
-
function issue(code,
|
|
893
|
-
return { code, field, message };
|
|
1644
|
+
function issue(code, field2, message) {
|
|
1645
|
+
return { code, field: field2, message };
|
|
894
1646
|
}
|
|
895
1647
|
function typeName(value) {
|
|
896
1648
|
if (value === null)
|
|
@@ -905,25 +1657,25 @@ function typeName(value) {
|
|
|
905
1657
|
return "a number";
|
|
906
1658
|
return `a ${typeof value}`;
|
|
907
1659
|
}
|
|
908
|
-
function requiredString(data,
|
|
909
|
-
const value = data[
|
|
1660
|
+
function requiredString(data, field2) {
|
|
1661
|
+
const value = data[field2];
|
|
910
1662
|
if (value === undefined || value === null) {
|
|
911
|
-
return [issue("missing-field",
|
|
1663
|
+
return [issue("missing-field", field2, `${field2} is required and is absent`)];
|
|
912
1664
|
}
|
|
913
1665
|
if (typeof value !== "string") {
|
|
914
|
-
return [issue("wrong-type",
|
|
1666
|
+
return [issue("wrong-type", field2, `${field2} must be a string, not ${typeName(value)}`)];
|
|
915
1667
|
}
|
|
916
1668
|
if (value.trim() === "") {
|
|
917
|
-
return [issue("missing-field",
|
|
1669
|
+
return [issue("missing-field", field2, `${field2} is required and is empty`)];
|
|
918
1670
|
}
|
|
919
1671
|
return [];
|
|
920
1672
|
}
|
|
921
|
-
function requiredChoice(data,
|
|
922
|
-
const issues = requiredString(data,
|
|
1673
|
+
function requiredChoice(data, field2, allowed, code) {
|
|
1674
|
+
const issues = requiredString(data, field2);
|
|
923
1675
|
if (issues.length > 0)
|
|
924
1676
|
return issues;
|
|
925
|
-
const value = data[
|
|
926
|
-
return allowed.includes(value) ? [] : [issue(code,
|
|
1677
|
+
const value = data[field2];
|
|
1678
|
+
return allowed.includes(value) ? [] : [issue(code, field2, `${field2} "${value}" is not one of ${allowed.join(", ")}`)];
|
|
927
1679
|
}
|
|
928
1680
|
function checkId(data) {
|
|
929
1681
|
const issues = requiredString(data, "id");
|
|
@@ -959,11 +1711,11 @@ function checkSources(data) {
|
|
|
959
1711
|
return [issue("wrong-type", "sources", `sources must be a list, not ${typeName(value)}`)];
|
|
960
1712
|
}
|
|
961
1713
|
return value.flatMap((entry, index) => {
|
|
962
|
-
const
|
|
1714
|
+
const field2 = `sources[${index}]`;
|
|
963
1715
|
if (typeof entry !== "string") {
|
|
964
|
-
return [issue("wrong-type",
|
|
1716
|
+
return [issue("wrong-type", field2, `sources entries must be strings, and this one is ${typeName(entry)}`)];
|
|
965
1717
|
}
|
|
966
|
-
return entry.trim() === "" ? [issue("wrong-type",
|
|
1718
|
+
return entry.trim() === "" ? [issue("wrong-type", field2, "sources entries must name a source, and this one is blank")] : [];
|
|
967
1719
|
});
|
|
968
1720
|
}
|
|
969
1721
|
function checkPromotedTo(data) {
|
|
@@ -992,26 +1744,26 @@ function checkProjectId(data) {
|
|
|
992
1744
|
}
|
|
993
1745
|
return PROJECT_ID_PATTERN.test(value) ? [] : [issue("invalid-project-id", "project_id", `project_id "${value}" is not ${PROJECT_ID_LENGTH} lowercase hex characters`)];
|
|
994
1746
|
}
|
|
995
|
-
function checkTimestamp(data,
|
|
996
|
-
const issues = requiredString(data,
|
|
1747
|
+
function checkTimestamp(data, field2) {
|
|
1748
|
+
const issues = requiredString(data, field2);
|
|
997
1749
|
if (issues.length > 0)
|
|
998
1750
|
return issues;
|
|
999
|
-
const value = data[
|
|
1000
|
-
return TIMESTAMP_PATTERN.test(value) && !Number.isNaN(Date.parse(value)) ? [] : [issue("invalid-timestamp",
|
|
1751
|
+
const value = data[field2];
|
|
1752
|
+
return TIMESTAMP_PATTERN.test(value) && !Number.isNaN(Date.parse(value)) ? [] : [issue("invalid-timestamp", field2, `${field2} "${value}" is not an ISO 8601 instant such as 2026-09-11T10:00:00Z`)];
|
|
1001
1753
|
}
|
|
1002
1754
|
function isScalar(value) {
|
|
1003
1755
|
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
1004
1756
|
}
|
|
1005
1757
|
function checkEvidenceEntry(entry, index) {
|
|
1006
|
-
const
|
|
1758
|
+
const field2 = `evidence[${index}]`;
|
|
1007
1759
|
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
1008
|
-
return [issue("wrong-type",
|
|
1760
|
+
return [issue("wrong-type", field2, `evidence entries must be mappings, and this one is ${typeName(entry)}`)];
|
|
1009
1761
|
}
|
|
1010
1762
|
const entries = Object.entries(entry);
|
|
1011
1763
|
if (entries.length === 0) {
|
|
1012
|
-
return [issue("wrong-type",
|
|
1764
|
+
return [issue("wrong-type", field2, "evidence entries must say something, and this one is empty")];
|
|
1013
1765
|
}
|
|
1014
|
-
return entries.flatMap(([key, value]) => isScalar(value) ? [] : [issue("wrong-type", `${
|
|
1766
|
+
return entries.flatMap(([key, value]) => isScalar(value) ? [] : [issue("wrong-type", `${field2}.${key}`, `evidence values must be text, numbers or booleans, and this one is ${typeName(value)}`)]);
|
|
1015
1767
|
}
|
|
1016
1768
|
function checkEvidence(data) {
|
|
1017
1769
|
const value = data["evidence"];
|
|
@@ -1171,11 +1923,11 @@ function writeInstinct(instinct, newline = `
|
|
|
1171
1923
|
}
|
|
1172
1924
|
|
|
1173
1925
|
// src/commands/instinct/instinct-records.ts
|
|
1174
|
-
function isRecordFile(
|
|
1175
|
-
return !
|
|
1926
|
+
function isRecordFile(name2) {
|
|
1927
|
+
return !name2.startsWith(".") && name2.endsWith(MARKDOWN_EXTENSION) && name2 !== MARKDOWN_EXTENSION;
|
|
1176
1928
|
}
|
|
1177
|
-
function recordId(
|
|
1178
|
-
return
|
|
1929
|
+
function recordId(name2) {
|
|
1930
|
+
return name2.slice(0, -MARKDOWN_EXTENSION.length);
|
|
1179
1931
|
}
|
|
1180
1932
|
function unreadableIssue(message) {
|
|
1181
1933
|
return { code: "missing-frontmatter", field: "file", message: `the file could not be read: ${message}` };
|
|
@@ -1183,7 +1935,7 @@ function unreadableIssue(message) {
|
|
|
1183
1935
|
function readRecord(path, scope, id) {
|
|
1184
1936
|
let text;
|
|
1185
1937
|
try {
|
|
1186
|
-
text =
|
|
1938
|
+
text = readFileSync4(path, "utf8");
|
|
1187
1939
|
} catch (error) {
|
|
1188
1940
|
const message = error instanceof Error ? error.message : String(error);
|
|
1189
1941
|
return { scope, path, id, instinct: null, issues: [unreadableIssue(message)] };
|
|
@@ -1199,7 +1951,7 @@ function isFile(path) {
|
|
|
1199
1951
|
}
|
|
1200
1952
|
}
|
|
1201
1953
|
function recordFiles(dir) {
|
|
1202
|
-
return
|
|
1954
|
+
return readdirSync3(dir).filter((name2) => isRecordFile(name2) && isFile(join9(dir, name2))).sort((left, right) => left.localeCompare(right));
|
|
1203
1955
|
}
|
|
1204
1956
|
function readScope(scope, dir) {
|
|
1205
1957
|
if (!tierExists(dir))
|
|
@@ -1208,7 +1960,7 @@ function readScope(scope, dir) {
|
|
|
1208
1960
|
scope,
|
|
1209
1961
|
dir,
|
|
1210
1962
|
exists: true,
|
|
1211
|
-
records: recordFiles(dir).map((
|
|
1963
|
+
records: recordFiles(dir).map((name2) => readRecord(join9(dir, name2), scope, recordId(name2)))
|
|
1212
1964
|
};
|
|
1213
1965
|
}
|
|
1214
1966
|
function readScopes(seams) {
|
|
@@ -1220,9 +1972,9 @@ function allRecords(listings) {
|
|
|
1220
1972
|
function findRecords(listings, id) {
|
|
1221
1973
|
return allRecords(listings).filter((entry) => entry.id === id);
|
|
1222
1974
|
}
|
|
1223
|
-
function instinctProject(context,
|
|
1975
|
+
function instinctProject(context, name2) {
|
|
1224
1976
|
if (context.project === null)
|
|
1225
|
-
throw new Error(`${
|
|
1977
|
+
throw new Error(`${name2} runs inside a project, and was handed none`);
|
|
1226
1978
|
return context.project;
|
|
1227
1979
|
}
|
|
1228
1980
|
|
|
@@ -1305,7 +2057,7 @@ var PREFIX = "local learning";
|
|
|
1305
2057
|
var PUSH_REFUSAL = `${PREFIX}: refused to store a push`;
|
|
1306
2058
|
var INSTINCTS_FILE = "instincts.ndjson";
|
|
1307
2059
|
var FLAGS_FILE = "flags.ndjson";
|
|
1308
|
-
var
|
|
2060
|
+
var RECORD_EXTENSION2 = ".md";
|
|
1309
2061
|
var MIN_CONFIDENCE = 0.3;
|
|
1310
2062
|
var MAX_CONFIDENCE = 0.9;
|
|
1311
2063
|
var SIGNALS = ["loud", "silent"];
|
|
@@ -1489,7 +2241,7 @@ function producedRecords(result) {
|
|
|
1489
2241
|
for (const record of result.decisions.flatMap((decision) => decision.produced)) {
|
|
1490
2242
|
const found = byId.get(record.id);
|
|
1491
2243
|
if (found !== undefined && found !== record) {
|
|
1492
|
-
throw new Error(`${PUSH_REFUSAL}: two records would be written to ${record.id}${
|
|
2244
|
+
throw new Error(`${PUSH_REFUSAL}: two records would be written to ${record.id}${RECORD_EXTENSION2}`);
|
|
1493
2245
|
}
|
|
1494
2246
|
byId.set(record.id, record);
|
|
1495
2247
|
}
|
|
@@ -1501,7 +2253,7 @@ function heldChanges(dir, held, result, described) {
|
|
|
1501
2253
|
const writes = [];
|
|
1502
2254
|
const kept = new Set;
|
|
1503
2255
|
for (const record of producedRecords(result)) {
|
|
1504
|
-
const file = `${record.id}${
|
|
2256
|
+
const file = `${record.id}${RECORD_EXTENSION2}`;
|
|
1505
2257
|
const instinct = toHeldInstinct(record, heldInstincts, payloadDescription(record, described));
|
|
1506
2258
|
if (instinct === null) {
|
|
1507
2259
|
throw new Error(`${PUSH_REFUSAL}: ${file} has no held member and no payload description to be written with`);
|
|
@@ -1511,7 +2263,7 @@ function heldChanges(dir, held, result, described) {
|
|
|
1511
2263
|
if (issue2 !== undefined) {
|
|
1512
2264
|
throw new Error(`${PUSH_REFUSAL}: ${file} would not read back: ${issue2.field}: ${issue2.message}`);
|
|
1513
2265
|
}
|
|
1514
|
-
const path =
|
|
2266
|
+
const path = join10(dir, file);
|
|
1515
2267
|
if (held.unreadable.has(path)) {
|
|
1516
2268
|
throw new Error(`${PUSH_REFUSAL}: it would overwrite ${path}, which could not be read`);
|
|
1517
2269
|
}
|
|
@@ -1541,18 +2293,18 @@ function pushLogLines(sourceId, described, result) {
|
|
|
1541
2293
|
});
|
|
1542
2294
|
}
|
|
1543
2295
|
function localInstinctsDir(repoRoot) {
|
|
1544
|
-
return
|
|
2296
|
+
return join10(repoRoot, RAFA_INSTINCTS_PATH);
|
|
1545
2297
|
}
|
|
1546
2298
|
function userInstinctsDir(home) {
|
|
1547
|
-
return
|
|
2299
|
+
return join10(home, RAFA_INSTINCTS_PATH);
|
|
1548
2300
|
}
|
|
1549
2301
|
function createLocalLearning(options) {
|
|
1550
2302
|
const { instinctsDir, minConfidence } = options;
|
|
1551
2303
|
const userDir = userInstinctsDir(options.home);
|
|
1552
2304
|
const now = options.now ?? (() => new Date().toISOString());
|
|
1553
2305
|
const warn = options.warn ?? warnThroughActiveOutput;
|
|
1554
|
-
const instinctsPath =
|
|
1555
|
-
const flagsPath =
|
|
2306
|
+
const instinctsPath = join10(instinctsDir, INSTINCTS_FILE);
|
|
2307
|
+
const flagsPath = join10(instinctsDir, FLAGS_FILE);
|
|
1556
2308
|
function readHeld(scope, dir) {
|
|
1557
2309
|
const { records } = readScope(scope, dir);
|
|
1558
2310
|
const unreadable = new Set;
|
|
@@ -1669,9 +2421,9 @@ function createJsonOutput({
|
|
|
1669
2421
|
}
|
|
1670
2422
|
|
|
1671
2423
|
// src/adapters/planner/claude.ts
|
|
1672
|
-
import { existsSync as
|
|
2424
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1673
2425
|
import { mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
|
|
1674
|
-
import { posix, resolve } from "path";
|
|
2426
|
+
import { posix, resolve as resolve3 } from "path";
|
|
1675
2427
|
|
|
1676
2428
|
// src/board/spec-review.ts
|
|
1677
2429
|
var SPEC_REVIEW_KIND = "spec-review";
|
|
@@ -1693,7 +2445,7 @@ var VERDICT_BREAK = /[\s_]+/gu;
|
|
|
1693
2445
|
function isMapping2(value) {
|
|
1694
2446
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1695
2447
|
}
|
|
1696
|
-
function
|
|
2448
|
+
function describeValue3(value) {
|
|
1697
2449
|
if (typeof value === "string")
|
|
1698
2450
|
return JSON.stringify(value);
|
|
1699
2451
|
if (value === null)
|
|
@@ -1726,39 +2478,39 @@ function verdictOf(value) {
|
|
|
1726
2478
|
const word = spelled.replace(VERDICT_BREAK, "-");
|
|
1727
2479
|
return SPEC_REVIEW_VERDICTS.includes(word) ? word : null;
|
|
1728
2480
|
}
|
|
1729
|
-
function issueAt(
|
|
1730
|
-
return { field, text };
|
|
2481
|
+
function issueAt(field2, text) {
|
|
2482
|
+
return { field: field2, text };
|
|
1731
2483
|
}
|
|
1732
|
-
function readBlocking(item,
|
|
2484
|
+
function readBlocking(item, field2, issues) {
|
|
1733
2485
|
const value = valueAt(item, "blocking");
|
|
1734
2486
|
if (typeof value === "boolean")
|
|
1735
2487
|
return value;
|
|
1736
|
-
const text = `${
|
|
1737
|
-
issues.push(issueAt(`${
|
|
2488
|
+
const text = `${field2}.blocking is ${describeValue3(value)}, not true or false; ` + "the gap is read as blocking";
|
|
2489
|
+
issues.push(issueAt(`${field2}.blocking`, text));
|
|
1738
2490
|
return true;
|
|
1739
2491
|
}
|
|
1740
|
-
function readGap(item,
|
|
2492
|
+
function readGap(item, field2, issues) {
|
|
1741
2493
|
if (!isMapping2(item)) {
|
|
1742
|
-
const text = `${
|
|
1743
|
-
issues.push(issueAt(
|
|
2494
|
+
const text = `${field2} is ${describeValue3(item)}, not a mapping of heading and what; dropped`;
|
|
2495
|
+
issues.push(issueAt(field2, text));
|
|
1744
2496
|
return null;
|
|
1745
2497
|
}
|
|
1746
2498
|
const what = stringAt(item, "what");
|
|
1747
2499
|
if (what === null) {
|
|
1748
|
-
const text = `${
|
|
1749
|
-
issues.push(issueAt(`${
|
|
2500
|
+
const text = `${field2}.what is ${describeValue3(valueAt(item, "what"))}, ` + "not a sentence; the gap is dropped";
|
|
2501
|
+
issues.push(issueAt(`${field2}.what`, text));
|
|
1750
2502
|
return null;
|
|
1751
2503
|
}
|
|
1752
2504
|
const heading = stringAt(item, "heading");
|
|
1753
2505
|
if (heading === null) {
|
|
1754
|
-
const text = `${
|
|
1755
|
-
issues.push(issueAt(`${
|
|
2506
|
+
const text = `${field2}.heading is ${describeValue3(valueAt(item, "heading"))}, ` + `not a heading; the gap is filed under ${REVIEW_HEADING}`;
|
|
2507
|
+
issues.push(issueAt(`${field2}.heading`, text));
|
|
1756
2508
|
}
|
|
1757
|
-
const blocking = readBlocking(item,
|
|
2509
|
+
const blocking = readBlocking(item, field2, issues);
|
|
1758
2510
|
const assumption = stringAt(item, "assumption");
|
|
1759
2511
|
if (!blocking && assumption === null) {
|
|
1760
|
-
const text = `${
|
|
1761
|
-
issues.push(issueAt(`${
|
|
2512
|
+
const text = `${field2}.assumption is ${describeValue3(valueAt(item, "assumption"))}, ` + "not what a non-blocking gap would be planned under; the gap is read as blocking";
|
|
2513
|
+
issues.push(issueAt(`${field2}.assumption`, text));
|
|
1762
2514
|
return { heading: heading ?? REVIEW_HEADING, what, blocking: true, assumption: null };
|
|
1763
2515
|
}
|
|
1764
2516
|
return { heading: heading ?? REVIEW_HEADING, what, blocking, assumption };
|
|
@@ -1768,7 +2520,7 @@ function readGaps(document, issues) {
|
|
|
1768
2520
|
if (value === null)
|
|
1769
2521
|
return [];
|
|
1770
2522
|
if (!Array.isArray(value)) {
|
|
1771
|
-
const text = `gaps is ${
|
|
2523
|
+
const text = `gaps is ${describeValue3(value)}, not a list; no gap is read from it`;
|
|
1772
2524
|
issues.push(issueAt("gaps", text));
|
|
1773
2525
|
return [];
|
|
1774
2526
|
}
|
|
@@ -1824,12 +2576,12 @@ function parseSpecReview(output) {
|
|
|
1824
2576
|
return unreadable("malformed", block, text);
|
|
1825
2577
|
}
|
|
1826
2578
|
if (!isMapping2(document)) {
|
|
1827
|
-
const text = `the rafa:spec-review block at line ${line} holds ${
|
|
2579
|
+
const text = `the rafa:spec-review block at line ${line} holds ${describeValue3(document)}, ` + "not a mapping of verdict and gaps";
|
|
1828
2580
|
return unreadable("malformed", block, text);
|
|
1829
2581
|
}
|
|
1830
2582
|
const verdict = verdictOf(valueAt(document, "verdict"));
|
|
1831
2583
|
if (verdict === null) {
|
|
1832
|
-
const text = `the rafa:spec-review block at line ${line} has a verdict of ` + `${
|
|
2584
|
+
const text = `the rafa:spec-review block at line ${line} has a verdict of ` + `${describeValue3(valueAt(document, "verdict"))}, ` + `not one of ${SPEC_REVIEW_VERDICTS.join(", ")}`;
|
|
1833
2585
|
return unreadable("malformed", block, text);
|
|
1834
2586
|
}
|
|
1835
2587
|
const issues = [];
|
|
@@ -1905,11 +2657,11 @@ class UndeclaredSpendError extends Error {
|
|
|
1905
2657
|
name = "UndeclaredSpendError";
|
|
1906
2658
|
}
|
|
1907
2659
|
function carriesFlag(flags, flag) {
|
|
1908
|
-
const
|
|
1909
|
-
const value = flags[
|
|
2660
|
+
const name2 = flag.replace(/^--/, "");
|
|
2661
|
+
const value = flags[name2];
|
|
1910
2662
|
if (value !== undefined && value !== false)
|
|
1911
2663
|
return true;
|
|
1912
|
-
return
|
|
2664
|
+
return name2.startsWith("no-") && flags[name2.slice(3)] === false;
|
|
1913
2665
|
}
|
|
1914
2666
|
function spendRefusal(running2) {
|
|
1915
2667
|
if (running2 === null)
|
|
@@ -2035,12 +2787,12 @@ function createClaudePlanner(options) {
|
|
|
2035
2787
|
const create = async ({ specPath, stub }) => {
|
|
2036
2788
|
const planPath = planFilePath(planDir, `PLAN-${stub}.md`);
|
|
2037
2789
|
const prerequisitesPath = planFilePath(planDir, `PREREQUISITES-${stub}.md`);
|
|
2038
|
-
const isWritten = (path) =>
|
|
2790
|
+
const isWritten = (path) => existsSync6(resolve3(repoRoot, path));
|
|
2039
2791
|
if (isWritten(planPath)) {
|
|
2040
2792
|
throw new ClaudePlannerError(`${planPath} already exists`, FAILURE_EXIT_CODE);
|
|
2041
2793
|
}
|
|
2042
|
-
const specContent = await readFile2(
|
|
2043
|
-
await mkdir2(
|
|
2794
|
+
const specContent = await readFile2(resolve3(repoRoot, specPath), "utf8");
|
|
2795
|
+
await mkdir2(resolve3(repoRoot, planDir), { recursive: true });
|
|
2044
2796
|
const session = await runClaudeCaptured(buildPrompt(specContent, stub), settingSources, [], spawn);
|
|
2045
2797
|
const review = parseSpecReview(session.stdout);
|
|
2046
2798
|
const { exitCode } = session;
|
|
@@ -2151,8 +2903,8 @@ async function withDeadline(work, timeoutMs) {
|
|
|
2151
2903
|
if (timeoutMs === undefined)
|
|
2152
2904
|
return work;
|
|
2153
2905
|
let timer;
|
|
2154
|
-
const deadline = new Promise((
|
|
2155
|
-
timer = setTimeout(() =>
|
|
2906
|
+
const deadline = new Promise((resolve4) => {
|
|
2907
|
+
timer = setTimeout(() => resolve4(TIMED_OUT), timeoutMs);
|
|
2156
2908
|
});
|
|
2157
2909
|
try {
|
|
2158
2910
|
return await Promise.race([work, deadline]);
|
|
@@ -2179,8 +2931,8 @@ function isRepoName(value) {
|
|
|
2179
2931
|
function isLabelPart(value) {
|
|
2180
2932
|
return typeof value === "string" && !value.includes(",");
|
|
2181
2933
|
}
|
|
2182
|
-
function optionalFlag(
|
|
2183
|
-
return value === undefined ? [] : [
|
|
2934
|
+
function optionalFlag(name2, value) {
|
|
2935
|
+
return value === undefined ? [] : [name2, value];
|
|
2184
2936
|
}
|
|
2185
2937
|
function detailOf(result) {
|
|
2186
2938
|
return result.stderr.trim() || result.stdout.trim() || "it exited non-zero and wrote nothing";
|
|
@@ -2243,7 +2995,7 @@ function labelNames(value) {
|
|
|
2243
2995
|
if (!Array.isArray(value))
|
|
2244
2996
|
return null;
|
|
2245
2997
|
const names = value.map((label) => isMapping(label) && typeof label["name"] === "string" ? label["name"] : null);
|
|
2246
|
-
return names.every((
|
|
2998
|
+
return names.every((name2) => name2 !== null) ? names : null;
|
|
2247
2999
|
}
|
|
2248
3000
|
function viewProblem(payload, number) {
|
|
2249
3001
|
if (!isMapping(payload))
|
|
@@ -2279,18 +3031,18 @@ function listedIssues(stdout) {
|
|
|
2279
3031
|
throw new Error(`${PREFIX2}: ${command} answered ${describeValue(payload)}, expected a list`);
|
|
2280
3032
|
}
|
|
2281
3033
|
return payload.map((row, index) => {
|
|
2282
|
-
const
|
|
3034
|
+
const refuse2 = (problem) => new Error(`${PREFIX2}: ${command} answered row ${index} ${problem}`);
|
|
2283
3035
|
if (!isMapping(row))
|
|
2284
|
-
throw
|
|
3036
|
+
throw refuse2(`${describeValue(row)}, expected a mapping`);
|
|
2285
3037
|
const { number, url } = row;
|
|
2286
3038
|
const labels = labelNames(row["labels"]);
|
|
2287
3039
|
if (typeof number !== "number" || !isIssueNumber(String(number))) {
|
|
2288
|
-
throw
|
|
3040
|
+
throw refuse2(`with number ${describeValue(number)}, expected an issue number`);
|
|
2289
3041
|
}
|
|
2290
3042
|
if (typeof url !== "string")
|
|
2291
|
-
throw
|
|
3043
|
+
throw refuse2(`with url ${describeValue(url)}, expected a string`);
|
|
2292
3044
|
if (labels === null)
|
|
2293
|
-
throw
|
|
3045
|
+
throw refuse2("with labels that are not a list of named labels");
|
|
2294
3046
|
return { number, url, labels };
|
|
2295
3047
|
});
|
|
2296
3048
|
}
|
|
@@ -2344,9 +3096,9 @@ function createGithubTracker(options) {
|
|
|
2344
3096
|
return result.stdout;
|
|
2345
3097
|
}
|
|
2346
3098
|
async function ensureLabels(names) {
|
|
2347
|
-
const missing = [...new Set(names)].filter((
|
|
2348
|
-
for (const
|
|
2349
|
-
await run(["label", "create",
|
|
3099
|
+
const missing = [...new Set(names)].filter((name2) => !knownLabels.has(name2));
|
|
3100
|
+
for (const name2 of missing) {
|
|
3101
|
+
await run(["label", "create", name2, "--force"], `gh label create ${describeValue(name2)}`);
|
|
2350
3102
|
}
|
|
2351
3103
|
knownLabels = new Set([...knownLabels, ...missing]);
|
|
2352
3104
|
}
|
|
@@ -2457,7 +3209,7 @@ function createGithubTracker(options) {
|
|
|
2457
3209
|
|
|
2458
3210
|
// src/adapters/tracker/local.ts
|
|
2459
3211
|
import { mkdir as mkdir3, readdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
2460
|
-
import { join as
|
|
3212
|
+
import { join as join11 } from "path";
|
|
2461
3213
|
var PREFIX3 = "local tracker";
|
|
2462
3214
|
var FRONTMATTER = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
|
|
2463
3215
|
var ISSUE_NUMBER2 = /^[1-9]\d*$/;
|
|
@@ -2510,11 +3262,11 @@ function parseIssueNumber(text) {
|
|
|
2510
3262
|
const number = Number(text);
|
|
2511
3263
|
return Number.isSafeInteger(number) ? number : null;
|
|
2512
3264
|
}
|
|
2513
|
-
function issueNumberOfFile(
|
|
2514
|
-
return
|
|
3265
|
+
function issueNumberOfFile(name2) {
|
|
3266
|
+
return name2.endsWith(ISSUE_EXTENSION) ? parseIssueNumber(name2.slice(0, -ISSUE_EXTENSION.length)) : null;
|
|
2515
3267
|
}
|
|
2516
3268
|
function issuePath(issuesDir, number) {
|
|
2517
|
-
return
|
|
3269
|
+
return join11(issuesDir, `${number}${ISSUE_EXTENSION}`);
|
|
2518
3270
|
}
|
|
2519
3271
|
async function issueNumbers(issuesDir) {
|
|
2520
3272
|
let names;
|
|
@@ -2528,7 +3280,7 @@ async function issueNumbers(issuesDir) {
|
|
|
2528
3280
|
return names.map(issueNumberOfFile).filter((number) => number !== null).sort((a, b) => a - b);
|
|
2529
3281
|
}
|
|
2530
3282
|
function localIssuesDir(repoRoot) {
|
|
2531
|
-
return
|
|
3283
|
+
return join11(repoRoot, ".rafa", "issues");
|
|
2532
3284
|
}
|
|
2533
3285
|
function renderLocalIssue(record) {
|
|
2534
3286
|
const { draft } = record;
|
|
@@ -2849,7 +3601,7 @@ var CORE_ADAPTERS = [
|
|
|
2849
3601
|
var CORE_ADAPTER_REGISTRY = createAdapterRegistry(CORE_ADAPTERS);
|
|
2850
3602
|
|
|
2851
3603
|
// src/effort/store/index.ts
|
|
2852
|
-
function
|
|
3604
|
+
function describeValue4(value) {
|
|
2853
3605
|
if (typeof value === "string")
|
|
2854
3606
|
return JSON.stringify(value);
|
|
2855
3607
|
if (typeof value === "object" && value !== null)
|
|
@@ -2860,9 +3612,9 @@ function selectEffortStore(repoRoot, config) {
|
|
|
2860
3612
|
const adapter = CORE_ADAPTER_REGISTRY.find("store", config.store);
|
|
2861
3613
|
if (adapter === undefined) {
|
|
2862
3614
|
const expected = CORE_ADAPTER_REGISTRY.kinds("store").join(", ");
|
|
2863
|
-
throw new TypeError(`effort store: store is ${
|
|
3615
|
+
throw new TypeError(`effort store: store is ${describeValue4(config.store)},` + ` expected one of: ${expected}`);
|
|
2864
3616
|
}
|
|
2865
3617
|
return adapter.create({ repoRoot });
|
|
2866
3618
|
}
|
|
2867
3619
|
|
|
2868
|
-
export { EFFORT_STORE_DIR,
|
|
3620
|
+
export { EFFORT_STORE_DIR, effortStoreFileName, EFFORT_DIR_VARIABLE, effortStoreDir, guardTestProcess, EFFORT_KEY_PROJECTIONS, openNdjsonStore, name, version, RAFA_VERSION, versionLine, SCOPE_DIR, INIT_COMMAND, DISK_FILE_SYSTEM, ScopeError, scopeAt, initHint, selfAndAncestors, resolveScope, SessionRecordError, SessionConflictError, SessionStateError, isSessionId, runsDir, sessionFilePath, errorCode, isPidAlive, readSessions, readSession, beginSession, updateSession, readRuntimeIdentity, DEVELOPMENT_NEXT_STEP, ADOPTION_NAME, DEVELOPMENT_MARK, appliedByName, DevelopmentBuildRefusedError, storeProjectRoot, refuseUnownedDevelopmentWrite, LEGACY_GATE_OPEN, LEGACY_GATE_CLOSED, SQLITE_MIGRATIONS, sqliteCatalogue, planSchema, MIGRATION_LOG_TABLE, bringForward, readStoreSchema, writeNames, setActiveStoreSettings, activeStoreSettings, SQLITE_STORE_FILE_NAME, sqliteStorePath, LONE_SURROGATE, withSqliteStore, writeSqliteStore, openSqliteStore, SKILL_FILE, MARKDOWN_EXTENSION, scanLayout, declaredNameIssue, readFrontmatterDocument, readFrontmatter, renderFrontmatter, mergeFrontmatter, writeFrontmatter, normalizeRemote, gitRemoteUrl, ACTION_HEADING, CAUSE_HEADING, parseInstinct, writeInstinct, readScopes, allRecords, findRecords, instinctProject, toHeldRecords, sessionSpawnEnv, getClaudeUsagePercent, checkUsage, CLAUDE_BIN, claudeArgs, interruptClaudeSessions, runClaude, spawnClaudeCaptured, runClaudeCaptured, planFilePath, ClaudePlannerError, ISSUE_TYPES, ISSUE_PRIORITIES, ISSUE_STATES, GITHUB_LABELS, createGhRunner, typeOfLabels, moduleOfLabels, createLocalTracker, PORT_VERSIONS, createAdapterRegistry, CORE_ADAPTER_REGISTRY, selectEffortStore };
|