@open-tomato/rafa 0.32.0 → 0.33.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 +109 -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 +172 -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 +71 -0
- package/dist/bundled/operators/skills/rafa-stretch-sweep/SKILL.md +111 -0
- package/dist/bundled/skills/dev-planner/SKILL.md +19 -1
- package/dist/cli.js +3019 -1128
- package/dist/{describe-d31jy6dk.js → describe-5hqfwbjc.js} +3 -3
- package/dist/effort/store/index.js +24 -4
- package/dist/{index-22stnzde.js → index-13caphhp.js} +450 -1119
- package/dist/{index-mt6hpsry.js → index-3mzw3kzh.js} +2 -2
- package/dist/{index-9aph6ft9.js → index-dv0ycmp8.js} +127 -5
- package/dist/{index-rs79dq9e.js → index-fzhd3d4y.js} +22 -4
- package/dist/{index-5tftj69y.js → index-k6sd4ym5.js} +2 -2
- package/dist/{index-wvm2s4pr.js → index-kz7e4gtt.js} +593 -80
- package/dist/{index-qgayjcym.js → index-w3vyb08t.js} +6141 -4087
- package/dist/{index-pbeyv281.js → index-zn08ywsv.js} +2 -2
- 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-dv0ycmp8.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
|
|
@@ -249,7 +252,7 @@ import { Database } from "bun:sqlite";
|
|
|
249
252
|
|
|
250
253
|
// package.json
|
|
251
254
|
var name = "@open-tomato/rafa";
|
|
252
|
-
var version = "0.
|
|
255
|
+
var version = "0.33.0";
|
|
253
256
|
|
|
254
257
|
// src/cli/version.ts
|
|
255
258
|
var RAFA_VERSION = version;
|
|
@@ -271,17 +274,7 @@ import {
|
|
|
271
274
|
rmSync as rmSync3,
|
|
272
275
|
writeFileSync as writeFileSync3
|
|
273
276
|
} 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";
|
|
277
|
+
import { dirname as dirname5, join as join7 } from "path";
|
|
285
278
|
|
|
286
279
|
// src/project/scope.ts
|
|
287
280
|
import { existsSync as existsSync3, realpathSync as realpathSync2 } from "fs";
|
|
@@ -403,16 +396,20 @@ function selfAndAncestors(path) {
|
|
|
403
396
|
const parent = dirname2(path);
|
|
404
397
|
return parent === path ? [path] : [path, ...selfAndAncestors(parent)];
|
|
405
398
|
}
|
|
406
|
-
|
|
399
|
+
var GIT_MARKER = ".git";
|
|
400
|
+
function walkUp(dirs, realHome, fs, stopAtTopLevel) {
|
|
407
401
|
let passedUserConfig = false;
|
|
408
|
-
for (const dir of
|
|
409
|
-
if (
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
402
|
+
for (const dir of dirs) {
|
|
403
|
+
if (fs.exists(configFilePath(dir))) {
|
|
404
|
+
if (dir !== realHome)
|
|
405
|
+
return { root: dir, passedUserConfig, topLevel: null };
|
|
406
|
+
passedUserConfig = true;
|
|
407
|
+
}
|
|
408
|
+
if (stopAtTopLevel && fs.exists(join4(dir, GIT_MARKER))) {
|
|
409
|
+
return { root: null, passedUserConfig, topLevel: dir };
|
|
410
|
+
}
|
|
414
411
|
}
|
|
415
|
-
return { root: null, passedUserConfig };
|
|
412
|
+
return { root: null, passedUserConfig, topLevel: null };
|
|
416
413
|
}
|
|
417
414
|
function mainCheckoutAt(realStart, mainCheckout) {
|
|
418
415
|
try {
|
|
@@ -430,6 +427,13 @@ function fallBack(realStart, realHome, fs, mainCheckout) {
|
|
|
430
427
|
}
|
|
431
428
|
return fs.exists(configFilePath(main)) ? { root: main, probed: main } : { root: null, probed: main };
|
|
432
429
|
}
|
|
430
|
+
function walkAbove(lower, realHome, fs) {
|
|
431
|
+
if (lower.topLevel === null)
|
|
432
|
+
return lower;
|
|
433
|
+
const above = selfAndAncestors(lower.topLevel).slice(1);
|
|
434
|
+
const upper = walkUp(above, realHome, fs, false);
|
|
435
|
+
return { ...upper, passedUserConfig: lower.passedUserConfig || upper.passedUserConfig };
|
|
436
|
+
}
|
|
433
437
|
function resolveScope(start, seams) {
|
|
434
438
|
const { home, fs = DISK_FILE_SYSTEM, mainCheckout = mainCheckoutOf } = seams;
|
|
435
439
|
requireAbsolute("start directory", start);
|
|
@@ -437,16 +441,29 @@ function resolveScope(start, seams) {
|
|
|
437
441
|
const user = scopeAt(home);
|
|
438
442
|
const realStart = realStartOf(start, fs);
|
|
439
443
|
const realHome = realHomeOf(home, fs);
|
|
440
|
-
const
|
|
441
|
-
const fallback =
|
|
442
|
-
|
|
444
|
+
const lower = walkUp(selfAndAncestors(realStart), realHome, fs, true);
|
|
445
|
+
const fallback = lower.root === null ? fallBack(realStart, realHome, fs, mainCheckout) : { root: lower.root, probed: null };
|
|
446
|
+
const walk = fallback.root === null ? walkAbove(lower, realHome, fs) : lower;
|
|
447
|
+
const root = fallback.root ?? walk.root;
|
|
448
|
+
if (root === null) {
|
|
443
449
|
const passed = walk.passedUserConfig ? user.configFile : null;
|
|
444
450
|
return { found: false, start, home, user, hint: initHint(start, passed, fallback.probed) };
|
|
445
451
|
}
|
|
446
|
-
return { found: true, root
|
|
452
|
+
return { found: true, root, home, project: scopeAt(root), user };
|
|
447
453
|
}
|
|
448
454
|
|
|
455
|
+
// src/loop/session-record-parse.ts
|
|
456
|
+
import { basename, isAbsolute as isAbsolute4 } from "path";
|
|
457
|
+
|
|
458
|
+
// src/next/hop-record.ts
|
|
459
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
460
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
461
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
462
|
+
|
|
449
463
|
// src/project/position.ts
|
|
464
|
+
import { randomUUID } from "crypto";
|
|
465
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync } from "fs";
|
|
466
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
450
467
|
var POSITION_FILE = "position.json";
|
|
451
468
|
function positionFilePath(root) {
|
|
452
469
|
return join5(scopeAt(root).dir, POSITION_FILE);
|
|
@@ -625,11 +642,13 @@ function staleAgainst(record, position) {
|
|
|
625
642
|
return position.home.board !== record.home.board || position.home.epic !== record.home.epic;
|
|
626
643
|
}
|
|
627
644
|
|
|
628
|
-
// src/loop/
|
|
645
|
+
// src/loop/session-record-parse.ts
|
|
629
646
|
var SESSION_STATES = Object.freeze(["running", "paused", "stopped", "done"]);
|
|
630
|
-
var
|
|
647
|
+
var SESSION_PHASES = Object.freeze(["task", "wrap-up", "pull-request", "ci", "repair"]);
|
|
648
|
+
var DEFAULT_PHASE = "task";
|
|
631
649
|
var RECORD_EXTENSION = ".json";
|
|
632
650
|
var SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
651
|
+
var SESSION_STEP_KINDS = Object.freeze(["baseline", "task", "stage", "pre-wrap-up"]);
|
|
633
652
|
|
|
634
653
|
class SessionRecordError extends Error {
|
|
635
654
|
file;
|
|
@@ -639,40 +658,9 @@ class SessionRecordError extends Error {
|
|
|
639
658
|
this.file = file;
|
|
640
659
|
}
|
|
641
660
|
}
|
|
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
661
|
function isSessionId(value) {
|
|
665
662
|
return SESSION_ID_PATTERN.test(value);
|
|
666
663
|
}
|
|
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
664
|
function describeValue2(value) {
|
|
677
665
|
return value === undefined ? "missing" : JSON.stringify(value) ?? String(value);
|
|
678
666
|
}
|
|
@@ -710,6 +698,63 @@ function worktreeProblem(fields) {
|
|
|
710
698
|
const worktree = field(fields, "worktree");
|
|
711
699
|
return isText(worktree) && isAbsolute4(worktree) ? null : `worktree is ${describeValue2(worktree)}, expected an absolute path`;
|
|
712
700
|
}
|
|
701
|
+
function isTextList(value) {
|
|
702
|
+
return Array.isArray(value) && value.every(isText);
|
|
703
|
+
}
|
|
704
|
+
function isFailure(value) {
|
|
705
|
+
return isObject(value) && isText(field(value, "file")) && isText(field(value, "name"));
|
|
706
|
+
}
|
|
707
|
+
function isFailureList(value) {
|
|
708
|
+
return Array.isArray(value) && value.every(isFailure);
|
|
709
|
+
}
|
|
710
|
+
function failureKey(failure) {
|
|
711
|
+
return JSON.stringify([failure.file, failure.name]);
|
|
712
|
+
}
|
|
713
|
+
function stepFailuresProblem(step, at) {
|
|
714
|
+
const failures = field(step, "failures");
|
|
715
|
+
const newFailures = field(step, "newFailures");
|
|
716
|
+
if (!isFailureList(failures))
|
|
717
|
+
return `${at}.failures is ${describeValue2(failures)}, expected a list of failing tests`;
|
|
718
|
+
if (!isFailureList(newFailures)) {
|
|
719
|
+
return `${at}.newFailures is ${describeValue2(newFailures)}, expected a list of failing tests`;
|
|
720
|
+
}
|
|
721
|
+
const known = new Set(failures.map(failureKey));
|
|
722
|
+
const stray = newFailures.find((failure) => !known.has(failureKey(failure)));
|
|
723
|
+
return stray === undefined ? null : `${at}.newFailures names ${describeValue2(stray)}, which ${at}.failures does not`;
|
|
724
|
+
}
|
|
725
|
+
function stepInterruptedProblem(step, at) {
|
|
726
|
+
if (!Object.hasOwn(step, "interrupted"))
|
|
727
|
+
return null;
|
|
728
|
+
const interrupted = field(step, "interrupted");
|
|
729
|
+
return interrupted === true ? null : `${at}.interrupted is ${describeValue2(interrupted)}, expected true or no key`;
|
|
730
|
+
}
|
|
731
|
+
function stepProblems(step, at) {
|
|
732
|
+
if (!isObject(step))
|
|
733
|
+
return [`${at} is ${describeValue2(step)}, expected a step`];
|
|
734
|
+
const kind = field(step, "kind");
|
|
735
|
+
const scope = field(step, "scope");
|
|
736
|
+
const command = field(step, "command");
|
|
737
|
+
const exitCode = field(step, "exitCode");
|
|
738
|
+
const summary = field(step, "summary");
|
|
739
|
+
const problems = [
|
|
740
|
+
SESSION_STEP_KINDS.includes(kind) ? null : `${at}.kind is ${describeValue2(kind)}, expected one of ${SESSION_STEP_KINDS.join(", ")}`,
|
|
741
|
+
TEST_SCOPES.includes(scope) || isTextList(scope) ? null : `${at}.scope is ${describeValue2(scope)}, expected one of ${TEST_SCOPES.join(", ")} or a list of paths`,
|
|
742
|
+
isTextList(command) && command.length > 0 ? null : `${at}.command is ${describeValue2(command)}, expected a non-empty list of arguments`,
|
|
743
|
+
typeof exitCode === "number" && Number.isSafeInteger(exitCode) ? null : `${at}.exitCode is ${describeValue2(exitCode)}, expected a whole number`,
|
|
744
|
+
summary === null || typeof summary === "string" ? null : `${at}.summary is ${describeValue2(summary)}, expected null or a string`,
|
|
745
|
+
stepFailuresProblem(step, at),
|
|
746
|
+
stepInterruptedProblem(step, at)
|
|
747
|
+
];
|
|
748
|
+
return problems.filter((problem) => problem !== null);
|
|
749
|
+
}
|
|
750
|
+
function stepsProblems(fields) {
|
|
751
|
+
if (!Object.hasOwn(fields, "steps"))
|
|
752
|
+
return [];
|
|
753
|
+
const steps = field(fields, "steps");
|
|
754
|
+
if (!Array.isArray(steps))
|
|
755
|
+
return [`steps is ${describeValue2(steps)}, expected a list of steps`];
|
|
756
|
+
return steps.flatMap((step, index) => stepProblems(step, `steps[${index}]`));
|
|
757
|
+
}
|
|
713
758
|
function sessionIdProblem(sessionId, file) {
|
|
714
759
|
if (typeof sessionId !== "string" || !isSessionId(sessionId)) {
|
|
715
760
|
return `sessionId is ${describeValue2(sessionId)}, expected a plain file name`;
|
|
@@ -735,14 +780,41 @@ function recordProblems(fields, file) {
|
|
|
735
780
|
SESSION_STATES.includes(state) ? null : `state is ${describeValue2(state)}, expected one of ${SESSION_STATES.join(", ")}`,
|
|
736
781
|
taskProblem(field(fields, "task")),
|
|
737
782
|
hopProblem(fields),
|
|
738
|
-
worktreeProblem(fields)
|
|
783
|
+
worktreeProblem(fields),
|
|
784
|
+
...stepsProblems(fields)
|
|
739
785
|
];
|
|
740
786
|
return problems.filter((problem) => problem !== null);
|
|
741
787
|
}
|
|
788
|
+
function isSessionPhase(value) {
|
|
789
|
+
return SESSION_PHASES.includes(value);
|
|
790
|
+
}
|
|
791
|
+
function phaseEntry(phase) {
|
|
792
|
+
return isSessionPhase(phase) ? { phase } : {};
|
|
793
|
+
}
|
|
742
794
|
function freezeHop(value) {
|
|
743
795
|
const hop2 = asHopRecord(value);
|
|
744
796
|
return Object.freeze({ ...hop2, home: Object.freeze(hop2.home), from: Object.freeze(hop2.from) });
|
|
745
797
|
}
|
|
798
|
+
function freezeFailures(failures) {
|
|
799
|
+
return Object.freeze(failures.map((failure) => Object.freeze({ file: failure.file, name: failure.name })));
|
|
800
|
+
}
|
|
801
|
+
function freezeStep(step) {
|
|
802
|
+
return Object.freeze({
|
|
803
|
+
kind: step.kind,
|
|
804
|
+
scope: typeof step.scope === "string" ? step.scope : Object.freeze([...step.scope]),
|
|
805
|
+
command: Object.freeze([...step.command]),
|
|
806
|
+
exitCode: step.exitCode,
|
|
807
|
+
summary: step.summary,
|
|
808
|
+
failures: freezeFailures(step.failures),
|
|
809
|
+
newFailures: freezeFailures(step.newFailures),
|
|
810
|
+
...step.interrupted === true ? { interrupted: true } : {}
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
function stepsEntry(steps) {
|
|
814
|
+
if (!Array.isArray(steps) || steps.length === 0)
|
|
815
|
+
return {};
|
|
816
|
+
return { steps: Object.freeze(steps.map(freezeStep)) };
|
|
817
|
+
}
|
|
746
818
|
function freezeRecord(fields) {
|
|
747
819
|
const task = field(fields, "task");
|
|
748
820
|
const hop2 = field(fields, "hop");
|
|
@@ -756,10 +828,18 @@ function freezeRecord(fields) {
|
|
|
756
828
|
startedAt: field(fields, "startedAt"),
|
|
757
829
|
state: field(fields, "state"),
|
|
758
830
|
task: isObject(task) ? Object.freeze({ line: field(task, "line"), text: field(task, "text") }) : null,
|
|
831
|
+
...phaseEntry(field(fields, "phase")),
|
|
759
832
|
...hop2 === undefined ? {} : { hop: freezeHop(hop2) },
|
|
760
|
-
...worktree === undefined ? {} : { worktree }
|
|
833
|
+
...worktree === undefined ? {} : { worktree },
|
|
834
|
+
...stepsEntry(field(fields, "steps"))
|
|
761
835
|
});
|
|
762
836
|
}
|
|
837
|
+
function sessionSteps(record) {
|
|
838
|
+
return record.steps ?? [];
|
|
839
|
+
}
|
|
840
|
+
function sessionPhase(record) {
|
|
841
|
+
return record.phase ?? DEFAULT_PHASE;
|
|
842
|
+
}
|
|
763
843
|
function parseSessionRecord(text, file) {
|
|
764
844
|
let parsed;
|
|
765
845
|
try {
|
|
@@ -774,6 +854,40 @@ function parseSessionRecord(text, file) {
|
|
|
774
854
|
throw new SessionRecordError(file, problems.join("; "));
|
|
775
855
|
return freezeRecord(parsed);
|
|
776
856
|
}
|
|
857
|
+
|
|
858
|
+
// src/loop/sessions.ts
|
|
859
|
+
var RUNS_DIR = "runs";
|
|
860
|
+
|
|
861
|
+
class SessionConflictError extends Error {
|
|
862
|
+
conflicts;
|
|
863
|
+
constructor(conflicts) {
|
|
864
|
+
const named = conflicts.map((conflict) => `${conflict.record.sessionId} (${conflict.reason})`);
|
|
865
|
+
super(`the plan's sessions refuse the run: ${named.join(", ")}`);
|
|
866
|
+
this.name = "SessionConflictError";
|
|
867
|
+
this.conflicts = conflicts;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
class SessionStateError extends Error {
|
|
872
|
+
file;
|
|
873
|
+
state;
|
|
874
|
+
constructor(file, state, onlyFrom) {
|
|
875
|
+
const acted = onlyFrom.length === 0 ? "no state" : onlyFrom.join(" or ");
|
|
876
|
+
super(`session record ${file}: stores ${state}, and the change acts on ${acted} alone`);
|
|
877
|
+
this.name = "SessionStateError";
|
|
878
|
+
this.file = file;
|
|
879
|
+
this.state = state;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
function runsDir(root) {
|
|
883
|
+
return join7(scopeAt(root).dir, RUNS_DIR);
|
|
884
|
+
}
|
|
885
|
+
function sessionFilePath(root, sessionId) {
|
|
886
|
+
if (!isSessionId(sessionId)) {
|
|
887
|
+
throw new Error(`session record: unusable session id ${JSON.stringify(sessionId)}`);
|
|
888
|
+
}
|
|
889
|
+
return join7(runsDir(root), `${sessionId}${RECORD_EXTENSION}`);
|
|
890
|
+
}
|
|
777
891
|
function errorCode(error) {
|
|
778
892
|
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : null;
|
|
779
893
|
}
|
|
@@ -870,10 +984,15 @@ function updateSession(root, sessionId, change) {
|
|
|
870
984
|
if (change.onlyFrom !== undefined && !change.onlyFrom.includes(stored.state)) {
|
|
871
985
|
throw new SessionStateError(file, stored.state, change.onlyFrom);
|
|
872
986
|
}
|
|
987
|
+
const appended = change.appendStep === undefined ? [] : stepProblems(change.appendStep, `steps[${sessionSteps(stored).length}]`);
|
|
988
|
+
if (appended.length > 0)
|
|
989
|
+
throw new SessionRecordError(file, `not written: ${appended.join("; ")}`);
|
|
873
990
|
const record = freezeRecord({
|
|
874
991
|
...stored,
|
|
875
992
|
state: change.state ?? stored.state,
|
|
876
|
-
task: change.task === undefined ? stored.task : change.task
|
|
993
|
+
task: change.task === undefined ? stored.task : change.task,
|
|
994
|
+
phase: change.phase ?? stored.phase,
|
|
995
|
+
steps: change.appendStep === undefined ? sessionSteps(stored) : [...sessionSteps(stored), change.appendStep]
|
|
877
996
|
});
|
|
878
997
|
writeRecordFile(file, record, false);
|
|
879
998
|
return record;
|
|
@@ -2865,7 +2984,7 @@ var LIVE2 = "live";
|
|
|
2865
2984
|
function asidePaths2(path, first, stamp) {
|
|
2866
2985
|
return { parallelPath: `${path}.migrate-${stamp}`, backupPath: `${path}.before-${first}-${stamp}.bak` };
|
|
2867
2986
|
}
|
|
2868
|
-
function liveLoop(path, options, nothingDone = "Nothing was migrated.") {
|
|
2987
|
+
function liveLoop(path, options, nothingDone = "Nothing was migrated.", ownSessionId = null) {
|
|
2869
2988
|
const root = storeProjectRoot(path);
|
|
2870
2989
|
if (root === null)
|
|
2871
2990
|
return;
|
|
@@ -2875,7 +2994,7 @@ function liveLoop(path, options, nothingDone = "Nothing was migrated.") {
|
|
|
2875
2994
|
} catch (error) {
|
|
2876
2995
|
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
2996
|
}
|
|
2878
|
-
return records.find((record) => record.state === "running" || record.state === "paused");
|
|
2997
|
+
return records.find((record) => (record.state === "running" || record.state === "paused") && record.sessionId !== ownSessionId);
|
|
2879
2998
|
}
|
|
2880
2999
|
function refuseLiveLoop2(path, breaking, options) {
|
|
2881
3000
|
const loop = liveLoop(path, options);
|
|
@@ -3011,8 +3130,8 @@ function refuseOtherProject(options) {
|
|
|
3011
3130
|
}
|
|
3012
3131
|
return other?.storeId ?? null;
|
|
3013
3132
|
}
|
|
3014
|
-
function refuseLiveLoop3(path, options) {
|
|
3015
|
-
const loop = liveLoop(path, options, NOTHING_MERGED);
|
|
3133
|
+
function refuseLiveLoop3(path, options, sessionId) {
|
|
3134
|
+
const loop = liveLoop(path, options, NOTHING_MERGED, sessionId);
|
|
3016
3135
|
if (loop === undefined)
|
|
3017
3136
|
return;
|
|
3018
3137
|
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 +3222,7 @@ function refuseUnmergeable(options, migrations) {
|
|
|
3103
3222
|
const otherStore = refuseOtherProject(options);
|
|
3104
3223
|
if (!options.dryRun) {
|
|
3105
3224
|
refuseUnownedDevelopmentWrite(path, [...writeNames(localPlan), MERGES_TABLE], options);
|
|
3106
|
-
refuseLiveLoop3(path, options);
|
|
3225
|
+
refuseLiveLoop3(path, options, options.sessionId ?? null);
|
|
3107
3226
|
}
|
|
3108
3227
|
return { otherPlan, otherStore };
|
|
3109
3228
|
}
|
|
@@ -3171,10 +3290,11 @@ function createFileSync(options) {
|
|
|
3171
3290
|
const copied = copyEffortStore({ source, target });
|
|
3172
3291
|
return Object.freeze({ status: "pushed", path: join12(copied.directory, SQLITE_STORE_FILE_NAME) });
|
|
3173
3292
|
};
|
|
3174
|
-
const pull = async ({ from, dryRun }) => {
|
|
3293
|
+
const pull = async ({ from, dryRun, sessionId = null }) => {
|
|
3175
3294
|
const otherPath = resolve5(repoRoot, requirePath("from", from));
|
|
3176
3295
|
const path = join12(storeDir(), SQLITE_STORE_FILE_NAME);
|
|
3177
|
-
const
|
|
3296
|
+
const stamp = fileStamp(now());
|
|
3297
|
+
const merge2 = mergeStore({ ...mergeSeams(options), path, otherPath, backend, dryRun, stamp, now, sessionId });
|
|
3178
3298
|
return Object.freeze({ status: "pulled", merge: merge2 });
|
|
3179
3299
|
};
|
|
3180
3300
|
return Object.freeze({ kind: "file", push, pull });
|
|
@@ -3218,12 +3338,19 @@ class SyncModuleMissing extends Error {
|
|
|
3218
3338
|
function isModuleStrategy(kind) {
|
|
3219
3339
|
return MODULE_SYNC_STRATEGIES.includes(kind);
|
|
3220
3340
|
}
|
|
3341
|
+
function contextOf(repoRoot, config) {
|
|
3342
|
+
const hub = hubContextOf(config);
|
|
3343
|
+
return {
|
|
3344
|
+
repoRoot,
|
|
3345
|
+
...config.store === undefined ? {} : { store: config.store },
|
|
3346
|
+
...hub === undefined ? {} : { hub }
|
|
3347
|
+
};
|
|
3348
|
+
}
|
|
3221
3349
|
function selectSync(repoRoot, config, registry = CORE_ADAPTER_REGISTRY) {
|
|
3222
3350
|
const kind = config.effortSync;
|
|
3223
3351
|
const adapter = typeof kind === "string" ? registry.find("sync", kind) : undefined;
|
|
3224
|
-
if (adapter !== undefined)
|
|
3225
|
-
return adapter.create(
|
|
3226
|
-
}
|
|
3352
|
+
if (adapter !== undefined)
|
|
3353
|
+
return adapter.create(contextOf(repoRoot, config));
|
|
3227
3354
|
const registered = registry.kinds("sync");
|
|
3228
3355
|
if (isModuleStrategy(kind))
|
|
3229
3356
|
throw new SyncModuleMissing(kind, registered);
|
|
@@ -4335,6 +4462,34 @@ function createLocalLearning(options) {
|
|
|
4335
4462
|
return Object.freeze(learning);
|
|
4336
4463
|
}
|
|
4337
4464
|
|
|
4465
|
+
// src/adapters/output/events.ts
|
|
4466
|
+
var EVENT_PREFIX = "rafa\xB7 ";
|
|
4467
|
+
var KIND_COLUMN = 17;
|
|
4468
|
+
function padKind(kind) {
|
|
4469
|
+
return `${kind} `.padEnd(KIND_COLUMN);
|
|
4470
|
+
}
|
|
4471
|
+
function oneLine(message) {
|
|
4472
|
+
return message.split(`
|
|
4473
|
+
`).map((part) => part.trim()).filter((part) => part !== "").join(" ");
|
|
4474
|
+
}
|
|
4475
|
+
function createEventsOutput({ stream }) {
|
|
4476
|
+
const writeLine = (line) => {
|
|
4477
|
+
stream.write(`${EVENT_PREFIX}${line}
|
|
4478
|
+
`);
|
|
4479
|
+
};
|
|
4480
|
+
const error = (message) => {
|
|
4481
|
+
writeLine(`${padKind("error")}${oneLine(message)}`);
|
|
4482
|
+
};
|
|
4483
|
+
const silent = () => {};
|
|
4484
|
+
const emit = (event) => {
|
|
4485
|
+
if (event.type === "event")
|
|
4486
|
+
writeLine(oneLine(event.summary));
|
|
4487
|
+
if (event.type === "log" && event.level === "error")
|
|
4488
|
+
error(event.message);
|
|
4489
|
+
};
|
|
4490
|
+
return Object.freeze({ info: silent, warn: silent, debug: silent, error, emit, result: silent });
|
|
4491
|
+
}
|
|
4492
|
+
|
|
4338
4493
|
// src/adapters/output/json.ts
|
|
4339
4494
|
var SECOND_RESULT_REFUSAL = "json output: refused a second terminal result event; a command emits exactly one";
|
|
4340
4495
|
function createJsonOutput({
|
|
@@ -4643,7 +4798,7 @@ function guardSpend() {
|
|
|
4643
4798
|
}
|
|
4644
4799
|
async function spawnClaude(args, prompt, options = {}) {
|
|
4645
4800
|
guardSpend();
|
|
4646
|
-
if (activeOutputMode()
|
|
4801
|
+
if (activeOutputMode() !== "text") {
|
|
4647
4802
|
const { exitCode } = await spawnCaptured(args, prompt, options);
|
|
4648
4803
|
return exitCode;
|
|
4649
4804
|
}
|
|
@@ -4677,22 +4832,22 @@ function writeWholeLines(text, write) {
|
|
|
4677
4832
|
}
|
|
4678
4833
|
async function teeToOperator(stream) {
|
|
4679
4834
|
const decoder = new TextDecoder;
|
|
4680
|
-
const
|
|
4835
|
+
const routed = activeOutputMode() === "text" ? null : activeOutput();
|
|
4681
4836
|
const writeLine = (line) => {
|
|
4682
|
-
|
|
4837
|
+
routed?.info(line);
|
|
4683
4838
|
};
|
|
4684
4839
|
let text = "";
|
|
4685
4840
|
let pending = "";
|
|
4686
4841
|
for await (const chunk of stream) {
|
|
4687
|
-
if (
|
|
4842
|
+
if (routed === null)
|
|
4688
4843
|
process.stdout.write(chunk);
|
|
4689
4844
|
const decoded = decoder.decode(chunk, { stream: true });
|
|
4690
4845
|
text += decoded;
|
|
4691
|
-
if (
|
|
4846
|
+
if (routed !== null)
|
|
4692
4847
|
pending = writeWholeLines(pending + decoded, writeLine);
|
|
4693
4848
|
}
|
|
4694
4849
|
const tail = decoder.decode();
|
|
4695
|
-
if (
|
|
4850
|
+
if (routed !== null) {
|
|
4696
4851
|
const rest = writeWholeLines(pending + tail, writeLine);
|
|
4697
4852
|
if (rest !== "")
|
|
4698
4853
|
writeLine(rest);
|
|
@@ -5513,6 +5668,12 @@ var CORE_ADAPTERS = [
|
|
|
5513
5668
|
portVersion: PORT_VERSIONS.output,
|
|
5514
5669
|
create: ({ stream = process.stdout }) => createJsonOutput({ stream })
|
|
5515
5670
|
},
|
|
5671
|
+
{
|
|
5672
|
+
port: "output",
|
|
5673
|
+
kind: "events",
|
|
5674
|
+
portVersion: PORT_VERSIONS.output,
|
|
5675
|
+
create: ({ stream = process.stdout }) => createEventsOutput({ stream })
|
|
5676
|
+
},
|
|
5516
5677
|
{
|
|
5517
5678
|
port: "tracker",
|
|
5518
5679
|
kind: "local",
|
|
@@ -5579,6 +5740,358 @@ var CORE_ADAPTERS = [
|
|
|
5579
5740
|
];
|
|
5580
5741
|
var CORE_ADAPTER_REGISTRY = createAdapterRegistry(CORE_ADAPTERS);
|
|
5581
5742
|
|
|
5743
|
+
// src/board/trust.ts
|
|
5744
|
+
var REPO_PATH = "{owner}/{repo}";
|
|
5745
|
+
var TRUSTED_PERMISSIONS = ["admin", "maintain", "write"];
|
|
5746
|
+
var TRUST_REFUSAL_EXIT = 2;
|
|
5747
|
+
var REMEDY = Object.freeze({
|
|
5748
|
+
issue: "a member must open the spec",
|
|
5749
|
+
"pull request": "a member must open the pull request"
|
|
5750
|
+
});
|
|
5751
|
+
var UNREPORTED = "the lookup wrote nothing";
|
|
5752
|
+
function failureDetail(reading) {
|
|
5753
|
+
const detail = reading.permission?.detail ?? "";
|
|
5754
|
+
return detail === "" ? UNREPORTED : detail;
|
|
5755
|
+
}
|
|
5756
|
+
function isTrustedPermission(value) {
|
|
5757
|
+
return TRUSTED_PERMISSIONS.some((permission) => permission === value);
|
|
5758
|
+
}
|
|
5759
|
+
function stringOrNull(value) {
|
|
5760
|
+
return typeof value === "string" ? value : null;
|
|
5761
|
+
}
|
|
5762
|
+
function lookupFailed(login, detail) {
|
|
5763
|
+
return { login, permission: null, roleName: null, detail };
|
|
5764
|
+
}
|
|
5765
|
+
function detailOf2(result, command) {
|
|
5766
|
+
const written = result.stderr.trim() || result.stdout.trim();
|
|
5767
|
+
return written === "" ? `${command} failed and wrote nothing` : `${command} failed: ${written}`;
|
|
5768
|
+
}
|
|
5769
|
+
function createGhPermissions(options) {
|
|
5770
|
+
const { gh } = options;
|
|
5771
|
+
return async (login) => {
|
|
5772
|
+
if (!isGitHubLogin(login)) {
|
|
5773
|
+
return lookupFailed(login, `${describeValue(login)} is not a GitHub login, so no permission was read`);
|
|
5774
|
+
}
|
|
5775
|
+
const path = `repos/${REPO_PATH}/collaborators/${login}/permission`;
|
|
5776
|
+
const command = `gh api ${path}`;
|
|
5777
|
+
const result = await gh(["api", path]);
|
|
5778
|
+
if (!result.ok)
|
|
5779
|
+
return lookupFailed(login, detailOf2(result, command));
|
|
5780
|
+
let payload;
|
|
5781
|
+
try {
|
|
5782
|
+
payload = JSON.parse(result.stdout);
|
|
5783
|
+
} catch (error) {
|
|
5784
|
+
return lookupFailed(login, `${command} wrote output that is not JSON: ${messageOf(error)}`);
|
|
5785
|
+
}
|
|
5786
|
+
if (!isMapping(payload)) {
|
|
5787
|
+
return lookupFailed(login, `${command} answered ${describeValue(payload)}, expected a mapping`);
|
|
5788
|
+
}
|
|
5789
|
+
const permission = stringOrNull(payload["permission"]);
|
|
5790
|
+
const roleName = stringOrNull(payload["role_name"]);
|
|
5791
|
+
return permission === null && roleName === null ? lookupFailed(login, `${command} answered neither a permission nor a role_name`) : { login, permission, roleName, detail: "" };
|
|
5792
|
+
};
|
|
5793
|
+
}
|
|
5794
|
+
async function readAuthorTrust(options) {
|
|
5795
|
+
const { login, permissions, trustedAuthors } = options;
|
|
5796
|
+
if (!isGitHubLogin(login)) {
|
|
5797
|
+
const detail = `${describeValue(login)} is not a GitHub login, so no permission was read`;
|
|
5798
|
+
return {
|
|
5799
|
+
login,
|
|
5800
|
+
trusted: false,
|
|
5801
|
+
source: null,
|
|
5802
|
+
refusal: "lookup-failed",
|
|
5803
|
+
permission: lookupFailed(login, detail)
|
|
5804
|
+
};
|
|
5805
|
+
}
|
|
5806
|
+
const wanted = login.toLowerCase();
|
|
5807
|
+
const listed2 = trustedAuthors.some((author) => author.toLowerCase() === wanted);
|
|
5808
|
+
if (listed2) {
|
|
5809
|
+
return { login, trusted: true, source: "allow-list", refusal: null, permission: null };
|
|
5810
|
+
}
|
|
5811
|
+
const permission = await permissions(login);
|
|
5812
|
+
if (permission.detail !== "") {
|
|
5813
|
+
return { login, trusted: false, source: null, refusal: "lookup-failed", permission };
|
|
5814
|
+
}
|
|
5815
|
+
const trusted = isTrustedPermission(permission.permission) || isTrustedPermission(permission.roleName);
|
|
5816
|
+
return {
|
|
5817
|
+
login,
|
|
5818
|
+
trusted,
|
|
5819
|
+
source: trusted ? "permission" : null,
|
|
5820
|
+
refusal: trusted ? null : "no-write-access",
|
|
5821
|
+
permission
|
|
5822
|
+
};
|
|
5823
|
+
}
|
|
5824
|
+
function ghBoardTrust(options) {
|
|
5825
|
+
return {
|
|
5826
|
+
permissions: createGhPermissions({ gh: options.gh }),
|
|
5827
|
+
trustedAuthors: options.trustedAuthors,
|
|
5828
|
+
repo: options.repo
|
|
5829
|
+
};
|
|
5830
|
+
}
|
|
5831
|
+
function readBoardTrust(trust, login) {
|
|
5832
|
+
return readAuthorTrust({
|
|
5833
|
+
login,
|
|
5834
|
+
permissions: trust.permissions,
|
|
5835
|
+
trustedAuthors: trust.trustedAuthors
|
|
5836
|
+
});
|
|
5837
|
+
}
|
|
5838
|
+
async function requireTrustedBoardAuthor(item, trust, login) {
|
|
5839
|
+
const reading = await readBoardTrust(trust, login);
|
|
5840
|
+
requireTrustedAuthor({ kind: item.kind, number: item.number, repo: trust.repo }, reading);
|
|
5841
|
+
return reading;
|
|
5842
|
+
}
|
|
5843
|
+
function trustRefusalClause(repo, reading) {
|
|
5844
|
+
if (reading.trusted) {
|
|
5845
|
+
throw new TypeError(`board trust: ${reading.login} is trusted, and has no refusal to name`);
|
|
5846
|
+
}
|
|
5847
|
+
return reading.refusal === "lookup-failed" ? `whose write access to ${repo} could not be read (${failureDetail(reading)})` : `who has no write access to ${repo}`;
|
|
5848
|
+
}
|
|
5849
|
+
function trustRefusalMessage(item, reading) {
|
|
5850
|
+
const because = trustRefusalClause(item.repo, reading);
|
|
5851
|
+
const opened = `${item.kind} #${String(item.number)} was opened by ${reading.login}`;
|
|
5852
|
+
return `${opened}, ${because}; ${REMEDY[item.kind]}`;
|
|
5853
|
+
}
|
|
5854
|
+
function requireTrustedAuthor(item, reading) {
|
|
5855
|
+
if (reading.trusted)
|
|
5856
|
+
return;
|
|
5857
|
+
throw new CommandExit(TRUST_REFUSAL_EXIT, trustRefusalMessage(item, reading));
|
|
5858
|
+
}
|
|
5859
|
+
// src/effort/sync/wire.ts
|
|
5860
|
+
import { existsSync as existsSync14, mkdtempSync as mkdtempSync2, rmSync as rmSync7 } from "fs";
|
|
5861
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
5862
|
+
import { join as join17 } from "path";
|
|
5863
|
+
import { Database as Database6, SQLiteError as SQLiteError3 } from "bun:sqlite";
|
|
5864
|
+
var WIRE_FORMAT = "rafa-effort-wire";
|
|
5865
|
+
var WIRE_VERSION = 1;
|
|
5866
|
+
|
|
5867
|
+
class WireFormatError extends Error {
|
|
5868
|
+
name = "WireFormatError";
|
|
5869
|
+
}
|
|
5870
|
+
|
|
5871
|
+
class WireExportRefusal extends Error {
|
|
5872
|
+
name = "WireExportRefusal";
|
|
5873
|
+
}
|
|
5874
|
+
var MERGED_TABLES = Object.entries(MERGE_RULES).filter(([, rule]) => rule.scope === "merged").map(([table]) => table);
|
|
5875
|
+
var ORIGIN_COLUMNS = ["origin_store", "origin_seq"];
|
|
5876
|
+
function isRecord4(value) {
|
|
5877
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5878
|
+
}
|
|
5879
|
+
function isCount(value) {
|
|
5880
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
5881
|
+
}
|
|
5882
|
+
function isMergedTable(name2) {
|
|
5883
|
+
return MERGED_TABLES.includes(name2);
|
|
5884
|
+
}
|
|
5885
|
+
function checkCursor(value) {
|
|
5886
|
+
if (!isRecord4(value))
|
|
5887
|
+
throw new WireFormatError("wire: cursor is not an object");
|
|
5888
|
+
const entries = Object.entries(value).map(([table, seq]) => {
|
|
5889
|
+
if (!isMergedTable(table))
|
|
5890
|
+
throw new WireFormatError(`wire: cursor names ${JSON.stringify(table)}, which is no merged table`);
|
|
5891
|
+
if (!isCount(seq))
|
|
5892
|
+
throw new WireFormatError(`wire: cursor for ${table} is ${JSON.stringify(seq)}, expected a whole number of 0 or more`);
|
|
5893
|
+
return [table, seq];
|
|
5894
|
+
});
|
|
5895
|
+
return Object.freeze(Object.fromEntries(entries));
|
|
5896
|
+
}
|
|
5897
|
+
function checkRow(table, value, index) {
|
|
5898
|
+
const where = `wire: ${table} row ${String(index)}`;
|
|
5899
|
+
if (!isRecord4(value))
|
|
5900
|
+
throw new WireFormatError(`${where} is not an object`);
|
|
5901
|
+
for (const [column, cell] of Object.entries(value)) {
|
|
5902
|
+
const carried = cell === null || typeof cell === "string" || typeof cell === "number" && Number.isFinite(cell);
|
|
5903
|
+
if (!carried)
|
|
5904
|
+
throw new WireFormatError(`${where} holds ${JSON.stringify(cell) ?? typeof cell} in ${column}, expected a string, a number or null`);
|
|
5905
|
+
}
|
|
5906
|
+
const seq = value.seq;
|
|
5907
|
+
if (!isCount(seq) || seq === 0)
|
|
5908
|
+
throw new WireFormatError(`${where} has seq ${JSON.stringify(seq) ?? "undefined"}, expected a whole number above 0`);
|
|
5909
|
+
return Object.freeze({ ...value });
|
|
5910
|
+
}
|
|
5911
|
+
function checkMigrations(value) {
|
|
5912
|
+
if (!Array.isArray(value))
|
|
5913
|
+
throw new WireFormatError("wire: migrations is not a list");
|
|
5914
|
+
const ids = value.map((id) => {
|
|
5915
|
+
if (typeof id !== "string" || id === "")
|
|
5916
|
+
throw new WireFormatError(`wire: migration id ${JSON.stringify(id)} is not a name`);
|
|
5917
|
+
return id;
|
|
5918
|
+
});
|
|
5919
|
+
const twice = ids.filter((id, index) => ids.indexOf(id) !== index);
|
|
5920
|
+
if (twice.length > 0)
|
|
5921
|
+
throw new WireFormatError(`wire: migrations name ${twice.join(", ")} more than once`);
|
|
5922
|
+
return Object.freeze(ids);
|
|
5923
|
+
}
|
|
5924
|
+
function checkTables(value) {
|
|
5925
|
+
if (!isRecord4(value))
|
|
5926
|
+
throw new WireFormatError("wire: tables is not an object");
|
|
5927
|
+
const entries = Object.entries(value).map(([table, rows]) => {
|
|
5928
|
+
if (!isMergedTable(table))
|
|
5929
|
+
throw new WireFormatError(`wire: tables names ${JSON.stringify(table)}, which is no merged table`);
|
|
5930
|
+
if (!Array.isArray(rows))
|
|
5931
|
+
throw new WireFormatError(`wire: tables.${table} is not a list`);
|
|
5932
|
+
return [table, Object.freeze(rows.map((row, index) => checkRow(table, row, index)))];
|
|
5933
|
+
});
|
|
5934
|
+
return Object.freeze(Object.fromEntries(entries));
|
|
5935
|
+
}
|
|
5936
|
+
function checkPayload(value) {
|
|
5937
|
+
if (!isRecord4(value))
|
|
5938
|
+
throw new WireFormatError("wire: the payload is not an object");
|
|
5939
|
+
if (value.format !== WIRE_FORMAT)
|
|
5940
|
+
throw new WireFormatError(`wire: format is ${JSON.stringify(value.format) ?? "undefined"}, expected ${JSON.stringify(WIRE_FORMAT)}`);
|
|
5941
|
+
if (value.version !== WIRE_VERSION)
|
|
5942
|
+
throw new WireFormatError(`wire: version is ${JSON.stringify(value.version) ?? "undefined"}; this rafa reads version ${String(WIRE_VERSION)}`);
|
|
5943
|
+
return Object.freeze({
|
|
5944
|
+
format: WIRE_FORMAT,
|
|
5945
|
+
version: WIRE_VERSION,
|
|
5946
|
+
migrations: checkMigrations(value.migrations),
|
|
5947
|
+
tables: checkTables(value.tables),
|
|
5948
|
+
cursor: checkCursor(value.cursor)
|
|
5949
|
+
});
|
|
5950
|
+
}
|
|
5951
|
+
function encodeWirePayload(payload) {
|
|
5952
|
+
return JSON.stringify(payload);
|
|
5953
|
+
}
|
|
5954
|
+
function decodeWirePayload(text) {
|
|
5955
|
+
let parsed;
|
|
5956
|
+
try {
|
|
5957
|
+
parsed = JSON.parse(text);
|
|
5958
|
+
} catch (error) {
|
|
5959
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
5960
|
+
throw new WireFormatError(`wire: the payload is not JSON (${reason})`);
|
|
5961
|
+
}
|
|
5962
|
+
return checkPayload(parsed);
|
|
5963
|
+
}
|
|
5964
|
+
function readLoggedIds(db) {
|
|
5965
|
+
const hasLog = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(MIGRATION_LOG_TABLE) !== null;
|
|
5966
|
+
if (!hasLog)
|
|
5967
|
+
return null;
|
|
5968
|
+
return db.query(`SELECT id FROM ${MIGRATION_LOG_TABLE} ORDER BY seq`).all().map(({ id }) => id);
|
|
5969
|
+
}
|
|
5970
|
+
function columnsOf(db, table) {
|
|
5971
|
+
return db.query(`PRAGMA table_info(${quoted(table)})`).all().map(({ name: name2 }) => name2);
|
|
5972
|
+
}
|
|
5973
|
+
function wireValue(value, where) {
|
|
5974
|
+
if (typeof value === "bigint") {
|
|
5975
|
+
const number = Number(value);
|
|
5976
|
+
if (Number.isSafeInteger(number))
|
|
5977
|
+
return number;
|
|
5978
|
+
throw new WireExportRefusal(`wire: ${where} holds ${String(value)}, past the whole numbers JSON carries exactly`);
|
|
5979
|
+
}
|
|
5980
|
+
if (value instanceof Uint8Array)
|
|
5981
|
+
throw new WireExportRefusal(`wire: ${where} holds a BLOB, which the wire does not carry`);
|
|
5982
|
+
return value;
|
|
5983
|
+
}
|
|
5984
|
+
function readRows(db, table, columns, after, path) {
|
|
5985
|
+
const named = columns.map(quoted).join(", ");
|
|
5986
|
+
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]) => [
|
|
5987
|
+
column,
|
|
5988
|
+
wireValue(value, `${path} ${table}.${column} at seq ${String(row.seq)}`)
|
|
5989
|
+
]))));
|
|
5990
|
+
}
|
|
5991
|
+
function readPayload(db, path, cursor) {
|
|
5992
|
+
const migrations = readLoggedIds(db);
|
|
5993
|
+
if (migrations === null) {
|
|
5994
|
+
throw new WireExportRefusal(`wire: ${path} has no migration log; any rafa command that writes the store brings it forward`);
|
|
5995
|
+
}
|
|
5996
|
+
const tables = {};
|
|
5997
|
+
const next = { ...cursor };
|
|
5998
|
+
for (const table of MERGED_TABLES) {
|
|
5999
|
+
const columns = columnsOf(db, table);
|
|
6000
|
+
if (columns.length === 0)
|
|
6001
|
+
continue;
|
|
6002
|
+
const missing = ORIGIN_COLUMNS.filter((column) => !columns.includes(column));
|
|
6003
|
+
if (missing.length > 0) {
|
|
6004
|
+
throw new WireExportRefusal(`wire: ${path} holds no ${missing.map((column) => `${table}.${column}`).join(", ")}, so its rows carry no origin pair`);
|
|
6005
|
+
}
|
|
6006
|
+
const rows = readRows(db, table, columns, cursor[table] ?? 0, path);
|
|
6007
|
+
tables[table] = Object.freeze(rows);
|
|
6008
|
+
const last = rows[rows.length - 1];
|
|
6009
|
+
next[table] = last === undefined ? cursor[table] ?? 0 : Number(last.seq);
|
|
6010
|
+
}
|
|
6011
|
+
return Object.freeze({
|
|
6012
|
+
format: WIRE_FORMAT,
|
|
6013
|
+
version: WIRE_VERSION,
|
|
6014
|
+
migrations: Object.freeze([...migrations]),
|
|
6015
|
+
tables: Object.freeze(tables),
|
|
6016
|
+
cursor: Object.freeze(next)
|
|
6017
|
+
});
|
|
6018
|
+
}
|
|
6019
|
+
function exportWirePayload(options) {
|
|
6020
|
+
const { path } = options;
|
|
6021
|
+
const cursor = checkCursor(options.cursor ?? {});
|
|
6022
|
+
guardTestProcess(path);
|
|
6023
|
+
if (!existsSync14(path))
|
|
6024
|
+
throw new WireExportRefusal(`wire: ${path} is not there`);
|
|
6025
|
+
const db = new Database6(path, { readonly: true, safeIntegers: true });
|
|
6026
|
+
try {
|
|
6027
|
+
db.run(`PRAGMA busy_timeout = ${String(activeStoreSettings().busyTimeoutMs)}`);
|
|
6028
|
+
return db.transaction(() => readPayload(db, path, cursor))();
|
|
6029
|
+
} finally {
|
|
6030
|
+
db.close();
|
|
6031
|
+
}
|
|
6032
|
+
}
|
|
6033
|
+
function namedMigrations(ids, catalogue) {
|
|
6034
|
+
const known = new Set(catalogue.map(({ id }) => id));
|
|
6035
|
+
const unknown = ids.filter((id) => !known.has(id));
|
|
6036
|
+
if (unknown.length > 0) {
|
|
6037
|
+
throw new WireFormatError(`wire: the payload's store holds migration ${unknown.join(", ")}, which this rafa does not know; update rafa to take its rows`);
|
|
6038
|
+
}
|
|
6039
|
+
return catalogue.filter(({ id }) => ids.includes(id));
|
|
6040
|
+
}
|
|
6041
|
+
function insertRows(db, table, rows) {
|
|
6042
|
+
const columns = columnsOf(db, table);
|
|
6043
|
+
if (columns.length === 0)
|
|
6044
|
+
throw new WireFormatError(`wire: the payload carries ${table}, which its migrations do not make`);
|
|
6045
|
+
const statement = db.query(`INSERT INTO ${quoted(table)} (${columns.map(quoted).join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`);
|
|
6046
|
+
const expected = [...columns].sort().join(",");
|
|
6047
|
+
rows.forEach((row, index) => {
|
|
6048
|
+
const where = `wire: ${table} row ${String(index)}`;
|
|
6049
|
+
const held = Object.keys(row);
|
|
6050
|
+
if ([...held].sort().join(",") !== expected) {
|
|
6051
|
+
throw new WireFormatError(`${where} holds columns ${held.join(", ")}; its migrations make ${columns.join(", ")}`);
|
|
6052
|
+
}
|
|
6053
|
+
try {
|
|
6054
|
+
statement.run(...columns.map((column) => row[column] ?? null));
|
|
6055
|
+
} catch (error) {
|
|
6056
|
+
if (!(error instanceof SQLiteError3))
|
|
6057
|
+
throw error;
|
|
6058
|
+
throw new WireFormatError(`${where} (seq ${String(row.seq)}) is refused by the table: ${error.message}`);
|
|
6059
|
+
}
|
|
6060
|
+
});
|
|
6061
|
+
}
|
|
6062
|
+
function buildStore(path, payload, options) {
|
|
6063
|
+
const migrations = namedMigrations(payload.migrations, options.migrations ?? SQLITE_MIGRATIONS);
|
|
6064
|
+
const db = new Database6(path, { create: true, readwrite: true });
|
|
6065
|
+
try {
|
|
6066
|
+
bringForward(db, path, "write", "open", {
|
|
6067
|
+
migrations,
|
|
6068
|
+
appliedBy: appliedByName(options.identity ?? readRuntimeIdentity()),
|
|
6069
|
+
now: options.now,
|
|
6070
|
+
builtAside: true
|
|
6071
|
+
});
|
|
6072
|
+
db.transaction(() => {
|
|
6073
|
+
for (const [table, rows] of Object.entries(payload.tables))
|
|
6074
|
+
insertRows(db, table, rows);
|
|
6075
|
+
})();
|
|
6076
|
+
} finally {
|
|
6077
|
+
db.close();
|
|
6078
|
+
}
|
|
6079
|
+
}
|
|
6080
|
+
function materialiseWirePayload(payload, options = {}) {
|
|
6081
|
+
const checked = checkPayload(payload);
|
|
6082
|
+
const directory = mkdtempSync2(join17(options.tempDir ?? tmpdir4(), "rafa-wire-"));
|
|
6083
|
+
const release = () => rmSync7(directory, { recursive: true, force: true });
|
|
6084
|
+
const path = join17(directory, SQLITE_STORE_FILE_NAME);
|
|
6085
|
+
try {
|
|
6086
|
+
guardTestProcess(path);
|
|
6087
|
+
buildStore(path, checked, options);
|
|
6088
|
+
} catch (error) {
|
|
6089
|
+
release();
|
|
6090
|
+
throw error;
|
|
6091
|
+
}
|
|
6092
|
+
return Object.freeze({ directory, path, release });
|
|
6093
|
+
}
|
|
6094
|
+
|
|
5582
6095
|
// src/effort/store/index.ts
|
|
5583
6096
|
function describeValue4(value) {
|
|
5584
6097
|
if (typeof value === "string")
|
|
@@ -5596,4 +6109,4 @@ function selectEffortStore(repoRoot, config) {
|
|
|
5596
6109
|
return adapter.create({ repoRoot });
|
|
5597
6110
|
}
|
|
5598
6111
|
|
|
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,
|
|
6112
|
+
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 };
|