@open-tomato/rafa 0.32.0 → 0.34.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 +134 -4
- package/dist/bundled/agents/build-error-resolver.md +22 -14
- package/dist/bundled/agents/code-reviewer.md +22 -14
- package/dist/bundled/agents/loop-implementer.md +22 -8
- package/dist/bundled/agents/tdd-guide.md +23 -15
- package/dist/bundled/operators/agents/rafa-stretch-engineer.md +177 -0
- package/dist/bundled/operators/agents/rafa-stretch-watchtower.md +91 -0
- package/dist/bundled/operators/skills/rafa-stretch-gap-log/SKILL.md +72 -0
- package/dist/bundled/operators/skills/rafa-stretch-pit-stop/SKILL.md +77 -0
- package/dist/bundled/operators/skills/rafa-stretch-sweep/SKILL.md +111 -0
- package/dist/bundled/skills/dev-planner/SKILL.md +22 -4
- package/dist/bundled/skills/git-workflow/SKILL.md +1 -1
- package/dist/cli.js +4495 -1789
- package/dist/{describe-d31jy6dk.js → describe-5hqfwbjc.js} +3 -3
- package/dist/effort/store/index.js +24 -4
- package/dist/{index-mt6hpsry.js → index-3mzw3kzh.js} +2 -2
- package/dist/{index-qgayjcym.js → index-4b3m7s0z.js} +7148 -4603
- package/dist/{index-22stnzde.js → index-b8t7q5hc.js} +666 -1137
- package/dist/{index-wvm2s4pr.js → index-bvfbfa63.js} +699 -132
- package/dist/{index-rs79dq9e.js → index-fzhd3d4y.js} +22 -4
- package/dist/{index-5tftj69y.js → index-k6sd4ym5.js} +2 -2
- package/dist/{index-pbeyv281.js → index-v8d7c6sw.js} +2 -2
- package/dist/{index-9aph6ft9.js → index-vp0xghk2.js} +167 -5
- package/dist/index.js +27 -7
- package/dist/plan/index.js +3 -3
- package/dist/plan-prompt.md +12 -0
- package/package.json +5 -2
|
@@ -10,11 +10,12 @@ import {
|
|
|
10
10
|
activeOutputMode,
|
|
11
11
|
configFilePath,
|
|
12
12
|
createTextOutput,
|
|
13
|
+
hubContextOf,
|
|
13
14
|
isStampableStub,
|
|
14
15
|
parseConfigText,
|
|
15
16
|
readRafaBlocks,
|
|
16
17
|
resolveConfig
|
|
17
|
-
} from "./index-
|
|
18
|
+
} from "./index-vp0xghk2.js";
|
|
18
19
|
import {
|
|
19
20
|
bless,
|
|
20
21
|
merge
|
|
@@ -22,21 +23,23 @@ import {
|
|
|
22
23
|
import {
|
|
23
24
|
CommandExit,
|
|
24
25
|
commandSpelling
|
|
25
|
-
} from "./index-
|
|
26
|
+
} from "./index-3mzw3kzh.js";
|
|
26
27
|
import {
|
|
27
28
|
BUSY_TIMEOUT_MAX_MS,
|
|
28
29
|
BUSY_TIMEOUT_MIN_MS,
|
|
29
30
|
RAFA_INSTINCTS_PATH,
|
|
30
31
|
STORE_BACKENDS,
|
|
31
32
|
SYNC_STRATEGIES,
|
|
33
|
+
TEST_SCOPES,
|
|
32
34
|
bundledBinDirectory,
|
|
33
35
|
describeValue,
|
|
34
36
|
isBusyTimeoutMs,
|
|
37
|
+
isGitHubLogin,
|
|
35
38
|
isMapping,
|
|
36
39
|
messageOf,
|
|
37
40
|
resolveInstinctScopes,
|
|
38
41
|
tierExists
|
|
39
|
-
} from "./index-
|
|
42
|
+
} from "./index-fzhd3d4y.js";
|
|
40
43
|
import {
|
|
41
44
|
actionHash,
|
|
42
45
|
triggerKey
|
|
@@ -158,7 +161,7 @@ function appendRows(path, rows, keyOf) {
|
|
|
158
161
|
// src/effort/store/location.ts
|
|
159
162
|
import { existsSync as existsSync2, realpathSync } from "fs";
|
|
160
163
|
import { tmpdir } from "os";
|
|
161
|
-
import { isAbsolute, join as join2, relative, resolve, sep } from "path";
|
|
164
|
+
import { basename, dirname as dirname2, isAbsolute, join as join2, relative, resolve, sep } from "path";
|
|
162
165
|
var EFFORT_DIR_VARIABLE = "RAFA_EFFORT_DIR";
|
|
163
166
|
var TEST_PROCESS_VARIABLE = "RAFA_TEST";
|
|
164
167
|
var TEST_ENTRY_SUFFIX = ".test.ts";
|
|
@@ -166,6 +169,13 @@ function canonical(path) {
|
|
|
166
169
|
const resolved = resolve(path);
|
|
167
170
|
return existsSync2(resolved) ? realpathSync(resolved) : resolved;
|
|
168
171
|
}
|
|
172
|
+
function realPathOf(path) {
|
|
173
|
+
const resolved = resolve(path);
|
|
174
|
+
if (existsSync2(resolved))
|
|
175
|
+
return realpathSync(resolved);
|
|
176
|
+
const parent = dirname2(resolved);
|
|
177
|
+
return parent === resolved ? resolved : join2(realPathOf(parent), basename(resolved));
|
|
178
|
+
}
|
|
169
179
|
function isWithin(path, dir) {
|
|
170
180
|
const between = relative(resolve(dir), resolve(path));
|
|
171
181
|
const climbsOut = between === ".." || between.startsWith(`..${sep}`);
|
|
@@ -190,7 +200,7 @@ function isTestProcess(main = Bun.main, env = process.env) {
|
|
|
190
200
|
return main.endsWith(TEST_ENTRY_SUFFIX) || env[TEST_PROCESS_VARIABLE] === "1";
|
|
191
201
|
}
|
|
192
202
|
function isUnderTempDir(path, tempDir = tmpdir()) {
|
|
193
|
-
return isWithin(path, tempDir) || isWithin(path,
|
|
203
|
+
return isWithin(path, tempDir) || isWithin(realPathOf(path), realPathOf(tempDir));
|
|
194
204
|
}
|
|
195
205
|
function currentProbe() {
|
|
196
206
|
return { main: Bun.main, env: process.env, tempDir: tmpdir() };
|
|
@@ -244,12 +254,12 @@ function openNdjsonStore(repoRoot) {
|
|
|
244
254
|
|
|
245
255
|
// src/effort/store/sqlite.ts
|
|
246
256
|
import { existsSync as existsSync5, mkdirSync as mkdirSync5 } from "fs";
|
|
247
|
-
import { dirname as
|
|
257
|
+
import { dirname as dirname10, join as join9 } from "path";
|
|
248
258
|
import { Database } from "bun:sqlite";
|
|
249
259
|
|
|
250
260
|
// package.json
|
|
251
261
|
var name = "@open-tomato/rafa";
|
|
252
|
-
var version = "0.
|
|
262
|
+
var version = "0.34.0";
|
|
253
263
|
|
|
254
264
|
// src/cli/version.ts
|
|
255
265
|
var RAFA_VERSION = version;
|
|
@@ -259,7 +269,7 @@ function versionLine() {
|
|
|
259
269
|
|
|
260
270
|
// src/effort/store/development-build.ts
|
|
261
271
|
import { tmpdir as tmpdir2 } from "os";
|
|
262
|
-
import { basename as
|
|
272
|
+
import { basename as basename3, dirname as dirname8, isAbsolute as isAbsolute5 } from "path";
|
|
263
273
|
|
|
264
274
|
// src/loop/sessions.ts
|
|
265
275
|
import {
|
|
@@ -271,21 +281,11 @@ import {
|
|
|
271
281
|
rmSync as rmSync3,
|
|
272
282
|
writeFileSync as writeFileSync3
|
|
273
283
|
} from "fs";
|
|
274
|
-
import {
|
|
275
|
-
|
|
276
|
-
// src/next/hop-record.ts
|
|
277
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
278
|
-
import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
279
|
-
import { dirname as dirname4, join as join6 } from "path";
|
|
280
|
-
|
|
281
|
-
// src/project/position.ts
|
|
282
|
-
import { randomUUID } from "crypto";
|
|
283
|
-
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync } from "fs";
|
|
284
|
-
import { dirname as dirname3, join as join5 } from "path";
|
|
284
|
+
import { dirname as dirname6, join as join7 } from "path";
|
|
285
285
|
|
|
286
286
|
// src/project/scope.ts
|
|
287
287
|
import { existsSync as existsSync3, realpathSync as realpathSync2 } from "fs";
|
|
288
|
-
import { dirname as
|
|
288
|
+
import { dirname as dirname3, isAbsolute as isAbsolute3, join as join4 } from "path";
|
|
289
289
|
|
|
290
290
|
// src/project/worktree-root.ts
|
|
291
291
|
import { isAbsolute as isAbsolute2 } from "path";
|
|
@@ -352,7 +352,7 @@ function mainCheckoutOf(dir, seams = {}) {
|
|
|
352
352
|
}
|
|
353
353
|
|
|
354
354
|
// src/project/scope.ts
|
|
355
|
-
var SCOPE_DIR =
|
|
355
|
+
var SCOPE_DIR = dirname3(CONFIG_FILE);
|
|
356
356
|
var INIT_COMMAND = "rafa init";
|
|
357
357
|
var DISK_FILE_SYSTEM = Object.freeze({
|
|
358
358
|
exists: (path) => existsSync3(path),
|
|
@@ -400,19 +400,23 @@ function realHomeOf(home, fs) {
|
|
|
400
400
|
}
|
|
401
401
|
}
|
|
402
402
|
function selfAndAncestors(path) {
|
|
403
|
-
const parent =
|
|
403
|
+
const parent = dirname3(path);
|
|
404
404
|
return parent === path ? [path] : [path, ...selfAndAncestors(parent)];
|
|
405
405
|
}
|
|
406
|
-
|
|
406
|
+
var GIT_MARKER = ".git";
|
|
407
|
+
function walkUp(dirs, realHome, fs, stopAtTopLevel) {
|
|
407
408
|
let passedUserConfig = false;
|
|
408
|
-
for (const dir of
|
|
409
|
-
if (
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
409
|
+
for (const dir of dirs) {
|
|
410
|
+
if (fs.exists(configFilePath(dir))) {
|
|
411
|
+
if (dir !== realHome)
|
|
412
|
+
return { root: dir, passedUserConfig, topLevel: null };
|
|
413
|
+
passedUserConfig = true;
|
|
414
|
+
}
|
|
415
|
+
if (stopAtTopLevel && fs.exists(join4(dir, GIT_MARKER))) {
|
|
416
|
+
return { root: null, passedUserConfig, topLevel: dir };
|
|
417
|
+
}
|
|
414
418
|
}
|
|
415
|
-
return { root: null, passedUserConfig };
|
|
419
|
+
return { root: null, passedUserConfig, topLevel: null };
|
|
416
420
|
}
|
|
417
421
|
function mainCheckoutAt(realStart, mainCheckout) {
|
|
418
422
|
try {
|
|
@@ -430,6 +434,13 @@ function fallBack(realStart, realHome, fs, mainCheckout) {
|
|
|
430
434
|
}
|
|
431
435
|
return fs.exists(configFilePath(main)) ? { root: main, probed: main } : { root: null, probed: main };
|
|
432
436
|
}
|
|
437
|
+
function walkAbove(lower, realHome, fs) {
|
|
438
|
+
if (lower.topLevel === null)
|
|
439
|
+
return lower;
|
|
440
|
+
const above = selfAndAncestors(lower.topLevel).slice(1);
|
|
441
|
+
const upper = walkUp(above, realHome, fs, false);
|
|
442
|
+
return { ...upper, passedUserConfig: lower.passedUserConfig || upper.passedUserConfig };
|
|
443
|
+
}
|
|
433
444
|
function resolveScope(start, seams) {
|
|
434
445
|
const { home, fs = DISK_FILE_SYSTEM, mainCheckout = mainCheckoutOf } = seams;
|
|
435
446
|
requireAbsolute("start directory", start);
|
|
@@ -437,16 +448,29 @@ function resolveScope(start, seams) {
|
|
|
437
448
|
const user = scopeAt(home);
|
|
438
449
|
const realStart = realStartOf(start, fs);
|
|
439
450
|
const realHome = realHomeOf(home, fs);
|
|
440
|
-
const
|
|
441
|
-
const fallback =
|
|
442
|
-
|
|
451
|
+
const lower = walkUp(selfAndAncestors(realStart), realHome, fs, true);
|
|
452
|
+
const fallback = lower.root === null ? fallBack(realStart, realHome, fs, mainCheckout) : { root: lower.root, probed: null };
|
|
453
|
+
const walk = fallback.root === null ? walkAbove(lower, realHome, fs) : lower;
|
|
454
|
+
const root = fallback.root ?? walk.root;
|
|
455
|
+
if (root === null) {
|
|
443
456
|
const passed = walk.passedUserConfig ? user.configFile : null;
|
|
444
457
|
return { found: false, start, home, user, hint: initHint(start, passed, fallback.probed) };
|
|
445
458
|
}
|
|
446
|
-
return { found: true, root
|
|
459
|
+
return { found: true, root, home, project: scopeAt(root), user };
|
|
447
460
|
}
|
|
448
461
|
|
|
462
|
+
// src/loop/session-record-parse.ts
|
|
463
|
+
import { basename as basename2, isAbsolute as isAbsolute4 } from "path";
|
|
464
|
+
|
|
465
|
+
// src/next/hop-record.ts
|
|
466
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
467
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
468
|
+
import { dirname as dirname5, join as join6 } from "path";
|
|
469
|
+
|
|
449
470
|
// src/project/position.ts
|
|
471
|
+
import { randomUUID } from "crypto";
|
|
472
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync } from "fs";
|
|
473
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
450
474
|
var POSITION_FILE = "position.json";
|
|
451
475
|
function positionFilePath(root) {
|
|
452
476
|
return join5(scopeAt(root).dir, POSITION_FILE);
|
|
@@ -496,7 +520,7 @@ function readPositionFile(root) {
|
|
|
496
520
|
}
|
|
497
521
|
function writePositionFile(root, position) {
|
|
498
522
|
const file = positionFilePath(root);
|
|
499
|
-
mkdirSync2(
|
|
523
|
+
mkdirSync2(dirname4(file), { recursive: true });
|
|
500
524
|
const temporary = `${file}.${String(process.pid)}.${randomUUID()}.tmp`;
|
|
501
525
|
const { current, previous, home } = position;
|
|
502
526
|
try {
|
|
@@ -610,7 +634,7 @@ function ownKeys(record) {
|
|
|
610
634
|
}
|
|
611
635
|
function writeHopRecord(root, record) {
|
|
612
636
|
const file = hopFilePath(root);
|
|
613
|
-
mkdirSync3(
|
|
637
|
+
mkdirSync3(dirname5(file), { recursive: true });
|
|
614
638
|
const temporary = `${file}.${String(process.pid)}.${randomUUID2()}.tmp`;
|
|
615
639
|
try {
|
|
616
640
|
writeFileSync2(temporary, `${JSON.stringify(ownKeys(record), null, 2)}
|
|
@@ -625,11 +649,13 @@ function staleAgainst(record, position) {
|
|
|
625
649
|
return position.home.board !== record.home.board || position.home.epic !== record.home.epic;
|
|
626
650
|
}
|
|
627
651
|
|
|
628
|
-
// src/loop/
|
|
652
|
+
// src/loop/session-record-parse.ts
|
|
629
653
|
var SESSION_STATES = Object.freeze(["running", "paused", "stopped", "done"]);
|
|
630
|
-
var
|
|
654
|
+
var SESSION_PHASES = Object.freeze(["task", "wrap-up", "pull-request", "ci", "repair"]);
|
|
655
|
+
var DEFAULT_PHASE = "task";
|
|
631
656
|
var RECORD_EXTENSION = ".json";
|
|
632
657
|
var SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
658
|
+
var SESSION_STEP_KINDS = Object.freeze(["baseline", "task", "stage", "pre-wrap-up"]);
|
|
633
659
|
|
|
634
660
|
class SessionRecordError extends Error {
|
|
635
661
|
file;
|
|
@@ -639,40 +665,9 @@ class SessionRecordError extends Error {
|
|
|
639
665
|
this.file = file;
|
|
640
666
|
}
|
|
641
667
|
}
|
|
642
|
-
|
|
643
|
-
class SessionConflictError extends Error {
|
|
644
|
-
conflicts;
|
|
645
|
-
constructor(conflicts) {
|
|
646
|
-
const named = conflicts.map((conflict) => `${conflict.record.sessionId} (${conflict.reason})`);
|
|
647
|
-
super(`the plan's sessions refuse the run: ${named.join(", ")}`);
|
|
648
|
-
this.name = "SessionConflictError";
|
|
649
|
-
this.conflicts = conflicts;
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
class SessionStateError extends Error {
|
|
654
|
-
file;
|
|
655
|
-
state;
|
|
656
|
-
constructor(file, state, onlyFrom) {
|
|
657
|
-
const acted = onlyFrom.length === 0 ? "no state" : onlyFrom.join(" or ");
|
|
658
|
-
super(`session record ${file}: stores ${state}, and the change acts on ${acted} alone`);
|
|
659
|
-
this.name = "SessionStateError";
|
|
660
|
-
this.file = file;
|
|
661
|
-
this.state = state;
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
668
|
function isSessionId(value) {
|
|
665
669
|
return SESSION_ID_PATTERN.test(value);
|
|
666
670
|
}
|
|
667
|
-
function runsDir(root) {
|
|
668
|
-
return join7(scopeAt(root).dir, RUNS_DIR);
|
|
669
|
-
}
|
|
670
|
-
function sessionFilePath(root, sessionId) {
|
|
671
|
-
if (!isSessionId(sessionId)) {
|
|
672
|
-
throw new Error(`session record: unusable session id ${JSON.stringify(sessionId)}`);
|
|
673
|
-
}
|
|
674
|
-
return join7(runsDir(root), `${sessionId}${RECORD_EXTENSION}`);
|
|
675
|
-
}
|
|
676
671
|
function describeValue2(value) {
|
|
677
672
|
return value === undefined ? "missing" : JSON.stringify(value) ?? String(value);
|
|
678
673
|
}
|
|
@@ -710,11 +705,68 @@ function worktreeProblem(fields) {
|
|
|
710
705
|
const worktree = field(fields, "worktree");
|
|
711
706
|
return isText(worktree) && isAbsolute4(worktree) ? null : `worktree is ${describeValue2(worktree)}, expected an absolute path`;
|
|
712
707
|
}
|
|
708
|
+
function isTextList(value) {
|
|
709
|
+
return Array.isArray(value) && value.every(isText);
|
|
710
|
+
}
|
|
711
|
+
function isFailure(value) {
|
|
712
|
+
return isObject(value) && isText(field(value, "file")) && isText(field(value, "name"));
|
|
713
|
+
}
|
|
714
|
+
function isFailureList(value) {
|
|
715
|
+
return Array.isArray(value) && value.every(isFailure);
|
|
716
|
+
}
|
|
717
|
+
function failureKey(failure) {
|
|
718
|
+
return JSON.stringify([failure.file, failure.name]);
|
|
719
|
+
}
|
|
720
|
+
function stepFailuresProblem(step, at) {
|
|
721
|
+
const failures = field(step, "failures");
|
|
722
|
+
const newFailures = field(step, "newFailures");
|
|
723
|
+
if (!isFailureList(failures))
|
|
724
|
+
return `${at}.failures is ${describeValue2(failures)}, expected a list of failing tests`;
|
|
725
|
+
if (!isFailureList(newFailures)) {
|
|
726
|
+
return `${at}.newFailures is ${describeValue2(newFailures)}, expected a list of failing tests`;
|
|
727
|
+
}
|
|
728
|
+
const known = new Set(failures.map(failureKey));
|
|
729
|
+
const stray = newFailures.find((failure) => !known.has(failureKey(failure)));
|
|
730
|
+
return stray === undefined ? null : `${at}.newFailures names ${describeValue2(stray)}, which ${at}.failures does not`;
|
|
731
|
+
}
|
|
732
|
+
function stepInterruptedProblem(step, at) {
|
|
733
|
+
if (!Object.hasOwn(step, "interrupted"))
|
|
734
|
+
return null;
|
|
735
|
+
const interrupted = field(step, "interrupted");
|
|
736
|
+
return interrupted === true ? null : `${at}.interrupted is ${describeValue2(interrupted)}, expected true or no key`;
|
|
737
|
+
}
|
|
738
|
+
function stepProblems(step, at) {
|
|
739
|
+
if (!isObject(step))
|
|
740
|
+
return [`${at} is ${describeValue2(step)}, expected a step`];
|
|
741
|
+
const kind = field(step, "kind");
|
|
742
|
+
const scope = field(step, "scope");
|
|
743
|
+
const command = field(step, "command");
|
|
744
|
+
const exitCode = field(step, "exitCode");
|
|
745
|
+
const summary = field(step, "summary");
|
|
746
|
+
const problems = [
|
|
747
|
+
SESSION_STEP_KINDS.includes(kind) ? null : `${at}.kind is ${describeValue2(kind)}, expected one of ${SESSION_STEP_KINDS.join(", ")}`,
|
|
748
|
+
TEST_SCOPES.includes(scope) || isTextList(scope) ? null : `${at}.scope is ${describeValue2(scope)}, expected one of ${TEST_SCOPES.join(", ")} or a list of paths`,
|
|
749
|
+
isTextList(command) && command.length > 0 ? null : `${at}.command is ${describeValue2(command)}, expected a non-empty list of arguments`,
|
|
750
|
+
typeof exitCode === "number" && Number.isSafeInteger(exitCode) ? null : `${at}.exitCode is ${describeValue2(exitCode)}, expected a whole number`,
|
|
751
|
+
summary === null || typeof summary === "string" ? null : `${at}.summary is ${describeValue2(summary)}, expected null or a string`,
|
|
752
|
+
stepFailuresProblem(step, at),
|
|
753
|
+
stepInterruptedProblem(step, at)
|
|
754
|
+
];
|
|
755
|
+
return problems.filter((problem) => problem !== null);
|
|
756
|
+
}
|
|
757
|
+
function stepsProblems(fields) {
|
|
758
|
+
if (!Object.hasOwn(fields, "steps"))
|
|
759
|
+
return [];
|
|
760
|
+
const steps = field(fields, "steps");
|
|
761
|
+
if (!Array.isArray(steps))
|
|
762
|
+
return [`steps is ${describeValue2(steps)}, expected a list of steps`];
|
|
763
|
+
return steps.flatMap((step, index) => stepProblems(step, `steps[${index}]`));
|
|
764
|
+
}
|
|
713
765
|
function sessionIdProblem(sessionId, file) {
|
|
714
766
|
if (typeof sessionId !== "string" || !isSessionId(sessionId)) {
|
|
715
767
|
return `sessionId is ${describeValue2(sessionId)}, expected a plain file name`;
|
|
716
768
|
}
|
|
717
|
-
return
|
|
769
|
+
return basename2(file) === `${sessionId}${RECORD_EXTENSION}` ? null : `sessionId ${JSON.stringify(sessionId)} is not the file's name`;
|
|
718
770
|
}
|
|
719
771
|
function planStubProblem(planStub) {
|
|
720
772
|
return planStub === null || typeof planStub === "string" && isStampableStub(planStub) ? null : `planStub is ${describeValue2(planStub)}, expected null or a plan stub`;
|
|
@@ -735,14 +787,41 @@ function recordProblems(fields, file) {
|
|
|
735
787
|
SESSION_STATES.includes(state) ? null : `state is ${describeValue2(state)}, expected one of ${SESSION_STATES.join(", ")}`,
|
|
736
788
|
taskProblem(field(fields, "task")),
|
|
737
789
|
hopProblem(fields),
|
|
738
|
-
worktreeProblem(fields)
|
|
790
|
+
worktreeProblem(fields),
|
|
791
|
+
...stepsProblems(fields)
|
|
739
792
|
];
|
|
740
793
|
return problems.filter((problem) => problem !== null);
|
|
741
794
|
}
|
|
795
|
+
function isSessionPhase(value) {
|
|
796
|
+
return SESSION_PHASES.includes(value);
|
|
797
|
+
}
|
|
798
|
+
function phaseEntry(phase) {
|
|
799
|
+
return isSessionPhase(phase) ? { phase } : {};
|
|
800
|
+
}
|
|
742
801
|
function freezeHop(value) {
|
|
743
802
|
const hop2 = asHopRecord(value);
|
|
744
803
|
return Object.freeze({ ...hop2, home: Object.freeze(hop2.home), from: Object.freeze(hop2.from) });
|
|
745
804
|
}
|
|
805
|
+
function freezeFailures(failures) {
|
|
806
|
+
return Object.freeze(failures.map((failure) => Object.freeze({ file: failure.file, name: failure.name })));
|
|
807
|
+
}
|
|
808
|
+
function freezeStep(step) {
|
|
809
|
+
return Object.freeze({
|
|
810
|
+
kind: step.kind,
|
|
811
|
+
scope: typeof step.scope === "string" ? step.scope : Object.freeze([...step.scope]),
|
|
812
|
+
command: Object.freeze([...step.command]),
|
|
813
|
+
exitCode: step.exitCode,
|
|
814
|
+
summary: step.summary,
|
|
815
|
+
failures: freezeFailures(step.failures),
|
|
816
|
+
newFailures: freezeFailures(step.newFailures),
|
|
817
|
+
...step.interrupted === true ? { interrupted: true } : {}
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
function stepsEntry(steps) {
|
|
821
|
+
if (!Array.isArray(steps) || steps.length === 0)
|
|
822
|
+
return {};
|
|
823
|
+
return { steps: Object.freeze(steps.map(freezeStep)) };
|
|
824
|
+
}
|
|
746
825
|
function freezeRecord(fields) {
|
|
747
826
|
const task = field(fields, "task");
|
|
748
827
|
const hop2 = field(fields, "hop");
|
|
@@ -756,10 +835,18 @@ function freezeRecord(fields) {
|
|
|
756
835
|
startedAt: field(fields, "startedAt"),
|
|
757
836
|
state: field(fields, "state"),
|
|
758
837
|
task: isObject(task) ? Object.freeze({ line: field(task, "line"), text: field(task, "text") }) : null,
|
|
838
|
+
...phaseEntry(field(fields, "phase")),
|
|
759
839
|
...hop2 === undefined ? {} : { hop: freezeHop(hop2) },
|
|
760
|
-
...worktree === undefined ? {} : { worktree }
|
|
840
|
+
...worktree === undefined ? {} : { worktree },
|
|
841
|
+
...stepsEntry(field(fields, "steps"))
|
|
761
842
|
});
|
|
762
843
|
}
|
|
844
|
+
function sessionSteps(record) {
|
|
845
|
+
return record.steps ?? [];
|
|
846
|
+
}
|
|
847
|
+
function sessionPhase(record) {
|
|
848
|
+
return record.phase ?? DEFAULT_PHASE;
|
|
849
|
+
}
|
|
763
850
|
function parseSessionRecord(text, file) {
|
|
764
851
|
let parsed;
|
|
765
852
|
try {
|
|
@@ -774,6 +861,40 @@ function parseSessionRecord(text, file) {
|
|
|
774
861
|
throw new SessionRecordError(file, problems.join("; "));
|
|
775
862
|
return freezeRecord(parsed);
|
|
776
863
|
}
|
|
864
|
+
|
|
865
|
+
// src/loop/sessions.ts
|
|
866
|
+
var RUNS_DIR = "runs";
|
|
867
|
+
|
|
868
|
+
class SessionConflictError extends Error {
|
|
869
|
+
conflicts;
|
|
870
|
+
constructor(conflicts) {
|
|
871
|
+
const named = conflicts.map((conflict) => `${conflict.record.sessionId} (${conflict.reason})`);
|
|
872
|
+
super(`the plan's sessions refuse the run: ${named.join(", ")}`);
|
|
873
|
+
this.name = "SessionConflictError";
|
|
874
|
+
this.conflicts = conflicts;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
class SessionStateError extends Error {
|
|
879
|
+
file;
|
|
880
|
+
state;
|
|
881
|
+
constructor(file, state, onlyFrom) {
|
|
882
|
+
const acted = onlyFrom.length === 0 ? "no state" : onlyFrom.join(" or ");
|
|
883
|
+
super(`session record ${file}: stores ${state}, and the change acts on ${acted} alone`);
|
|
884
|
+
this.name = "SessionStateError";
|
|
885
|
+
this.file = file;
|
|
886
|
+
this.state = state;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
function runsDir(root) {
|
|
890
|
+
return join7(scopeAt(root).dir, RUNS_DIR);
|
|
891
|
+
}
|
|
892
|
+
function sessionFilePath(root, sessionId) {
|
|
893
|
+
if (!isSessionId(sessionId)) {
|
|
894
|
+
throw new Error(`session record: unusable session id ${JSON.stringify(sessionId)}`);
|
|
895
|
+
}
|
|
896
|
+
return join7(runsDir(root), `${sessionId}${RECORD_EXTENSION}`);
|
|
897
|
+
}
|
|
777
898
|
function errorCode(error) {
|
|
778
899
|
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : null;
|
|
779
900
|
}
|
|
@@ -842,7 +963,7 @@ function checkRecord(record, file) {
|
|
|
842
963
|
}
|
|
843
964
|
function writeRecordFile(file, record, isNew) {
|
|
844
965
|
checkRecord(record, file);
|
|
845
|
-
mkdirSync4(
|
|
966
|
+
mkdirSync4(dirname6(file), { recursive: true });
|
|
846
967
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
847
968
|
writeFileSync3(temporary, `${JSON.stringify(record, null, 2)}
|
|
848
969
|
`);
|
|
@@ -870,10 +991,15 @@ function updateSession(root, sessionId, change) {
|
|
|
870
991
|
if (change.onlyFrom !== undefined && !change.onlyFrom.includes(stored.state)) {
|
|
871
992
|
throw new SessionStateError(file, stored.state, change.onlyFrom);
|
|
872
993
|
}
|
|
994
|
+
const appended = change.appendStep === undefined ? [] : stepProblems(change.appendStep, `steps[${sessionSteps(stored).length}]`);
|
|
995
|
+
if (appended.length > 0)
|
|
996
|
+
throw new SessionRecordError(file, `not written: ${appended.join("; ")}`);
|
|
873
997
|
const record = freezeRecord({
|
|
874
998
|
...stored,
|
|
875
999
|
state: change.state ?? stored.state,
|
|
876
|
-
task: change.task === undefined ? stored.task : change.task
|
|
1000
|
+
task: change.task === undefined ? stored.task : change.task,
|
|
1001
|
+
phase: change.phase ?? stored.phase,
|
|
1002
|
+
steps: change.appendStep === undefined ? sessionSteps(stored) : [...sessionSteps(stored), change.appendStep]
|
|
877
1003
|
});
|
|
878
1004
|
writeRecordFile(file, record, false);
|
|
879
1005
|
return record;
|
|
@@ -881,7 +1007,7 @@ function updateSession(root, sessionId, change) {
|
|
|
881
1007
|
|
|
882
1008
|
// src/runtime/identity.ts
|
|
883
1009
|
import { existsSync as existsSync4, readFileSync as readFileSync5, realpathSync as realpathSync3 } from "fs";
|
|
884
|
-
import { dirname as
|
|
1010
|
+
import { dirname as dirname7, join as join8, resolve as resolve2 } from "path";
|
|
885
1011
|
var MANIFEST = "package.json";
|
|
886
1012
|
var CHECKOUT_ENTRY = join8("src", "rafa.ts");
|
|
887
1013
|
function realOrResolved(path) {
|
|
@@ -906,7 +1032,7 @@ function nearestRafaPackage(start) {
|
|
|
906
1032
|
const manifest = join8(dir, MANIFEST);
|
|
907
1033
|
if (existsSync4(manifest) && manifestName(manifest) === name)
|
|
908
1034
|
return dir;
|
|
909
|
-
const parent =
|
|
1035
|
+
const parent = dirname7(dir);
|
|
910
1036
|
if (parent === dir)
|
|
911
1037
|
return null;
|
|
912
1038
|
dir = parent;
|
|
@@ -914,7 +1040,7 @@ function nearestRafaPackage(start) {
|
|
|
914
1040
|
}
|
|
915
1041
|
function readRuntimeIdentity(entry = Bun.main) {
|
|
916
1042
|
const real = realOrResolved(entry);
|
|
917
|
-
const packageDir = nearestRafaPackage(
|
|
1043
|
+
const packageDir = nearestRafaPackage(dirname7(real));
|
|
918
1044
|
if (packageDir !== null && existsSync4(join8(packageDir, CHECKOUT_ENTRY))) {
|
|
919
1045
|
return { kind: "development", entry: real, checkout: packageDir };
|
|
920
1046
|
}
|
|
@@ -941,11 +1067,11 @@ class DevelopmentBuildRefusedError extends Error {
|
|
|
941
1067
|
}
|
|
942
1068
|
}
|
|
943
1069
|
function storeProjectRoot(path) {
|
|
944
|
-
const storeDir =
|
|
945
|
-
const scopeDir =
|
|
946
|
-
if (
|
|
1070
|
+
const storeDir = dirname8(path);
|
|
1071
|
+
const scopeDir = dirname8(storeDir);
|
|
1072
|
+
if (basename3(storeDir) !== EFFORT_DIR_NAME || basename3(scopeDir) !== SCOPE_DIR)
|
|
947
1073
|
return null;
|
|
948
|
-
return
|
|
1074
|
+
return dirname8(scopeDir);
|
|
949
1075
|
}
|
|
950
1076
|
function isOwnedStore(path, env, tempDir) {
|
|
951
1077
|
if (isUnderTempDir(path, tempDir))
|
|
@@ -1575,7 +1701,7 @@ function activeStoreSettings() {
|
|
|
1575
1701
|
|
|
1576
1702
|
// src/effort/store/store-meta.ts
|
|
1577
1703
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
1578
|
-
import { dirname as
|
|
1704
|
+
import { dirname as dirname9 } from "path";
|
|
1579
1705
|
|
|
1580
1706
|
// src/effort/store/store-identity.ts
|
|
1581
1707
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
@@ -1717,7 +1843,7 @@ function readStoreMeta(db) {
|
|
|
1717
1843
|
};
|
|
1718
1844
|
}
|
|
1719
1845
|
function projectFor(storePath, recorded, seams) {
|
|
1720
|
-
const read = (seams.readProject ?? readProjectIdentity)(
|
|
1846
|
+
const read = (seams.readProject ?? readProjectIdentity)(dirname9(storePath));
|
|
1721
1847
|
if (read.rootCommit !== null)
|
|
1722
1848
|
return { rootCommit: read.rootCommit, remote: read.remote };
|
|
1723
1849
|
if (recorded === null)
|
|
@@ -1788,7 +1914,7 @@ function batchEntries(kind, rows) {
|
|
|
1788
1914
|
function withSqliteStore(path, access, create, use, identity = {}) {
|
|
1789
1915
|
guardTestProcess(path);
|
|
1790
1916
|
if (create)
|
|
1791
|
-
mkdirSync5(
|
|
1917
|
+
mkdirSync5(dirname10(path), { recursive: true });
|
|
1792
1918
|
const db = new Database(path, { readwrite: true, create });
|
|
1793
1919
|
try {
|
|
1794
1920
|
db.run(`PRAGMA busy_timeout = ${String(activeStoreSettings().busyTimeoutMs)}`);
|
|
@@ -2448,7 +2574,7 @@ function copyEffortStore(request) {
|
|
|
2448
2574
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
2449
2575
|
import { existsSync as existsSync11, mkdtempSync, rmSync as rmSync6 } from "fs";
|
|
2450
2576
|
import { tmpdir as tmpdir3 } from "os";
|
|
2451
|
-
import { dirname as
|
|
2577
|
+
import { dirname as dirname11, join as join11 } from "path";
|
|
2452
2578
|
import { Database as Database5, SQLiteError as SQLiteError2 } from "bun:sqlite";
|
|
2453
2579
|
|
|
2454
2580
|
// src/effort/commits.ts
|
|
@@ -2865,7 +2991,7 @@ var LIVE2 = "live";
|
|
|
2865
2991
|
function asidePaths2(path, first, stamp) {
|
|
2866
2992
|
return { parallelPath: `${path}.migrate-${stamp}`, backupPath: `${path}.before-${first}-${stamp}.bak` };
|
|
2867
2993
|
}
|
|
2868
|
-
function liveLoop(path, options, nothingDone = "Nothing was migrated.") {
|
|
2994
|
+
function liveLoop(path, options, nothingDone = "Nothing was migrated.", ownSessionId = null) {
|
|
2869
2995
|
const root = storeProjectRoot(path);
|
|
2870
2996
|
if (root === null)
|
|
2871
2997
|
return;
|
|
@@ -2875,7 +3001,7 @@ function liveLoop(path, options, nothingDone = "Nothing was migrated.") {
|
|
|
2875
3001
|
} catch (error) {
|
|
2876
3002
|
throw new RebuildRefusal(`the loop's run records cannot be read, so a live loop on ${path} cannot be ruled out` + ` (${messageOf(error)}). ${nothingDone}`);
|
|
2877
3003
|
}
|
|
2878
|
-
return records.find((record) => record.state === "running" || record.state === "paused");
|
|
3004
|
+
return records.find((record) => (record.state === "running" || record.state === "paused") && record.sessionId !== ownSessionId);
|
|
2879
3005
|
}
|
|
2880
3006
|
function refuseLiveLoop2(path, breaking, options) {
|
|
2881
3007
|
const loop = liveLoop(path, options);
|
|
@@ -3004,15 +3130,15 @@ function recordedMeta(path) {
|
|
|
3004
3130
|
}
|
|
3005
3131
|
function refuseOtherProject(options) {
|
|
3006
3132
|
const other = recordedMeta(options.otherPath);
|
|
3007
|
-
const here = recordedMeta(options.path)?.projectRootCommit ?? (options.readProject ?? readProjectIdentity)(
|
|
3133
|
+
const here = recordedMeta(options.path)?.projectRootCommit ?? (options.readProject ?? readProjectIdentity)(dirname11(options.path)).rootCommit;
|
|
3008
3134
|
const there = other?.projectRootCommit ?? null;
|
|
3009
3135
|
if (here !== null && there !== null && here !== there) {
|
|
3010
3136
|
throw new MergeRefusal("other-project", `REFUSED \u2014 ${options.otherPath} is the effort store of another project (root commit ${there});` + ` this store's project has root commit ${here}. ${NOTHING_MERGED}`);
|
|
3011
3137
|
}
|
|
3012
3138
|
return other?.storeId ?? null;
|
|
3013
3139
|
}
|
|
3014
|
-
function refuseLiveLoop3(path, options) {
|
|
3015
|
-
const loop = liveLoop(path, options, NOTHING_MERGED);
|
|
3140
|
+
function refuseLiveLoop3(path, options, sessionId) {
|
|
3141
|
+
const loop = liveLoop(path, options, NOTHING_MERGED, sessionId);
|
|
3016
3142
|
if (loop === undefined)
|
|
3017
3143
|
return;
|
|
3018
3144
|
throw new MergeRefusal("live-loop", `REFUSED \u2014 loop ${loop.sessionId} (pid ${String(loop.pid)}, plan ${loop.planStub ?? loop.plan}) is running on this store,` + ` and swapping the merged store in under it would lose what it writes. ${NOTHING_MERGED}` + " Finish or stop that loop, then run it again.");
|
|
@@ -3103,7 +3229,7 @@ function refuseUnmergeable(options, migrations) {
|
|
|
3103
3229
|
const otherStore = refuseOtherProject(options);
|
|
3104
3230
|
if (!options.dryRun) {
|
|
3105
3231
|
refuseUnownedDevelopmentWrite(path, [...writeNames(localPlan), MERGES_TABLE], options);
|
|
3106
|
-
refuseLiveLoop3(path, options);
|
|
3232
|
+
refuseLiveLoop3(path, options, options.sessionId ?? null);
|
|
3107
3233
|
}
|
|
3108
3234
|
return { otherPlan, otherStore };
|
|
3109
3235
|
}
|
|
@@ -3171,10 +3297,11 @@ function createFileSync(options) {
|
|
|
3171
3297
|
const copied = copyEffortStore({ source, target });
|
|
3172
3298
|
return Object.freeze({ status: "pushed", path: join12(copied.directory, SQLITE_STORE_FILE_NAME) });
|
|
3173
3299
|
};
|
|
3174
|
-
const pull = async ({ from, dryRun }) => {
|
|
3300
|
+
const pull = async ({ from, dryRun, sessionId = null }) => {
|
|
3175
3301
|
const otherPath = resolve5(repoRoot, requirePath("from", from));
|
|
3176
3302
|
const path = join12(storeDir(), SQLITE_STORE_FILE_NAME);
|
|
3177
|
-
const
|
|
3303
|
+
const stamp = fileStamp(now());
|
|
3304
|
+
const merge2 = mergeStore({ ...mergeSeams(options), path, otherPath, backend, dryRun, stamp, now, sessionId });
|
|
3178
3305
|
return Object.freeze({ status: "pulled", merge: merge2 });
|
|
3179
3306
|
};
|
|
3180
3307
|
return Object.freeze({ kind: "file", push, pull });
|
|
@@ -3218,12 +3345,19 @@ class SyncModuleMissing extends Error {
|
|
|
3218
3345
|
function isModuleStrategy(kind) {
|
|
3219
3346
|
return MODULE_SYNC_STRATEGIES.includes(kind);
|
|
3220
3347
|
}
|
|
3348
|
+
function contextOf(repoRoot, config) {
|
|
3349
|
+
const hub = hubContextOf(config);
|
|
3350
|
+
return {
|
|
3351
|
+
repoRoot,
|
|
3352
|
+
...config.store === undefined ? {} : { store: config.store },
|
|
3353
|
+
...hub === undefined ? {} : { hub }
|
|
3354
|
+
};
|
|
3355
|
+
}
|
|
3221
3356
|
function selectSync(repoRoot, config, registry = CORE_ADAPTER_REGISTRY) {
|
|
3222
3357
|
const kind = config.effortSync;
|
|
3223
3358
|
const adapter = typeof kind === "string" ? registry.find("sync", kind) : undefined;
|
|
3224
|
-
if (adapter !== undefined)
|
|
3225
|
-
return adapter.create(
|
|
3226
|
-
}
|
|
3359
|
+
if (adapter !== undefined)
|
|
3360
|
+
return adapter.create(contextOf(repoRoot, config));
|
|
3227
3361
|
const registered = registry.kinds("sync");
|
|
3228
3362
|
if (isModuleStrategy(kind))
|
|
3229
3363
|
throw new SyncModuleMissing(kind, registered);
|
|
@@ -4335,6 +4469,34 @@ function createLocalLearning(options) {
|
|
|
4335
4469
|
return Object.freeze(learning);
|
|
4336
4470
|
}
|
|
4337
4471
|
|
|
4472
|
+
// src/adapters/output/events.ts
|
|
4473
|
+
var EVENT_PREFIX = "rafa\xB7 ";
|
|
4474
|
+
var KIND_COLUMN = 17;
|
|
4475
|
+
function padKind(kind) {
|
|
4476
|
+
return `${kind} `.padEnd(KIND_COLUMN);
|
|
4477
|
+
}
|
|
4478
|
+
function oneLine(message) {
|
|
4479
|
+
return message.split(`
|
|
4480
|
+
`).map((part) => part.trim()).filter((part) => part !== "").join(" ");
|
|
4481
|
+
}
|
|
4482
|
+
function createEventsOutput({ stream }) {
|
|
4483
|
+
const writeLine = (line) => {
|
|
4484
|
+
stream.write(`${EVENT_PREFIX}${line}
|
|
4485
|
+
`);
|
|
4486
|
+
};
|
|
4487
|
+
const error = (message) => {
|
|
4488
|
+
writeLine(`${padKind("error")}${oneLine(message)}`);
|
|
4489
|
+
};
|
|
4490
|
+
const silent = () => {};
|
|
4491
|
+
const emit = (event) => {
|
|
4492
|
+
if (event.type === "event")
|
|
4493
|
+
writeLine(oneLine(event.summary));
|
|
4494
|
+
if (event.type === "log" && event.level === "error")
|
|
4495
|
+
error(event.message);
|
|
4496
|
+
};
|
|
4497
|
+
return Object.freeze({ info: silent, warn: silent, debug: silent, error, emit, result: silent });
|
|
4498
|
+
}
|
|
4499
|
+
|
|
4338
4500
|
// src/adapters/output/json.ts
|
|
4339
4501
|
var SECOND_RESULT_REFUSAL = "json output: refused a second terminal result event; a command emits exactly one";
|
|
4340
4502
|
function createJsonOutput({
|
|
@@ -4643,7 +4805,7 @@ function guardSpend() {
|
|
|
4643
4805
|
}
|
|
4644
4806
|
async function spawnClaude(args, prompt, options = {}) {
|
|
4645
4807
|
guardSpend();
|
|
4646
|
-
if (activeOutputMode()
|
|
4808
|
+
if (activeOutputMode() !== "text") {
|
|
4647
4809
|
const { exitCode } = await spawnCaptured(args, prompt, options);
|
|
4648
4810
|
return exitCode;
|
|
4649
4811
|
}
|
|
@@ -4677,22 +4839,22 @@ function writeWholeLines(text, write) {
|
|
|
4677
4839
|
}
|
|
4678
4840
|
async function teeToOperator(stream) {
|
|
4679
4841
|
const decoder = new TextDecoder;
|
|
4680
|
-
const
|
|
4842
|
+
const routed = activeOutputMode() === "text" ? null : activeOutput();
|
|
4681
4843
|
const writeLine = (line) => {
|
|
4682
|
-
|
|
4844
|
+
routed?.info(line);
|
|
4683
4845
|
};
|
|
4684
4846
|
let text = "";
|
|
4685
4847
|
let pending = "";
|
|
4686
4848
|
for await (const chunk of stream) {
|
|
4687
|
-
if (
|
|
4849
|
+
if (routed === null)
|
|
4688
4850
|
process.stdout.write(chunk);
|
|
4689
4851
|
const decoded = decoder.decode(chunk, { stream: true });
|
|
4690
4852
|
text += decoded;
|
|
4691
|
-
if (
|
|
4853
|
+
if (routed !== null)
|
|
4692
4854
|
pending = writeWholeLines(pending + decoded, writeLine);
|
|
4693
4855
|
}
|
|
4694
4856
|
const tail = decoder.decode();
|
|
4695
|
-
if (
|
|
4857
|
+
if (routed !== null) {
|
|
4696
4858
|
const rest = writeWholeLines(pending + tail, writeLine);
|
|
4697
4859
|
if (rest !== "")
|
|
4698
4860
|
writeLine(rest);
|
|
@@ -4810,9 +4972,12 @@ var GITHUB_LABELS = Object.freeze({
|
|
|
4810
4972
|
blockedByPrefix: "blocked-by:OPT-"
|
|
4811
4973
|
});
|
|
4812
4974
|
var VIEW_FIELDS = "number,title,body,state,stateReason,labels,url";
|
|
4975
|
+
var LIST_COMMAND = "gh issue list";
|
|
4813
4976
|
var LIST_FIELDS = "number,url,labels";
|
|
4977
|
+
var OPEN_LIST_FIELDS = "number,url,labels,title,body";
|
|
4978
|
+
var OPEN_ISSUES_LIMIT = 1000;
|
|
4814
4979
|
var DEFAULT_FIND_LIMIT = 30;
|
|
4815
|
-
var
|
|
4980
|
+
var CANCELLED_REASONS = new Set(["NOT_PLANNED", "DUPLICATE"]);
|
|
4816
4981
|
var ISSUE_NUMBER = /^[1-9]\d*$/;
|
|
4817
4982
|
var REPO_PART = /^[\w.-]+$/;
|
|
4818
4983
|
var CREATED_ISSUE_URL = /^https:\/\/[^\s/]+\/[\w.-]+\/[\w.-]+\/issues\/([1-9]\d*)$/;
|
|
@@ -4984,28 +5149,49 @@ function viewedIssue(stdout, number) {
|
|
|
4984
5149
|
const checked = payload;
|
|
4985
5150
|
return { ...checked, labels: labelNames(checked.labels) ?? [] };
|
|
4986
5151
|
}
|
|
4987
|
-
function
|
|
4988
|
-
const
|
|
4989
|
-
const payload = parseJson(stdout, command);
|
|
5152
|
+
function listedRows(stdout) {
|
|
5153
|
+
const payload = parseJson(stdout, LIST_COMMAND);
|
|
4990
5154
|
if (!Array.isArray(payload)) {
|
|
4991
|
-
throw new Error(`${PREFIX2}: ${
|
|
4992
|
-
}
|
|
4993
|
-
return payload
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5155
|
+
throw new Error(`${PREFIX2}: ${LIST_COMMAND} answered ${describeValue(payload)}, expected a list`);
|
|
5156
|
+
}
|
|
5157
|
+
return payload;
|
|
5158
|
+
}
|
|
5159
|
+
function rowRefusal(index, problem) {
|
|
5160
|
+
return new Error(`${PREFIX2}: ${LIST_COMMAND} answered row ${index} ${problem}`);
|
|
5161
|
+
}
|
|
5162
|
+
function listedRow(row, index) {
|
|
5163
|
+
if (!isMapping(row))
|
|
5164
|
+
throw rowRefusal(index, `${describeValue(row)}, expected a mapping`);
|
|
5165
|
+
const { number, url } = row;
|
|
5166
|
+
const labels = labelNames(row["labels"]);
|
|
5167
|
+
if (typeof number !== "number" || !isIssueNumber3(String(number))) {
|
|
5168
|
+
throw rowRefusal(index, `with number ${describeValue(number)}, expected an issue number`);
|
|
5169
|
+
}
|
|
5170
|
+
if (typeof url !== "string")
|
|
5171
|
+
throw rowRefusal(index, `with url ${describeValue(url)}, expected a string`);
|
|
5172
|
+
if (labels === null)
|
|
5173
|
+
throw rowRefusal(index, "with labels that are not a list of named labels");
|
|
5174
|
+
return { number, url, labels };
|
|
5175
|
+
}
|
|
5176
|
+
function listedIssues(stdout) {
|
|
5177
|
+
return listedRows(stdout).map(listedRow);
|
|
5178
|
+
}
|
|
5179
|
+
function listedOpenIssues(stdout) {
|
|
5180
|
+
return listedRows(stdout).map((row, index) => {
|
|
5181
|
+
const listed2 = listedRow(row, index);
|
|
5182
|
+
const { title, body } = row;
|
|
5183
|
+
if (typeof title !== "string")
|
|
5184
|
+
throw rowRefusal(index, `with title ${describeValue(title)}, expected a string`);
|
|
5185
|
+
if (typeof body !== "string")
|
|
5186
|
+
throw rowRefusal(index, `with body ${describeValue(body)}, expected a string`);
|
|
5187
|
+
return { ...listed2, title, body };
|
|
5007
5188
|
});
|
|
5008
5189
|
}
|
|
5190
|
+
function refOfRow(row) {
|
|
5191
|
+
const module = labelValue(row.labels, GITHUB_LABELS.modulePrefix);
|
|
5192
|
+
const ref = { opt: 0, kind: "github", externalId: String(row.number), url: row.url };
|
|
5193
|
+
return module === undefined ? ref : { ...ref, module };
|
|
5194
|
+
}
|
|
5009
5195
|
function labelValue(labels, prefix) {
|
|
5010
5196
|
return labels.find((label) => label.startsWith(prefix))?.slice(prefix.length);
|
|
5011
5197
|
}
|
|
@@ -5019,7 +5205,7 @@ function moduleOfLabels(labels) {
|
|
|
5019
5205
|
function stateOf(viewed) {
|
|
5020
5206
|
if (viewed.state === "OPEN")
|
|
5021
5207
|
return "todo";
|
|
5022
|
-
return viewed.stateReason
|
|
5208
|
+
return viewed.stateReason !== null && CANCELLED_REASONS.has(viewed.stateReason) ? "cancelled" : "done";
|
|
5023
5209
|
}
|
|
5024
5210
|
function issueOf(ref, viewed) {
|
|
5025
5211
|
const priority = labelValue(viewed.labels, GITHUB_LABELS.priorityPrefix);
|
|
@@ -5130,12 +5316,26 @@ function createGithubTracker(options) {
|
|
|
5130
5316
|
...optionalFlag("--label", moduleLabel),
|
|
5131
5317
|
...optionalFlag("--label", typeLabel),
|
|
5132
5318
|
...optionalFlag("--search", query.text)
|
|
5133
|
-
],
|
|
5134
|
-
return listedIssues(stdout).map(
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5319
|
+
], LIST_COMMAND);
|
|
5320
|
+
return listedIssues(stdout).map(refOfRow);
|
|
5321
|
+
},
|
|
5322
|
+
openIssues: async (type) => {
|
|
5323
|
+
if (!isOneOf(ISSUE_TYPES, type)) {
|
|
5324
|
+
throw new TypeError(`${PREFIX2}: openIssues refused type ${describeValue(type)}, expected one of: ${ISSUE_TYPES.join(", ")}`);
|
|
5325
|
+
}
|
|
5326
|
+
const stdout = await run([
|
|
5327
|
+
"issue",
|
|
5328
|
+
"list",
|
|
5329
|
+
"--state",
|
|
5330
|
+
"open",
|
|
5331
|
+
"--json",
|
|
5332
|
+
OPEN_LIST_FIELDS,
|
|
5333
|
+
"--limit",
|
|
5334
|
+
String(OPEN_ISSUES_LIMIT),
|
|
5335
|
+
"--label",
|
|
5336
|
+
`${GITHUB_LABELS.typePrefix}${type}`
|
|
5337
|
+
], LIST_COMMAND);
|
|
5338
|
+
return listedOpenIssues(stdout).map((row) => ({ ref: refOfRow(row), title: row.title, body: row.body }));
|
|
5139
5339
|
},
|
|
5140
5340
|
comment: async (ref, body) => {
|
|
5141
5341
|
const number = issueNumberOfRef(ref);
|
|
@@ -5173,6 +5373,7 @@ import { join as join16 } from "path";
|
|
|
5173
5373
|
var PREFIX3 = "local tracker";
|
|
5174
5374
|
var FRONTMATTER = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
|
|
5175
5375
|
var ISSUE_NUMBER2 = /^[1-9]\d*$/;
|
|
5376
|
+
var CLOSED_STATES = new Set(["done", "released", "cancelled"]);
|
|
5176
5377
|
var ISSUE_EXTENSION = ".md";
|
|
5177
5378
|
function isString2(value) {
|
|
5178
5379
|
return typeof value === "string";
|
|
@@ -5304,7 +5505,7 @@ function parseLocalIssue(contents, sourcePath) {
|
|
|
5304
5505
|
function warnThroughActiveOutput2(message) {
|
|
5305
5506
|
activeOutput().warn(message);
|
|
5306
5507
|
}
|
|
5307
|
-
async function listLocalIssues(issuesDir, warn = warnThroughActiveOutput2) {
|
|
5508
|
+
async function listLocalIssues(issuesDir, warn = warnThroughActiveOutput2, reader = "find") {
|
|
5308
5509
|
const numbers = await issueNumbers(issuesDir);
|
|
5309
5510
|
const settled = await Promise.allSettled(numbers.map(async (number) => {
|
|
5310
5511
|
const path = issuePath(issuesDir, number);
|
|
@@ -5319,7 +5520,7 @@ async function listLocalIssues(issuesDir, warn = warnThroughActiveOutput2) {
|
|
|
5319
5520
|
return settled.flatMap((result) => {
|
|
5320
5521
|
if (result.status === "fulfilled")
|
|
5321
5522
|
return [result.value];
|
|
5322
|
-
warn(`${messageOf(result.reason)};
|
|
5523
|
+
warn(`${messageOf(result.reason)}; ${reader} skipped it`);
|
|
5323
5524
|
return [];
|
|
5324
5525
|
});
|
|
5325
5526
|
}
|
|
@@ -5405,6 +5606,14 @@ function createLocalTracker(options) {
|
|
|
5405
5606
|
const matched = (await listLocalIssues(issuesDir, warn)).filter(({ record }) => matchesQuery(record, query));
|
|
5406
5607
|
return matched.slice(0, query.limit ?? matched.length).map(({ number, record }) => refFor(number, record.draft.opt));
|
|
5407
5608
|
},
|
|
5609
|
+
openIssues: async (type) => {
|
|
5610
|
+
const open = (await listLocalIssues(issuesDir, warn, "openIssues")).filter(({ record }) => record.draft.type === type && !CLOSED_STATES.has(record.state));
|
|
5611
|
+
return open.map(({ number, record }) => ({
|
|
5612
|
+
ref: refFor(number, record.draft.opt),
|
|
5613
|
+
title: record.draft.title,
|
|
5614
|
+
body: record.draft.body
|
|
5615
|
+
}));
|
|
5616
|
+
},
|
|
5408
5617
|
comment: async (ref, body) => {
|
|
5409
5618
|
const { number, record } = await read(ref);
|
|
5410
5619
|
await rewrite(number, { ...record, comments: [...record.comments, `${now()}: ${body}`] });
|
|
@@ -5513,6 +5722,12 @@ var CORE_ADAPTERS = [
|
|
|
5513
5722
|
portVersion: PORT_VERSIONS.output,
|
|
5514
5723
|
create: ({ stream = process.stdout }) => createJsonOutput({ stream })
|
|
5515
5724
|
},
|
|
5725
|
+
{
|
|
5726
|
+
port: "output",
|
|
5727
|
+
kind: "events",
|
|
5728
|
+
portVersion: PORT_VERSIONS.output,
|
|
5729
|
+
create: ({ stream = process.stdout }) => createEventsOutput({ stream })
|
|
5730
|
+
},
|
|
5516
5731
|
{
|
|
5517
5732
|
port: "tracker",
|
|
5518
5733
|
kind: "local",
|
|
@@ -5579,6 +5794,358 @@ var CORE_ADAPTERS = [
|
|
|
5579
5794
|
];
|
|
5580
5795
|
var CORE_ADAPTER_REGISTRY = createAdapterRegistry(CORE_ADAPTERS);
|
|
5581
5796
|
|
|
5797
|
+
// src/board/trust.ts
|
|
5798
|
+
var REPO_PATH = "{owner}/{repo}";
|
|
5799
|
+
var TRUSTED_PERMISSIONS = ["admin", "maintain", "write"];
|
|
5800
|
+
var TRUST_REFUSAL_EXIT = 2;
|
|
5801
|
+
var REMEDY = Object.freeze({
|
|
5802
|
+
issue: "a member must open the spec",
|
|
5803
|
+
"pull request": "a member must open the pull request"
|
|
5804
|
+
});
|
|
5805
|
+
var UNREPORTED = "the lookup wrote nothing";
|
|
5806
|
+
function failureDetail(reading) {
|
|
5807
|
+
const detail = reading.permission?.detail ?? "";
|
|
5808
|
+
return detail === "" ? UNREPORTED : detail;
|
|
5809
|
+
}
|
|
5810
|
+
function isTrustedPermission(value) {
|
|
5811
|
+
return TRUSTED_PERMISSIONS.some((permission) => permission === value);
|
|
5812
|
+
}
|
|
5813
|
+
function stringOrNull(value) {
|
|
5814
|
+
return typeof value === "string" ? value : null;
|
|
5815
|
+
}
|
|
5816
|
+
function lookupFailed(login, detail) {
|
|
5817
|
+
return { login, permission: null, roleName: null, detail };
|
|
5818
|
+
}
|
|
5819
|
+
function detailOf2(result, command) {
|
|
5820
|
+
const written = result.stderr.trim() || result.stdout.trim();
|
|
5821
|
+
return written === "" ? `${command} failed and wrote nothing` : `${command} failed: ${written}`;
|
|
5822
|
+
}
|
|
5823
|
+
function createGhPermissions(options) {
|
|
5824
|
+
const { gh } = options;
|
|
5825
|
+
return async (login) => {
|
|
5826
|
+
if (!isGitHubLogin(login)) {
|
|
5827
|
+
return lookupFailed(login, `${describeValue(login)} is not a GitHub login, so no permission was read`);
|
|
5828
|
+
}
|
|
5829
|
+
const path = `repos/${REPO_PATH}/collaborators/${login}/permission`;
|
|
5830
|
+
const command = `gh api ${path}`;
|
|
5831
|
+
const result = await gh(["api", path]);
|
|
5832
|
+
if (!result.ok)
|
|
5833
|
+
return lookupFailed(login, detailOf2(result, command));
|
|
5834
|
+
let payload;
|
|
5835
|
+
try {
|
|
5836
|
+
payload = JSON.parse(result.stdout);
|
|
5837
|
+
} catch (error) {
|
|
5838
|
+
return lookupFailed(login, `${command} wrote output that is not JSON: ${messageOf(error)}`);
|
|
5839
|
+
}
|
|
5840
|
+
if (!isMapping(payload)) {
|
|
5841
|
+
return lookupFailed(login, `${command} answered ${describeValue(payload)}, expected a mapping`);
|
|
5842
|
+
}
|
|
5843
|
+
const permission = stringOrNull(payload["permission"]);
|
|
5844
|
+
const roleName = stringOrNull(payload["role_name"]);
|
|
5845
|
+
return permission === null && roleName === null ? lookupFailed(login, `${command} answered neither a permission nor a role_name`) : { login, permission, roleName, detail: "" };
|
|
5846
|
+
};
|
|
5847
|
+
}
|
|
5848
|
+
async function readAuthorTrust(options) {
|
|
5849
|
+
const { login, permissions, trustedAuthors } = options;
|
|
5850
|
+
if (!isGitHubLogin(login)) {
|
|
5851
|
+
const detail = `${describeValue(login)} is not a GitHub login, so no permission was read`;
|
|
5852
|
+
return {
|
|
5853
|
+
login,
|
|
5854
|
+
trusted: false,
|
|
5855
|
+
source: null,
|
|
5856
|
+
refusal: "lookup-failed",
|
|
5857
|
+
permission: lookupFailed(login, detail)
|
|
5858
|
+
};
|
|
5859
|
+
}
|
|
5860
|
+
const wanted = login.toLowerCase();
|
|
5861
|
+
const listed2 = trustedAuthors.some((author) => author.toLowerCase() === wanted);
|
|
5862
|
+
if (listed2) {
|
|
5863
|
+
return { login, trusted: true, source: "allow-list", refusal: null, permission: null };
|
|
5864
|
+
}
|
|
5865
|
+
const permission = await permissions(login);
|
|
5866
|
+
if (permission.detail !== "") {
|
|
5867
|
+
return { login, trusted: false, source: null, refusal: "lookup-failed", permission };
|
|
5868
|
+
}
|
|
5869
|
+
const trusted = isTrustedPermission(permission.permission) || isTrustedPermission(permission.roleName);
|
|
5870
|
+
return {
|
|
5871
|
+
login,
|
|
5872
|
+
trusted,
|
|
5873
|
+
source: trusted ? "permission" : null,
|
|
5874
|
+
refusal: trusted ? null : "no-write-access",
|
|
5875
|
+
permission
|
|
5876
|
+
};
|
|
5877
|
+
}
|
|
5878
|
+
function ghBoardTrust(options) {
|
|
5879
|
+
return {
|
|
5880
|
+
permissions: createGhPermissions({ gh: options.gh }),
|
|
5881
|
+
trustedAuthors: options.trustedAuthors,
|
|
5882
|
+
repo: options.repo
|
|
5883
|
+
};
|
|
5884
|
+
}
|
|
5885
|
+
function readBoardTrust(trust, login) {
|
|
5886
|
+
return readAuthorTrust({
|
|
5887
|
+
login,
|
|
5888
|
+
permissions: trust.permissions,
|
|
5889
|
+
trustedAuthors: trust.trustedAuthors
|
|
5890
|
+
});
|
|
5891
|
+
}
|
|
5892
|
+
async function requireTrustedBoardAuthor(item, trust, login) {
|
|
5893
|
+
const reading = await readBoardTrust(trust, login);
|
|
5894
|
+
requireTrustedAuthor({ kind: item.kind, number: item.number, repo: trust.repo }, reading);
|
|
5895
|
+
return reading;
|
|
5896
|
+
}
|
|
5897
|
+
function trustRefusalClause(repo, reading) {
|
|
5898
|
+
if (reading.trusted) {
|
|
5899
|
+
throw new TypeError(`board trust: ${reading.login} is trusted, and has no refusal to name`);
|
|
5900
|
+
}
|
|
5901
|
+
return reading.refusal === "lookup-failed" ? `whose write access to ${repo} could not be read (${failureDetail(reading)})` : `who has no write access to ${repo}`;
|
|
5902
|
+
}
|
|
5903
|
+
function trustRefusalMessage(item, reading) {
|
|
5904
|
+
const because = trustRefusalClause(item.repo, reading);
|
|
5905
|
+
const opened = `${item.kind} #${String(item.number)} was opened by ${reading.login}`;
|
|
5906
|
+
return `${opened}, ${because}; ${REMEDY[item.kind]}`;
|
|
5907
|
+
}
|
|
5908
|
+
function requireTrustedAuthor(item, reading) {
|
|
5909
|
+
if (reading.trusted)
|
|
5910
|
+
return;
|
|
5911
|
+
throw new CommandExit(TRUST_REFUSAL_EXIT, trustRefusalMessage(item, reading));
|
|
5912
|
+
}
|
|
5913
|
+
// src/effort/sync/wire.ts
|
|
5914
|
+
import { existsSync as existsSync14, mkdtempSync as mkdtempSync2, rmSync as rmSync7 } from "fs";
|
|
5915
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
5916
|
+
import { join as join17 } from "path";
|
|
5917
|
+
import { Database as Database6, SQLiteError as SQLiteError3 } from "bun:sqlite";
|
|
5918
|
+
var WIRE_FORMAT = "rafa-effort-wire";
|
|
5919
|
+
var WIRE_VERSION = 1;
|
|
5920
|
+
|
|
5921
|
+
class WireFormatError extends Error {
|
|
5922
|
+
name = "WireFormatError";
|
|
5923
|
+
}
|
|
5924
|
+
|
|
5925
|
+
class WireExportRefusal extends Error {
|
|
5926
|
+
name = "WireExportRefusal";
|
|
5927
|
+
}
|
|
5928
|
+
var MERGED_TABLES = Object.entries(MERGE_RULES).filter(([, rule]) => rule.scope === "merged").map(([table]) => table);
|
|
5929
|
+
var ORIGIN_COLUMNS = ["origin_store", "origin_seq"];
|
|
5930
|
+
function isRecord4(value) {
|
|
5931
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5932
|
+
}
|
|
5933
|
+
function isCount(value) {
|
|
5934
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
5935
|
+
}
|
|
5936
|
+
function isMergedTable(name2) {
|
|
5937
|
+
return MERGED_TABLES.includes(name2);
|
|
5938
|
+
}
|
|
5939
|
+
function checkCursor(value) {
|
|
5940
|
+
if (!isRecord4(value))
|
|
5941
|
+
throw new WireFormatError("wire: cursor is not an object");
|
|
5942
|
+
const entries = Object.entries(value).map(([table, seq]) => {
|
|
5943
|
+
if (!isMergedTable(table))
|
|
5944
|
+
throw new WireFormatError(`wire: cursor names ${JSON.stringify(table)}, which is no merged table`);
|
|
5945
|
+
if (!isCount(seq))
|
|
5946
|
+
throw new WireFormatError(`wire: cursor for ${table} is ${JSON.stringify(seq)}, expected a whole number of 0 or more`);
|
|
5947
|
+
return [table, seq];
|
|
5948
|
+
});
|
|
5949
|
+
return Object.freeze(Object.fromEntries(entries));
|
|
5950
|
+
}
|
|
5951
|
+
function checkRow(table, value, index) {
|
|
5952
|
+
const where = `wire: ${table} row ${String(index)}`;
|
|
5953
|
+
if (!isRecord4(value))
|
|
5954
|
+
throw new WireFormatError(`${where} is not an object`);
|
|
5955
|
+
for (const [column, cell] of Object.entries(value)) {
|
|
5956
|
+
const carried = cell === null || typeof cell === "string" || typeof cell === "number" && Number.isFinite(cell);
|
|
5957
|
+
if (!carried)
|
|
5958
|
+
throw new WireFormatError(`${where} holds ${JSON.stringify(cell) ?? typeof cell} in ${column}, expected a string, a number or null`);
|
|
5959
|
+
}
|
|
5960
|
+
const seq = value.seq;
|
|
5961
|
+
if (!isCount(seq) || seq === 0)
|
|
5962
|
+
throw new WireFormatError(`${where} has seq ${JSON.stringify(seq) ?? "undefined"}, expected a whole number above 0`);
|
|
5963
|
+
return Object.freeze({ ...value });
|
|
5964
|
+
}
|
|
5965
|
+
function checkMigrations(value) {
|
|
5966
|
+
if (!Array.isArray(value))
|
|
5967
|
+
throw new WireFormatError("wire: migrations is not a list");
|
|
5968
|
+
const ids = value.map((id) => {
|
|
5969
|
+
if (typeof id !== "string" || id === "")
|
|
5970
|
+
throw new WireFormatError(`wire: migration id ${JSON.stringify(id)} is not a name`);
|
|
5971
|
+
return id;
|
|
5972
|
+
});
|
|
5973
|
+
const twice = ids.filter((id, index) => ids.indexOf(id) !== index);
|
|
5974
|
+
if (twice.length > 0)
|
|
5975
|
+
throw new WireFormatError(`wire: migrations name ${twice.join(", ")} more than once`);
|
|
5976
|
+
return Object.freeze(ids);
|
|
5977
|
+
}
|
|
5978
|
+
function checkTables(value) {
|
|
5979
|
+
if (!isRecord4(value))
|
|
5980
|
+
throw new WireFormatError("wire: tables is not an object");
|
|
5981
|
+
const entries = Object.entries(value).map(([table, rows]) => {
|
|
5982
|
+
if (!isMergedTable(table))
|
|
5983
|
+
throw new WireFormatError(`wire: tables names ${JSON.stringify(table)}, which is no merged table`);
|
|
5984
|
+
if (!Array.isArray(rows))
|
|
5985
|
+
throw new WireFormatError(`wire: tables.${table} is not a list`);
|
|
5986
|
+
return [table, Object.freeze(rows.map((row, index) => checkRow(table, row, index)))];
|
|
5987
|
+
});
|
|
5988
|
+
return Object.freeze(Object.fromEntries(entries));
|
|
5989
|
+
}
|
|
5990
|
+
function checkPayload(value) {
|
|
5991
|
+
if (!isRecord4(value))
|
|
5992
|
+
throw new WireFormatError("wire: the payload is not an object");
|
|
5993
|
+
if (value.format !== WIRE_FORMAT)
|
|
5994
|
+
throw new WireFormatError(`wire: format is ${JSON.stringify(value.format) ?? "undefined"}, expected ${JSON.stringify(WIRE_FORMAT)}`);
|
|
5995
|
+
if (value.version !== WIRE_VERSION)
|
|
5996
|
+
throw new WireFormatError(`wire: version is ${JSON.stringify(value.version) ?? "undefined"}; this rafa reads version ${String(WIRE_VERSION)}`);
|
|
5997
|
+
return Object.freeze({
|
|
5998
|
+
format: WIRE_FORMAT,
|
|
5999
|
+
version: WIRE_VERSION,
|
|
6000
|
+
migrations: checkMigrations(value.migrations),
|
|
6001
|
+
tables: checkTables(value.tables),
|
|
6002
|
+
cursor: checkCursor(value.cursor)
|
|
6003
|
+
});
|
|
6004
|
+
}
|
|
6005
|
+
function encodeWirePayload(payload) {
|
|
6006
|
+
return JSON.stringify(payload);
|
|
6007
|
+
}
|
|
6008
|
+
function decodeWirePayload(text) {
|
|
6009
|
+
let parsed;
|
|
6010
|
+
try {
|
|
6011
|
+
parsed = JSON.parse(text);
|
|
6012
|
+
} catch (error) {
|
|
6013
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
6014
|
+
throw new WireFormatError(`wire: the payload is not JSON (${reason})`);
|
|
6015
|
+
}
|
|
6016
|
+
return checkPayload(parsed);
|
|
6017
|
+
}
|
|
6018
|
+
function readLoggedIds(db) {
|
|
6019
|
+
const hasLog = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(MIGRATION_LOG_TABLE) !== null;
|
|
6020
|
+
if (!hasLog)
|
|
6021
|
+
return null;
|
|
6022
|
+
return db.query(`SELECT id FROM ${MIGRATION_LOG_TABLE} ORDER BY seq`).all().map(({ id }) => id);
|
|
6023
|
+
}
|
|
6024
|
+
function columnsOf(db, table) {
|
|
6025
|
+
return db.query(`PRAGMA table_info(${quoted(table)})`).all().map(({ name: name2 }) => name2);
|
|
6026
|
+
}
|
|
6027
|
+
function wireValue(value, where) {
|
|
6028
|
+
if (typeof value === "bigint") {
|
|
6029
|
+
const number = Number(value);
|
|
6030
|
+
if (Number.isSafeInteger(number))
|
|
6031
|
+
return number;
|
|
6032
|
+
throw new WireExportRefusal(`wire: ${where} holds ${String(value)}, past the whole numbers JSON carries exactly`);
|
|
6033
|
+
}
|
|
6034
|
+
if (value instanceof Uint8Array)
|
|
6035
|
+
throw new WireExportRefusal(`wire: ${where} holds a BLOB, which the wire does not carry`);
|
|
6036
|
+
return value;
|
|
6037
|
+
}
|
|
6038
|
+
function readRows(db, table, columns, after, path) {
|
|
6039
|
+
const named = columns.map(quoted).join(", ");
|
|
6040
|
+
return db.query(`SELECT ${named} FROM ${quoted(table)} WHERE seq > ? ORDER BY seq`).all(after).map((row) => Object.freeze(Object.fromEntries(Object.entries(row).map(([column, value]) => [
|
|
6041
|
+
column,
|
|
6042
|
+
wireValue(value, `${path} ${table}.${column} at seq ${String(row.seq)}`)
|
|
6043
|
+
]))));
|
|
6044
|
+
}
|
|
6045
|
+
function readPayload(db, path, cursor) {
|
|
6046
|
+
const migrations = readLoggedIds(db);
|
|
6047
|
+
if (migrations === null) {
|
|
6048
|
+
throw new WireExportRefusal(`wire: ${path} has no migration log; any rafa command that writes the store brings it forward`);
|
|
6049
|
+
}
|
|
6050
|
+
const tables = {};
|
|
6051
|
+
const next = { ...cursor };
|
|
6052
|
+
for (const table of MERGED_TABLES) {
|
|
6053
|
+
const columns = columnsOf(db, table);
|
|
6054
|
+
if (columns.length === 0)
|
|
6055
|
+
continue;
|
|
6056
|
+
const missing = ORIGIN_COLUMNS.filter((column) => !columns.includes(column));
|
|
6057
|
+
if (missing.length > 0) {
|
|
6058
|
+
throw new WireExportRefusal(`wire: ${path} holds no ${missing.map((column) => `${table}.${column}`).join(", ")}, so its rows carry no origin pair`);
|
|
6059
|
+
}
|
|
6060
|
+
const rows = readRows(db, table, columns, cursor[table] ?? 0, path);
|
|
6061
|
+
tables[table] = Object.freeze(rows);
|
|
6062
|
+
const last = rows[rows.length - 1];
|
|
6063
|
+
next[table] = last === undefined ? cursor[table] ?? 0 : Number(last.seq);
|
|
6064
|
+
}
|
|
6065
|
+
return Object.freeze({
|
|
6066
|
+
format: WIRE_FORMAT,
|
|
6067
|
+
version: WIRE_VERSION,
|
|
6068
|
+
migrations: Object.freeze([...migrations]),
|
|
6069
|
+
tables: Object.freeze(tables),
|
|
6070
|
+
cursor: Object.freeze(next)
|
|
6071
|
+
});
|
|
6072
|
+
}
|
|
6073
|
+
function exportWirePayload(options) {
|
|
6074
|
+
const { path } = options;
|
|
6075
|
+
const cursor = checkCursor(options.cursor ?? {});
|
|
6076
|
+
guardTestProcess(path);
|
|
6077
|
+
if (!existsSync14(path))
|
|
6078
|
+
throw new WireExportRefusal(`wire: ${path} is not there`);
|
|
6079
|
+
const db = new Database6(path, { readonly: true, safeIntegers: true });
|
|
6080
|
+
try {
|
|
6081
|
+
db.run(`PRAGMA busy_timeout = ${String(activeStoreSettings().busyTimeoutMs)}`);
|
|
6082
|
+
return db.transaction(() => readPayload(db, path, cursor))();
|
|
6083
|
+
} finally {
|
|
6084
|
+
db.close();
|
|
6085
|
+
}
|
|
6086
|
+
}
|
|
6087
|
+
function namedMigrations(ids, catalogue) {
|
|
6088
|
+
const known = new Set(catalogue.map(({ id }) => id));
|
|
6089
|
+
const unknown = ids.filter((id) => !known.has(id));
|
|
6090
|
+
if (unknown.length > 0) {
|
|
6091
|
+
throw new WireFormatError(`wire: the payload's store holds migration ${unknown.join(", ")}, which this rafa does not know; update rafa to take its rows`);
|
|
6092
|
+
}
|
|
6093
|
+
return catalogue.filter(({ id }) => ids.includes(id));
|
|
6094
|
+
}
|
|
6095
|
+
function insertRows(db, table, rows) {
|
|
6096
|
+
const columns = columnsOf(db, table);
|
|
6097
|
+
if (columns.length === 0)
|
|
6098
|
+
throw new WireFormatError(`wire: the payload carries ${table}, which its migrations do not make`);
|
|
6099
|
+
const statement = db.query(`INSERT INTO ${quoted(table)} (${columns.map(quoted).join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`);
|
|
6100
|
+
const expected = [...columns].sort().join(",");
|
|
6101
|
+
rows.forEach((row, index) => {
|
|
6102
|
+
const where = `wire: ${table} row ${String(index)}`;
|
|
6103
|
+
const held = Object.keys(row);
|
|
6104
|
+
if ([...held].sort().join(",") !== expected) {
|
|
6105
|
+
throw new WireFormatError(`${where} holds columns ${held.join(", ")}; its migrations make ${columns.join(", ")}`);
|
|
6106
|
+
}
|
|
6107
|
+
try {
|
|
6108
|
+
statement.run(...columns.map((column) => row[column] ?? null));
|
|
6109
|
+
} catch (error) {
|
|
6110
|
+
if (!(error instanceof SQLiteError3))
|
|
6111
|
+
throw error;
|
|
6112
|
+
throw new WireFormatError(`${where} (seq ${String(row.seq)}) is refused by the table: ${error.message}`);
|
|
6113
|
+
}
|
|
6114
|
+
});
|
|
6115
|
+
}
|
|
6116
|
+
function buildStore(path, payload, options) {
|
|
6117
|
+
const migrations = namedMigrations(payload.migrations, options.migrations ?? SQLITE_MIGRATIONS);
|
|
6118
|
+
const db = new Database6(path, { create: true, readwrite: true });
|
|
6119
|
+
try {
|
|
6120
|
+
bringForward(db, path, "write", "open", {
|
|
6121
|
+
migrations,
|
|
6122
|
+
appliedBy: appliedByName(options.identity ?? readRuntimeIdentity()),
|
|
6123
|
+
now: options.now,
|
|
6124
|
+
builtAside: true
|
|
6125
|
+
});
|
|
6126
|
+
db.transaction(() => {
|
|
6127
|
+
for (const [table, rows] of Object.entries(payload.tables))
|
|
6128
|
+
insertRows(db, table, rows);
|
|
6129
|
+
})();
|
|
6130
|
+
} finally {
|
|
6131
|
+
db.close();
|
|
6132
|
+
}
|
|
6133
|
+
}
|
|
6134
|
+
function materialiseWirePayload(payload, options = {}) {
|
|
6135
|
+
const checked = checkPayload(payload);
|
|
6136
|
+
const directory = mkdtempSync2(join17(options.tempDir ?? tmpdir4(), "rafa-wire-"));
|
|
6137
|
+
const release = () => rmSync7(directory, { recursive: true, force: true });
|
|
6138
|
+
const path = join17(directory, SQLITE_STORE_FILE_NAME);
|
|
6139
|
+
try {
|
|
6140
|
+
guardTestProcess(path);
|
|
6141
|
+
buildStore(path, checked, options);
|
|
6142
|
+
} catch (error) {
|
|
6143
|
+
release();
|
|
6144
|
+
throw error;
|
|
6145
|
+
}
|
|
6146
|
+
return Object.freeze({ directory, path, release });
|
|
6147
|
+
}
|
|
6148
|
+
|
|
5582
6149
|
// src/effort/store/index.ts
|
|
5583
6150
|
function describeValue4(value) {
|
|
5584
6151
|
if (typeof value === "string")
|
|
@@ -5596,4 +6163,4 @@ function selectEffortStore(repoRoot, config) {
|
|
|
5596
6163
|
return adapter.create({ repoRoot });
|
|
5597
6164
|
}
|
|
5598
6165
|
|
|
5599
|
-
export { EFFORT_STORE_DIR, effortStoreFileName, EFFORT_DIR_VARIABLE, effortStoreDir, guardTestProcess, EFFORT_KEY_PROJECTIONS, openNdjsonStore, name, version, RAFA_VERSION, versionLine, createGitRunner, gitSaid, NOT_A_REPOSITORY, mainCheckoutOf, SCOPE_DIR, INIT_COMMAND, DISK_FILE_SYSTEM, ScopeError, scopeAt, initHint, selfAndAncestors, resolveScope, POSITION_FILE, positionFilePath, readPositionFile, writePositionFile, positionAt, rehome, hop, goHome, HOP_FILE, hopFilePath, readHopRecord, writeHopRecord, staleAgainst, SessionRecordError, SessionConflictError, SessionStateError,
|
|
6166
|
+
export { EFFORT_STORE_DIR, effortStoreFileName, EFFORT_DIR_VARIABLE, effortStoreDir, guardTestProcess, EFFORT_KEY_PROJECTIONS, openNdjsonStore, name, version, RAFA_VERSION, versionLine, createGitRunner, gitSaid, NOT_A_REPOSITORY, mainCheckoutOf, SCOPE_DIR, INIT_COMMAND, DISK_FILE_SYSTEM, ScopeError, scopeAt, initHint, selfAndAncestors, resolveScope, POSITION_FILE, positionFilePath, readPositionFile, writePositionFile, positionAt, rehome, hop, goHome, HOP_FILE, hopFilePath, readHopRecord, writeHopRecord, staleAgainst, SessionRecordError, isSessionId, sessionPhase, SessionConflictError, SessionStateError, runsDir, sessionFilePath, errorCode, isPidAlive, readSessions, readSession, beginSession, updateSession, DEVELOPMENT_NEXT_STEP, DEVELOPMENT_MARK, DevelopmentBuildRefusedError, refuseUnownedDevelopmentWrite, LEGACY_GATE_OPEN, LEGACY_GATE_CLOSED, SQLITE_MIGRATIONS, sqliteCatalogue, planSchema, SchemaRefusedError, readStoreSchema, writeNames, ACROSS_STORES_ORDER, STAMPED_COLUMNS, stampedValues, activeStoreSettings, normalizeRemote, gitRemoteUrl, readStoreMeta, SQLITE_STORE_FILE_NAME, sqliteStorePath, LONE_SURROGATE, withSqliteStore, writeSqliteStore, openSqliteStore, RebuildRefusal, readConfigFile, loadConfig, plansDirAt, requireProject, resolvePlansDir, resolveProjectConfig, countTasks, formatCounts, checkbox, planFileName, stubOfPlanFile, isFile, issueLine, plural, expectNoArgument, expectOneArgument, expectAtMostOneArgument, readSwitch, fileStamp, fix_schema_default, EffortCopyRefusal, EffortCopyFailure, copyEffortStore, minutesBetween, readCommitLog, UnionSchemaMismatch, migrateStore, MOVE_TO_SQLITE, MergeRefusal, mergeStore, createFileSync, SyncModuleMissing, selectSync, SKILL_FILE, MARKDOWN_EXTENSION, scanLayout, declaredNameIssue, readFrontmatterDocument, readFrontmatter, renderFrontmatter, mergeFrontmatter, writeFrontmatter, ACTION_HEADING, CAUSE_HEADING, parseInstinct, writeInstinct, readScopes, allRecords, findRecords, instinctProject, toHeldRecords, padKind, oneLine, sessionSpawnEnv, getClaudeUsagePercent, checkUsage, CLAUDE_BIN, claudeArgs, interruptClaudeSessions, spawnClaude, runClaude, spawnClaudeCaptured, runClaudeCaptured, planFilePath, ClaudePlannerError, ISSUE_TYPES, ISSUE_PRIORITIES, ISSUE_STATES, GITHUB_LABELS, createGhRunner, typeOfLabels, moduleOfLabels, createLocalTracker, PORT_VERSIONS, createAdapterRegistry, CORE_ADAPTER_REGISTRY, TRUSTED_PERMISSIONS, createGhPermissions, readAuthorTrust, ghBoardTrust, readBoardTrust, requireTrustedBoardAuthor, trustRefusalClause, requireTrustedAuthor, WIRE_FORMAT, WIRE_VERSION, WireFormatError, WireExportRefusal, encodeWirePayload, decodeWirePayload, exportWirePayload, materialiseWirePayload, selectEffortStore };
|