@open-tomato/rafa 0.45.0 → 0.46.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 +255 -23
- package/dist/PROMPT.md +18 -1
- package/dist/adapters/logger/settings.d.ts +132 -0
- package/dist/bugs/closest.d.ts +19 -0
- package/dist/bugs/fenced.d.ts +8 -0
- package/dist/bugs/places.d.ts +20 -0
- package/dist/bugs/record.d.ts +145 -0
- package/dist/bugs/title.d.ts +83 -0
- package/dist/bundled/agents/doc-updater.md +17 -3
- package/dist/bundled/agents/loop-implementer.md +14 -2
- package/dist/bundled/operators/skills/rafa-stretch-pit-stop/SKILL.md +10 -3
- package/dist/bundled/operators/skills/rafa-stretch-sweep/SKILL.md +5 -0
- package/dist/bundled/skills/dev-planner/SKILL.md +101 -18
- package/dist/cli.js +74726 -62960
- package/dist/config/entry.d.ts +119 -0
- package/dist/config/index.d.ts +36 -0
- package/dist/config/index.js +51 -0
- package/dist/config/shape.d.ts +232 -0
- package/dist/config-registry.d.ts +127 -0
- package/dist/config-schema-board-project.d.ts +157 -0
- package/dist/config-schema-bugs.d.ts +71 -0
- package/dist/config-schema-errors.d.ts +54 -0
- package/dist/config-schema-features.d.ts +64 -0
- package/dist/config-schema-hub.d.ts +150 -0
- package/dist/config-schema-issues.d.ts +95 -0
- package/dist/config-schema-logger.d.ts +91 -0
- package/dist/config-schema-loop-continue.d.ts +122 -0
- package/dist/config-schema-loop-retries.d.ts +94 -0
- package/dist/config-schema-release.d.ts +307 -0
- package/dist/config-schema-tests.d.ts +170 -0
- package/dist/config-schema-triage.d.ts +142 -0
- package/dist/config-schema-wrap-up.d.ts +96 -0
- package/dist/core/command/command.d.ts +228 -0
- package/dist/core/command/confirm.d.ts +67 -0
- package/dist/core/command/index.d.ts +112 -0
- package/dist/core/command/lazy.d.ts +54 -0
- package/dist/core/command/registry-types.d.ts +96 -0
- package/dist/core/command/running.d.ts +96 -0
- package/dist/core/command/spends.d.ts +39 -0
- package/dist/core/command/types.d.ts +92 -0
- package/dist/core/command/version.d.ts +4 -0
- package/dist/core/config/branch-template.d.ts +20 -0
- package/dist/core/config/config-items.d.ts +86 -0
- package/dist/core/config/config-load.d.ts +47 -0
- package/dist/core/config/config-origins.d.ts +57 -0
- package/dist/core/config/config-readers.d.ts +167 -0
- package/dist/core/config/config-schema.d.ts +207 -0
- package/dist/core/config/config-sections.d.ts +493 -0
- package/dist/core/config/config.d.ts +320 -0
- package/dist/core/config/core-section.d.ts +39 -0
- package/dist/core/config/dangerous.d.ts +58 -0
- package/dist/core/config/default-routing.d.ts +16 -0
- package/dist/core/config/index.d.ts +141 -0
- package/dist/core/config/scope.d.ts +42 -0
- package/dist/core/config/section.d.ts +206 -0
- package/dist/core/config/sections.d.ts +45 -0
- package/dist/core/config/standard-schema.d.ts +27 -0
- package/dist/core/git/commit.d.ts +232 -0
- package/dist/core/git/gh-runner.d.ts +24 -0
- package/dist/core/git/git.d.ts +22 -0
- package/dist/core/git/index.d.ts +85 -0
- package/dist/core/loop/claude.d.ts +388 -0
- package/dist/core/loop/hop-record.d.ts +52 -0
- package/dist/core/loop/index.d.ts +179 -0
- package/dist/core/loop/session-env.d.ts +20 -0
- package/dist/core/loop/session-record-parse.d.ts +63 -0
- package/dist/core/loop/sessions.d.ts +408 -0
- package/dist/core/loop/suite-failure.d.ts +25 -0
- package/dist/core/output/active.d.ts +100 -0
- package/dist/core/output/stream.d.ts +20 -0
- package/dist/core/output/text.d.ts +55 -0
- package/dist/core/plan/block-reader.d.ts +46 -0
- package/dist/core/plan/blocks.d.ts +139 -0
- package/dist/core/plan/declaration.d.ts +366 -0
- package/dist/core/plan/plan-stamp.d.ts +75 -0
- package/dist/core/runtime/identity.d.ts +21 -0
- package/dist/core/runtime/index.d.ts +46 -0
- package/dist/effort/store/bring-forward.d.ts +131 -0
- package/dist/effort/store/check-rows.d.ts +31 -0
- package/dist/effort/store/commit-stats.d.ts +42 -0
- package/dist/effort/store/copy.d.ts +35 -0
- package/dist/effort/store/development-build.d.ts +105 -0
- package/dist/effort/store/effort-levels.d.ts +13 -0
- package/dist/effort/store/findings.d.ts +214 -0
- package/dist/effort/store/fix-schema.d.ts +184 -0
- package/dist/effort/store/index.js +10 -7
- package/dist/effort/store/location.d.ts +44 -0
- package/dist/effort/store/merge-commit-gaps.d.ts +68 -0
- package/dist/effort/store/merge-conflicts.d.ts +91 -0
- package/dist/effort/store/merge-rules.d.ts +82 -0
- package/dist/effort/store/merge-store.d.ts +211 -0
- package/dist/effort/store/merge-union.d.ts +92 -0
- package/dist/effort/store/migrate.d.ts +146 -0
- package/dist/effort/store/migrations.d.ts +92 -0
- package/dist/effort/store/ndjson-files.d.ts +114 -0
- package/dist/effort/store/origins.d.ts +116 -0
- package/dist/effort/store/plan-stubs.d.ts +79 -0
- package/dist/effort/store/rebuild-aside.d.ts +172 -0
- package/dist/effort/store/schema-plan.d.ts +102 -0
- package/dist/effort/store/session-row-types.d.ts +64 -0
- package/dist/effort/store/settings.d.ts +13 -0
- package/dist/effort/store/sqlite.d.ts +340 -0
- package/dist/effort/store/store-generation.d.ts +17 -0
- package/dist/effort/store/store-identity.d.ts +220 -0
- package/dist/effort/store/store-meta.d.ts +305 -0
- package/dist/effort/store/store-replaced.d.ts +72 -0
- package/dist/effort/store/types.d.ts +223 -0
- package/dist/errors/codes.d.ts +56 -0
- package/dist/errors/rafa-codes.d.ts +36 -0
- package/dist/index-2a7nb3an.js +125 -0
- package/dist/{index-r28fzsvc.js → index-39kbcw68.js} +350 -165
- package/dist/{index-nv34ptht.js → index-77ajfffn.js} +14 -9
- package/dist/index-cy98y87p.js +2597 -0
- package/dist/index-gqz4jn6j.js +13447 -0
- package/dist/index-gvek4r8m.js +710 -0
- package/dist/index-j4k2vdz3.js +1125 -0
- package/dist/index-jh1r4kcn.js +1067 -0
- package/dist/{index-mgh2s9bj.js → index-ykggr043.js} +58 -455
- package/dist/index-z1w83f81.js +4 -0
- package/dist/index.js +14715 -21516
- package/dist/learning/index.js +5 -6
- package/dist/plan/index.js +12 -11
- package/dist/plan-prompt.md +3 -1
- package/dist/ports/index.d.ts +732 -0
- package/dist/ports/learning.d.ts +101 -0
- package/dist/ports/log-level.d.ts +13 -0
- package/dist/ports/spec-review.d.ts +66 -0
- package/dist/pr/checks.d.ts +133 -0
- package/dist/pr/types.d.ts +422 -0
- package/dist/ready-4f8edezt.js +2016 -0
- package/dist/release-templates/release-bot-fragment.yml +153 -0
- package/dist/release-templates/release-publish.yml +110 -0
- package/dist/release-templates/release-settle.yml +122 -0
- package/dist/release-templates/release-tag.yml +102 -0
- package/dist/report/parse.d.ts +415 -0
- package/dist/schema/project-id.d.ts +79 -0
- package/dist/schema/tiers.d.ts +107 -0
- package/package.json +41 -3
- package/dist/index-hf2k497d.js +0 -16
- package/dist/index-nsr78ct2.js +0 -8219
|
@@ -0,0 +1,2597 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
activeOutput,
|
|
4
|
+
activeOutputMode,
|
|
5
|
+
isStampableStub
|
|
6
|
+
} from "./index-2a7nb3an.js";
|
|
7
|
+
import {
|
|
8
|
+
CORE_SECTION,
|
|
9
|
+
SCOPE_DIR,
|
|
10
|
+
scopeAt
|
|
11
|
+
} from "./index-gvek4r8m.js";
|
|
12
|
+
import {
|
|
13
|
+
BUSY_TIMEOUT_MAX_MS,
|
|
14
|
+
BUSY_TIMEOUT_MIN_MS,
|
|
15
|
+
TEST_SCOPES,
|
|
16
|
+
bundledBinDirectory,
|
|
17
|
+
describeValue,
|
|
18
|
+
isBusyTimeoutMs,
|
|
19
|
+
messageOf
|
|
20
|
+
} from "./index-ykggr043.js";
|
|
21
|
+
|
|
22
|
+
// packages/rafa/src/core/adapters/registry.ts
|
|
23
|
+
var REFUSAL = "adapter registry";
|
|
24
|
+
var PORT_VERSIONS = Object.freeze({
|
|
25
|
+
tracker: 1,
|
|
26
|
+
store: 1,
|
|
27
|
+
learning: 1,
|
|
28
|
+
output: 1,
|
|
29
|
+
planner: 1,
|
|
30
|
+
sync: 1,
|
|
31
|
+
logger: 1
|
|
32
|
+
});
|
|
33
|
+
var SERVED_VERSIONS = new Map(Object.entries(PORT_VERSIONS));
|
|
34
|
+
var PORT_TYPE_LIST = [...SERVED_VERSIONS.keys()].join(", ");
|
|
35
|
+
function checkPortType(port) {
|
|
36
|
+
if (!SERVED_VERSIONS.has(port)) {
|
|
37
|
+
throw new TypeError(`${REFUSAL}: port is ${describeValue(port)}, expected one of: ${PORT_TYPE_LIST}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function accept(adapter, held) {
|
|
41
|
+
if (typeof adapter !== "object" || adapter === null) {
|
|
42
|
+
throw new TypeError(`${REFUSAL}: an adapter is ${describeValue(adapter)}, expected a mapping`);
|
|
43
|
+
}
|
|
44
|
+
const { port, kind, portVersion, create } = adapter;
|
|
45
|
+
checkPortType(port);
|
|
46
|
+
if (typeof kind !== "string" || kind === "") {
|
|
47
|
+
throw new TypeError(`${REFUSAL}: a ${port} adapter has kind ${describeValue(kind)}, expected a non-empty name`);
|
|
48
|
+
}
|
|
49
|
+
const key = `${port}/${kind}`;
|
|
50
|
+
if (typeof create !== "function") {
|
|
51
|
+
throw new TypeError(`${REFUSAL}: ${key} has create ${describeValue(create)}, expected a function`);
|
|
52
|
+
}
|
|
53
|
+
const served = SERVED_VERSIONS.get(port);
|
|
54
|
+
if (portVersion !== served) {
|
|
55
|
+
throw new Error(`${REFUSAL}: ${key} implements ${port} port version ${describeValue(portVersion)},` + ` and core serves ${port} port version ${served}`);
|
|
56
|
+
}
|
|
57
|
+
if (held.some((other) => other.port === port && other.kind === kind)) {
|
|
58
|
+
throw new Error(`${REFUSAL}: ${key} is already registered`);
|
|
59
|
+
}
|
|
60
|
+
return Object.freeze({ port, kind, portVersion, create });
|
|
61
|
+
}
|
|
62
|
+
function indexByPort(adapters) {
|
|
63
|
+
const only = (port) => new Map(adapters.filter((adapter) => adapter.port === port).map((adapter) => [adapter.kind, adapter]));
|
|
64
|
+
return {
|
|
65
|
+
tracker: only("tracker"),
|
|
66
|
+
store: only("store"),
|
|
67
|
+
learning: only("learning"),
|
|
68
|
+
output: only("output"),
|
|
69
|
+
planner: only("planner"),
|
|
70
|
+
sync: only("sync"),
|
|
71
|
+
logger: only("logger")
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function createAdapterRegistry(adapters = []) {
|
|
75
|
+
const held = adapters.reduce((accepted, adapter) => [...accepted, accept(adapter, accepted)], []);
|
|
76
|
+
const byPort = indexByPort(held);
|
|
77
|
+
const registry = {
|
|
78
|
+
register: (adapter) => createAdapterRegistry([...held, adapter]),
|
|
79
|
+
find: (port, kind) => SERVED_VERSIONS.has(port) ? byPort[port].get(kind) : undefined,
|
|
80
|
+
resolve: (port, kind) => {
|
|
81
|
+
checkPortType(port);
|
|
82
|
+
const adapter = byPort[port].get(kind);
|
|
83
|
+
if (adapter !== undefined)
|
|
84
|
+
return adapter;
|
|
85
|
+
const registered = [...byPort[port].keys()];
|
|
86
|
+
const listed = registered.length === 0 ? "none" : registered.join(", ");
|
|
87
|
+
throw new Error(`${REFUSAL}: no ${port} adapter is registered as ${describeValue(kind)};` + ` registered: ${listed}`);
|
|
88
|
+
},
|
|
89
|
+
kinds: (port) => {
|
|
90
|
+
checkPortType(port);
|
|
91
|
+
return [...byPort[port].keys()];
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
return Object.freeze(registry);
|
|
95
|
+
}
|
|
96
|
+
var EMPTY_ADAPTER_REGISTRY = createAdapterRegistry();
|
|
97
|
+
var COMPOSITION_ROOT = "src/adapter-registry.ts";
|
|
98
|
+
var active = EMPTY_ADAPTER_REGISTRY;
|
|
99
|
+
function holdsNone(registry) {
|
|
100
|
+
return [...SERVED_VERSIONS.keys()].every((port) => registry.kinds(port).length === 0);
|
|
101
|
+
}
|
|
102
|
+
function registerAdapters(adapters) {
|
|
103
|
+
const previous = active;
|
|
104
|
+
active = adapters.reduce((registry, adapter) => registry.register(adapter), previous);
|
|
105
|
+
return previous;
|
|
106
|
+
}
|
|
107
|
+
function restoreAdapters(registry) {
|
|
108
|
+
if (registry === CORE_ADAPTER_REGISTRY) {
|
|
109
|
+
throw new TypeError(`${REFUSAL}: CORE_ADAPTER_REGISTRY reads the active registry and cannot be made the active one;` + " restore what registerAdapters or activeAdapterRegistry answered");
|
|
110
|
+
}
|
|
111
|
+
active = registry;
|
|
112
|
+
}
|
|
113
|
+
function activeAdapterRegistry() {
|
|
114
|
+
return active;
|
|
115
|
+
}
|
|
116
|
+
function registered() {
|
|
117
|
+
if (holdsNone(active)) {
|
|
118
|
+
throw new Error(`${REFUSAL}: no adapter is registered: call registerCoreAdapters() from ${COMPOSITION_ROOT}` + " before reading CORE_ADAPTER_REGISTRY");
|
|
119
|
+
}
|
|
120
|
+
return active;
|
|
121
|
+
}
|
|
122
|
+
var CORE_ADAPTER_REGISTRY = Object.freeze({
|
|
123
|
+
register: (adapter) => registered().register(adapter),
|
|
124
|
+
find: (port, kind) => registered().find(port, kind),
|
|
125
|
+
resolve: (port, kind) => registered().resolve(port, kind),
|
|
126
|
+
kinds: (port) => registered().kinds(port)
|
|
127
|
+
});
|
|
128
|
+
// packages/rafa/src/effort/store/types.ts
|
|
129
|
+
function keyOrNull(value) {
|
|
130
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
131
|
+
}
|
|
132
|
+
var EFFORT_KEY_PROJECTIONS = {
|
|
133
|
+
sessions: (row) => keyOrNull(row.sessionId),
|
|
134
|
+
commits: (row) => keyOrNull(row.sha)
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// packages/rafa/src/core/command/spends.ts
|
|
138
|
+
var SPENDS_GLYPH = "\uD83E\uDE99";
|
|
139
|
+
|
|
140
|
+
// packages/rafa/src/core/command/command.ts
|
|
141
|
+
var MAX_EXIT_CODE = 255;
|
|
142
|
+
|
|
143
|
+
class CommandExit extends Error {
|
|
144
|
+
exitCode;
|
|
145
|
+
constructor(exitCode, message = "", options) {
|
|
146
|
+
super(message, options);
|
|
147
|
+
if (!Number.isInteger(exitCode) || exitCode < 0 || exitCode > MAX_EXIT_CODE) {
|
|
148
|
+
throw new TypeError(`CommandExit: exit code is ${describeValue(exitCode)}, expected a whole number from 0 to ${MAX_EXIT_CODE}`);
|
|
149
|
+
}
|
|
150
|
+
this.name = "CommandExit";
|
|
151
|
+
this.exitCode = exitCode;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function isTopLevel(command) {
|
|
155
|
+
return command.subject === command.action;
|
|
156
|
+
}
|
|
157
|
+
function commandSpelling(command) {
|
|
158
|
+
return isTopLevel(command) ? command.subject : `${command.subject} ${command.action}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// packages/rafa/src/core/command/confirm.ts
|
|
162
|
+
import { createInterface } from "readline";
|
|
163
|
+
var YES_ANSWERS = Object.freeze(["y", "yes"]);
|
|
164
|
+
function createLinePrompter(input, output) {
|
|
165
|
+
const buffered = [];
|
|
166
|
+
const waiting = [];
|
|
167
|
+
let ended = false;
|
|
168
|
+
const lines = createInterface({ input, terminal: false, crlfDelay: Infinity });
|
|
169
|
+
lines.on("line", (line) => {
|
|
170
|
+
const reader = waiting.shift();
|
|
171
|
+
if (reader === undefined)
|
|
172
|
+
buffered.push(line);
|
|
173
|
+
else
|
|
174
|
+
reader(line);
|
|
175
|
+
});
|
|
176
|
+
lines.on("close", () => {
|
|
177
|
+
ended = true;
|
|
178
|
+
for (const reader of waiting.splice(0))
|
|
179
|
+
reader(null);
|
|
180
|
+
});
|
|
181
|
+
return {
|
|
182
|
+
say: (text) => {
|
|
183
|
+
output.write(`${text}
|
|
184
|
+
`);
|
|
185
|
+
},
|
|
186
|
+
ask: async (question) => {
|
|
187
|
+
output.write(question);
|
|
188
|
+
const next = buffered.shift();
|
|
189
|
+
if (next !== undefined)
|
|
190
|
+
return next;
|
|
191
|
+
if (ended)
|
|
192
|
+
return null;
|
|
193
|
+
return new Promise((settle) => {
|
|
194
|
+
waiting.push(settle);
|
|
195
|
+
});
|
|
196
|
+
},
|
|
197
|
+
close: () => {
|
|
198
|
+
lines.close();
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function lazyPrompter(open) {
|
|
203
|
+
let prompter = null;
|
|
204
|
+
return {
|
|
205
|
+
ask: async (question) => {
|
|
206
|
+
prompter ??= open();
|
|
207
|
+
const answer = await prompter.ask(question);
|
|
208
|
+
return answer !== null && YES_ANSWERS.includes(answer.trim().toLowerCase());
|
|
209
|
+
},
|
|
210
|
+
close: () => {
|
|
211
|
+
prompter?.close();
|
|
212
|
+
prompter = null;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
// packages/rafa/src/core/command/running.ts
|
|
217
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
218
|
+
var scopes = new AsyncLocalStorage;
|
|
219
|
+
var running = null;
|
|
220
|
+
function runningCommand() {
|
|
221
|
+
return scopes.getStore() ?? running;
|
|
222
|
+
}
|
|
223
|
+
// package.json
|
|
224
|
+
var version = "0.46.0";
|
|
225
|
+
|
|
226
|
+
// packages/rafa/src/core/command/version.ts
|
|
227
|
+
var RAFA_VERSION = version;
|
|
228
|
+
// packages/rafa/src/core/git/git.ts
|
|
229
|
+
import { spawnSync } from "child_process";
|
|
230
|
+
function createGitRunner(cwd) {
|
|
231
|
+
return (args) => {
|
|
232
|
+
const result = spawnSync("git", [...args], {
|
|
233
|
+
cwd,
|
|
234
|
+
encoding: "utf8",
|
|
235
|
+
env: { ...process.env, LC_ALL: "C" }
|
|
236
|
+
});
|
|
237
|
+
if (result.error !== undefined) {
|
|
238
|
+
return { ok: false, stdout: "", stderr: `could not run git in ${cwd}: ${messageOf(result.error)}` };
|
|
239
|
+
}
|
|
240
|
+
return { ok: result.status === 0, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function gitSaid(result) {
|
|
244
|
+
return [result.stderr, result.stdout].map((stream) => stream.trim()).filter((stream) => stream !== "").join(`
|
|
245
|
+
`);
|
|
246
|
+
}
|
|
247
|
+
// packages/rafa/src/core/git/commit.ts
|
|
248
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
249
|
+
var DEFAULT_COMMIT_TYPE = "chore";
|
|
250
|
+
var TEST_NOUN_WINDOW = 40;
|
|
251
|
+
var FIX_VERBS = ["fix", "repair", "correct", "resolve"];
|
|
252
|
+
var REFACTOR_VERBS = [
|
|
253
|
+
"remove",
|
|
254
|
+
"delete",
|
|
255
|
+
"drop",
|
|
256
|
+
"move",
|
|
257
|
+
"rename",
|
|
258
|
+
"relocate",
|
|
259
|
+
"extract",
|
|
260
|
+
"split",
|
|
261
|
+
"inline",
|
|
262
|
+
"collapse"
|
|
263
|
+
];
|
|
264
|
+
var FEAT_VERBS = [
|
|
265
|
+
"add",
|
|
266
|
+
"implement",
|
|
267
|
+
"introduce",
|
|
268
|
+
"register",
|
|
269
|
+
"extend",
|
|
270
|
+
"call",
|
|
271
|
+
"wire",
|
|
272
|
+
"expose",
|
|
273
|
+
"parse",
|
|
274
|
+
"strip",
|
|
275
|
+
"enforce",
|
|
276
|
+
"generate"
|
|
277
|
+
];
|
|
278
|
+
var CHORE_VERBS = [
|
|
279
|
+
"capture",
|
|
280
|
+
"measure",
|
|
281
|
+
"run",
|
|
282
|
+
"sweep",
|
|
283
|
+
"verify",
|
|
284
|
+
"ensure",
|
|
285
|
+
"regenerate",
|
|
286
|
+
"update",
|
|
287
|
+
"gitignore",
|
|
288
|
+
"take",
|
|
289
|
+
"bump",
|
|
290
|
+
"pin"
|
|
291
|
+
];
|
|
292
|
+
function openingVerbs(verbs) {
|
|
293
|
+
return new RegExp(`^(?:${verbs.join("|")})\\b`, "i");
|
|
294
|
+
}
|
|
295
|
+
var COMMIT_TYPE_RULES = [
|
|
296
|
+
{
|
|
297
|
+
type: "test",
|
|
298
|
+
pattern: new RegExp(`^.{0,${TEST_NOUN_WINDOW}}?\\btests?\\b`, "i")
|
|
299
|
+
},
|
|
300
|
+
{ type: "docs", pattern: /\.md\b/i },
|
|
301
|
+
{ type: "fix", pattern: openingVerbs(FIX_VERBS) },
|
|
302
|
+
{ type: "refactor", pattern: openingVerbs(REFACTOR_VERBS) },
|
|
303
|
+
{ type: "feat", pattern: openingVerbs(FEAT_VERBS) },
|
|
304
|
+
{ type: "chore", pattern: openingVerbs(CHORE_VERBS) }
|
|
305
|
+
];
|
|
306
|
+
var MAX_SUBJECT_LENGTH = 72;
|
|
307
|
+
var FALLBACK_DESCRIPTION = "complete the scoped task";
|
|
308
|
+
var MAX_MESSAGE_CHARS = 4000;
|
|
309
|
+
function normaliseTaskText(taskText) {
|
|
310
|
+
return taskText.replace(/\s+/g, " ").trim();
|
|
311
|
+
}
|
|
312
|
+
function inferCommitType(taskText) {
|
|
313
|
+
const text = normaliseTaskText(taskText);
|
|
314
|
+
for (const rule of COMMIT_TYPE_RULES) {
|
|
315
|
+
if (rule.pattern.test(text))
|
|
316
|
+
return rule.type;
|
|
317
|
+
}
|
|
318
|
+
return DEFAULT_COMMIT_TYPE;
|
|
319
|
+
}
|
|
320
|
+
function countBackticks(text) {
|
|
321
|
+
let count = 0;
|
|
322
|
+
for (const character of text) {
|
|
323
|
+
if (character === "`")
|
|
324
|
+
count += 1;
|
|
325
|
+
}
|
|
326
|
+
return count;
|
|
327
|
+
}
|
|
328
|
+
function balanceCodeSpans(text) {
|
|
329
|
+
let result = text;
|
|
330
|
+
while (countBackticks(result) % 2 === 1) {
|
|
331
|
+
const lastSpace = result.lastIndexOf(" ");
|
|
332
|
+
if (lastSpace <= 0)
|
|
333
|
+
return result.replace(/`/g, "");
|
|
334
|
+
result = result.slice(0, lastSpace).trimEnd();
|
|
335
|
+
}
|
|
336
|
+
return result;
|
|
337
|
+
}
|
|
338
|
+
function stripTrailingPeriod(text) {
|
|
339
|
+
if (!text.endsWith("."))
|
|
340
|
+
return text;
|
|
341
|
+
const withoutDot = text.slice(0, -1);
|
|
342
|
+
const lastToken = withoutDot.slice(withoutDot.lastIndexOf(" ") + 1);
|
|
343
|
+
if (lastToken.includes("."))
|
|
344
|
+
return text;
|
|
345
|
+
return withoutDot;
|
|
346
|
+
}
|
|
347
|
+
function lowerFirstWord(text) {
|
|
348
|
+
const match = text.match(/^[A-Za-z]+/);
|
|
349
|
+
const word = match?.[0];
|
|
350
|
+
if (word === undefined)
|
|
351
|
+
return text;
|
|
352
|
+
if (word.length > 1 && word === word.toUpperCase())
|
|
353
|
+
return text;
|
|
354
|
+
return text.slice(0, 1).toLowerCase() + text.slice(1);
|
|
355
|
+
}
|
|
356
|
+
var CLAUSE_ENDINGS = new Set([",", ":", "(", ")"]);
|
|
357
|
+
function backtickRunLength(text, start) {
|
|
358
|
+
let end = start;
|
|
359
|
+
while (text.charAt(end) === "`")
|
|
360
|
+
end += 1;
|
|
361
|
+
return end - start;
|
|
362
|
+
}
|
|
363
|
+
function codeSpanEnd(text, from, length) {
|
|
364
|
+
let index = text.indexOf("`", from);
|
|
365
|
+
while (index !== -1) {
|
|
366
|
+
const run = backtickRunLength(text, index);
|
|
367
|
+
if (run === length)
|
|
368
|
+
return index + run;
|
|
369
|
+
index = text.indexOf("`", index + run);
|
|
370
|
+
}
|
|
371
|
+
return -1;
|
|
372
|
+
}
|
|
373
|
+
function firstClause(text) {
|
|
374
|
+
let index = 0;
|
|
375
|
+
while (index < text.length) {
|
|
376
|
+
const character = text.charAt(index);
|
|
377
|
+
if (character === "`") {
|
|
378
|
+
const run = backtickRunLength(text, index);
|
|
379
|
+
const end = codeSpanEnd(text, index + run, run);
|
|
380
|
+
index = end === -1 ? index + run : end;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (CLAUSE_ENDINGS.has(character))
|
|
384
|
+
return text.slice(0, index).trimEnd();
|
|
385
|
+
index += 1;
|
|
386
|
+
}
|
|
387
|
+
return text.trimEnd();
|
|
388
|
+
}
|
|
389
|
+
function truncateDescription(description, budget) {
|
|
390
|
+
if (budget <= 0)
|
|
391
|
+
return "";
|
|
392
|
+
if (description.length <= budget)
|
|
393
|
+
return description;
|
|
394
|
+
const window = description.slice(0, budget + 1);
|
|
395
|
+
const lastSpace = window.lastIndexOf(" ");
|
|
396
|
+
const cut = lastSpace > 0 ? window.slice(0, lastSpace) : window.slice(0, budget);
|
|
397
|
+
return balanceCodeSpans(cut.trimEnd()).replace(/[\s,;:-]+$/, "");
|
|
398
|
+
}
|
|
399
|
+
function subjectDescription(normalised) {
|
|
400
|
+
const clause = firstClause(normalised);
|
|
401
|
+
const description = clause.length === 0 ? normalised : clause;
|
|
402
|
+
return lowerFirstWord(stripTrailingPeriod(description));
|
|
403
|
+
}
|
|
404
|
+
function deriveCommitSubject(taskText, options = {}) {
|
|
405
|
+
const type = inferCommitType(taskText);
|
|
406
|
+
const scope = options.scope;
|
|
407
|
+
const prefix = scope === undefined || scope.length === 0 ? `${type}: ` : `${type}(${scope}): `;
|
|
408
|
+
const normalised = normaliseTaskText(taskText);
|
|
409
|
+
const uncut = normalised.length === 0 ? FALLBACK_DESCRIPTION : subjectDescription(normalised);
|
|
410
|
+
const maxLength = options.maxLength ?? MAX_SUBJECT_LENGTH;
|
|
411
|
+
const description = truncateDescription(uncut, maxLength - prefix.length);
|
|
412
|
+
const truncated = description !== uncut;
|
|
413
|
+
return {
|
|
414
|
+
subject: `${prefix}${description}`,
|
|
415
|
+
type,
|
|
416
|
+
truncated
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function buildCommitMessage(taskText, options = {}) {
|
|
420
|
+
const derived = deriveCommitSubject(taskText, options);
|
|
421
|
+
const body = normaliseTaskText(taskText);
|
|
422
|
+
return { ...derived, body };
|
|
423
|
+
}
|
|
424
|
+
function stageAllArgs() {
|
|
425
|
+
return ["add", "-A"];
|
|
426
|
+
}
|
|
427
|
+
function stagedChangesArgs() {
|
|
428
|
+
return ["diff", "--cached", "--quiet"];
|
|
429
|
+
}
|
|
430
|
+
function commitArgs(subject, body = "") {
|
|
431
|
+
const args = ["commit", "--cleanup=whitespace", "-m", subject];
|
|
432
|
+
if (body.length > 0)
|
|
433
|
+
args.push("-m", body);
|
|
434
|
+
return args;
|
|
435
|
+
}
|
|
436
|
+
function headShaArgs() {
|
|
437
|
+
return ["rev-parse", "HEAD"];
|
|
438
|
+
}
|
|
439
|
+
var MAX_GIT_BUFFER = 16 * 1024 * 1024;
|
|
440
|
+
function failureMessage(result) {
|
|
441
|
+
const combined = [result.stderr, result.stdout].map((stream) => stream.trim()).filter((stream) => stream.length > 0).join(`
|
|
442
|
+
`);
|
|
443
|
+
if (combined.length <= MAX_MESSAGE_CHARS)
|
|
444
|
+
return combined;
|
|
445
|
+
return `${combined.slice(0, MAX_MESSAGE_CHARS)}
|
|
446
|
+
[truncated]`;
|
|
447
|
+
}
|
|
448
|
+
function failed(step, subject, result) {
|
|
449
|
+
return {
|
|
450
|
+
outcome: "failed",
|
|
451
|
+
subject,
|
|
452
|
+
sha: null,
|
|
453
|
+
failedStep: step,
|
|
454
|
+
exitCode: result.status,
|
|
455
|
+
message: failureMessage(result)
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function spawnGit(cwd) {
|
|
459
|
+
return (args) => {
|
|
460
|
+
const result = spawnSync2("git", args, {
|
|
461
|
+
cwd,
|
|
462
|
+
encoding: "utf8",
|
|
463
|
+
maxBuffer: MAX_GIT_BUFFER
|
|
464
|
+
});
|
|
465
|
+
if (result.error !== undefined) {
|
|
466
|
+
return { status: null, stdout: "", stderr: result.error.message };
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
status: result.status,
|
|
470
|
+
stdout: result.stdout ?? "",
|
|
471
|
+
stderr: result.stderr ?? ""
|
|
472
|
+
};
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
function commitTaskWork(options) {
|
|
476
|
+
const run = options.runGit ?? spawnGit(options.cwd ?? process.cwd());
|
|
477
|
+
const message = buildCommitMessage(options.taskText, {
|
|
478
|
+
scope: options.scope,
|
|
479
|
+
maxLength: options.maxLength
|
|
480
|
+
});
|
|
481
|
+
const { subject } = message;
|
|
482
|
+
const staged = run(stageAllArgs());
|
|
483
|
+
if (staged.status !== 0)
|
|
484
|
+
return failed("stage", subject, staged);
|
|
485
|
+
const pending = run(stagedChangesArgs());
|
|
486
|
+
if (pending.status === 0) {
|
|
487
|
+
return {
|
|
488
|
+
outcome: "nothing-to-commit",
|
|
489
|
+
subject,
|
|
490
|
+
sha: null,
|
|
491
|
+
failedStep: null,
|
|
492
|
+
exitCode: 0,
|
|
493
|
+
message: ""
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
if (pending.status !== 1)
|
|
497
|
+
return failed("inspect", subject, pending);
|
|
498
|
+
const committed = run(commitArgs(subject, message.body));
|
|
499
|
+
if (committed.status !== 0)
|
|
500
|
+
return failed("commit", subject, committed);
|
|
501
|
+
const head = run(headShaArgs());
|
|
502
|
+
const sha = head.status === 0 ? head.stdout.trim() : null;
|
|
503
|
+
return {
|
|
504
|
+
outcome: "committed",
|
|
505
|
+
subject,
|
|
506
|
+
sha,
|
|
507
|
+
failedStep: null,
|
|
508
|
+
exitCode: 0,
|
|
509
|
+
message: ""
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
// packages/rafa/src/effort/store/sqlite.ts
|
|
513
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
|
|
514
|
+
import { dirname as dirname7, join as join5 } from "path";
|
|
515
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
516
|
+
|
|
517
|
+
// packages/rafa/src/effort/store/development-build.ts
|
|
518
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
519
|
+
import { basename as basename3, dirname as dirname5, isAbsolute as isAbsolute3 } from "path";
|
|
520
|
+
|
|
521
|
+
// packages/rafa/src/core/loop/sessions.ts
|
|
522
|
+
import {
|
|
523
|
+
linkSync,
|
|
524
|
+
mkdirSync,
|
|
525
|
+
readdirSync,
|
|
526
|
+
readFileSync,
|
|
527
|
+
renameSync,
|
|
528
|
+
rmSync,
|
|
529
|
+
writeFileSync
|
|
530
|
+
} from "fs";
|
|
531
|
+
import { dirname, join } from "path";
|
|
532
|
+
|
|
533
|
+
// packages/rafa/src/core/loop/session-record-parse.ts
|
|
534
|
+
import { basename, isAbsolute } from "path";
|
|
535
|
+
|
|
536
|
+
// packages/rafa/src/core/loop/hop-record.ts
|
|
537
|
+
var HOP_KINDS = ["blocker", "dry"];
|
|
538
|
+
var HOP_STATES = ["away", "waiting", "merged", "halted"];
|
|
539
|
+
function isRecord(value) {
|
|
540
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
541
|
+
}
|
|
542
|
+
function isIssueNumber(value) {
|
|
543
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
544
|
+
}
|
|
545
|
+
function asPlace(value) {
|
|
546
|
+
if (!isRecord(value) || !isIssueNumber(value.board))
|
|
547
|
+
return null;
|
|
548
|
+
if (value.epic !== null && !isIssueNumber(value.epic))
|
|
549
|
+
return null;
|
|
550
|
+
return { board: value.board, epic: value.epic };
|
|
551
|
+
}
|
|
552
|
+
function isIssueOrNull(value) {
|
|
553
|
+
return value === null || isIssueNumber(value);
|
|
554
|
+
}
|
|
555
|
+
function isTimestamp(value) {
|
|
556
|
+
return typeof value === "string" && !Number.isNaN(Date.parse(value));
|
|
557
|
+
}
|
|
558
|
+
function issuesMatchKind(kind, blocked, target) {
|
|
559
|
+
return kind === "blocker" ? blocked !== null && target !== null : blocked === null && target === null;
|
|
560
|
+
}
|
|
561
|
+
function asHopRecord(value) {
|
|
562
|
+
if (!isRecord(value))
|
|
563
|
+
return null;
|
|
564
|
+
const { kind, blocked, target, targetEpic, targetBoard, state, pullRequest, startedAt } = value;
|
|
565
|
+
const home = asPlace(value.home);
|
|
566
|
+
const from = asPlace(value.from);
|
|
567
|
+
if (typeof kind !== "string" || !HOP_KINDS.includes(kind))
|
|
568
|
+
return null;
|
|
569
|
+
if (typeof state !== "string" || !HOP_STATES.includes(state))
|
|
570
|
+
return null;
|
|
571
|
+
if (home === null || from === null)
|
|
572
|
+
return null;
|
|
573
|
+
if (!isIssueOrNull(blocked) || !isIssueOrNull(target) || !isIssueOrNull(pullRequest))
|
|
574
|
+
return null;
|
|
575
|
+
if (!isIssueNumber(targetEpic) || !isIssueNumber(targetBoard) || !isTimestamp(startedAt))
|
|
576
|
+
return null;
|
|
577
|
+
const hopKind = kind;
|
|
578
|
+
if (!issuesMatchKind(hopKind, blocked, target))
|
|
579
|
+
return null;
|
|
580
|
+
return {
|
|
581
|
+
kind: hopKind,
|
|
582
|
+
home,
|
|
583
|
+
from,
|
|
584
|
+
blocked,
|
|
585
|
+
target,
|
|
586
|
+
targetEpic,
|
|
587
|
+
targetBoard,
|
|
588
|
+
state,
|
|
589
|
+
pullRequest,
|
|
590
|
+
startedAt
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// packages/rafa/src/core/loop/session-record-parse.ts
|
|
595
|
+
var SESSION_STATES = Object.freeze(["running", "paused", "stopped", "done"]);
|
|
596
|
+
var SESSION_PHASES = Object.freeze(["task", "wrap-up", "pull-request", "ci", "repair"]);
|
|
597
|
+
var RECORD_EXTENSION = ".json";
|
|
598
|
+
var SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
599
|
+
var SESSION_STEP_KINDS = Object.freeze(["baseline", "task", "stage", "pre-wrap-up"]);
|
|
600
|
+
var SESSION_STEP_REASONS = Object.freeze(["declared", "trigger", "fallback", "no-module-tests", "stage"]);
|
|
601
|
+
var SESSION_DECISION_STRATEGIES = Object.freeze(["jump", "defer"]);
|
|
602
|
+
|
|
603
|
+
class SessionRecordError extends Error {
|
|
604
|
+
file;
|
|
605
|
+
constructor(file, problem) {
|
|
606
|
+
super(`session record ${file}: ${problem}`);
|
|
607
|
+
this.name = "SessionRecordError";
|
|
608
|
+
this.file = file;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
function isSessionId(value) {
|
|
612
|
+
return SESSION_ID_PATTERN.test(value);
|
|
613
|
+
}
|
|
614
|
+
function describeValue2(value) {
|
|
615
|
+
return value === undefined ? "missing" : JSON.stringify(value) ?? String(value);
|
|
616
|
+
}
|
|
617
|
+
function field(fields, key) {
|
|
618
|
+
return Object.hasOwn(fields, key) ? fields[key] : undefined;
|
|
619
|
+
}
|
|
620
|
+
function isObject(value) {
|
|
621
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
622
|
+
}
|
|
623
|
+
function isPositiveWhole(value) {
|
|
624
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
625
|
+
}
|
|
626
|
+
function isText(value) {
|
|
627
|
+
return typeof value === "string" && value.trim() !== "";
|
|
628
|
+
}
|
|
629
|
+
function taskProblem(task) {
|
|
630
|
+
if (task === null)
|
|
631
|
+
return null;
|
|
632
|
+
if (!isObject(task))
|
|
633
|
+
return `task is ${describeValue2(task)}, expected null or an object`;
|
|
634
|
+
if (!isPositiveWhole(field(task, "line"))) {
|
|
635
|
+
return `task.line is ${describeValue2(field(task, "line"))}, expected a whole number from 1`;
|
|
636
|
+
}
|
|
637
|
+
return isText(field(task, "text")) ? null : `task.text is ${describeValue2(field(task, "text"))}, expected a non-empty string`;
|
|
638
|
+
}
|
|
639
|
+
function hopProblem(fields) {
|
|
640
|
+
if (!Object.hasOwn(fields, "hop"))
|
|
641
|
+
return null;
|
|
642
|
+
const hop = field(fields, "hop");
|
|
643
|
+
return asHopRecord(hop) === null ? `hop is ${describeValue2(hop)}, expected a hop record` : null;
|
|
644
|
+
}
|
|
645
|
+
function worktreeProblem(fields) {
|
|
646
|
+
if (!Object.hasOwn(fields, "worktree"))
|
|
647
|
+
return null;
|
|
648
|
+
const worktree = field(fields, "worktree");
|
|
649
|
+
return isText(worktree) && isAbsolute(worktree) ? null : `worktree is ${describeValue2(worktree)}, expected an absolute path`;
|
|
650
|
+
}
|
|
651
|
+
function isTextList(value) {
|
|
652
|
+
return Array.isArray(value) && value.every(isText);
|
|
653
|
+
}
|
|
654
|
+
function isFailure(value) {
|
|
655
|
+
return isObject(value) && isText(field(value, "file")) && isText(field(value, "name"));
|
|
656
|
+
}
|
|
657
|
+
function isFailureList(value) {
|
|
658
|
+
return Array.isArray(value) && value.every(isFailure);
|
|
659
|
+
}
|
|
660
|
+
function failureKey(failure) {
|
|
661
|
+
return JSON.stringify([failure.file, failure.name]);
|
|
662
|
+
}
|
|
663
|
+
function stepFailuresProblem(step, at) {
|
|
664
|
+
const failures = field(step, "failures");
|
|
665
|
+
const newFailures = field(step, "newFailures");
|
|
666
|
+
if (!isFailureList(failures))
|
|
667
|
+
return `${at}.failures is ${describeValue2(failures)}, expected a list of failing tests`;
|
|
668
|
+
if (!isFailureList(newFailures)) {
|
|
669
|
+
return `${at}.newFailures is ${describeValue2(newFailures)}, expected a list of failing tests`;
|
|
670
|
+
}
|
|
671
|
+
const known = new Set(failures.map(failureKey));
|
|
672
|
+
const stray = newFailures.find((failure) => !known.has(failureKey(failure)));
|
|
673
|
+
return stray === undefined ? null : `${at}.newFailures names ${describeValue2(stray)}, which ${at}.failures does not`;
|
|
674
|
+
}
|
|
675
|
+
function stepInterruptedProblem(step, at) {
|
|
676
|
+
if (!Object.hasOwn(step, "interrupted"))
|
|
677
|
+
return null;
|
|
678
|
+
const interrupted = field(step, "interrupted");
|
|
679
|
+
return interrupted === true ? null : `${at}.interrupted is ${describeValue2(interrupted)}, expected true or no key`;
|
|
680
|
+
}
|
|
681
|
+
function stepProblems(step, at) {
|
|
682
|
+
if (!isObject(step))
|
|
683
|
+
return [`${at} is ${describeValue2(step)}, expected a step`];
|
|
684
|
+
const kind = field(step, "kind");
|
|
685
|
+
const scope = field(step, "scope");
|
|
686
|
+
const command = field(step, "command");
|
|
687
|
+
const exitCode = field(step, "exitCode");
|
|
688
|
+
const summary = field(step, "summary");
|
|
689
|
+
const problems = [
|
|
690
|
+
SESSION_STEP_KINDS.includes(kind) ? null : `${at}.kind is ${describeValue2(kind)}, expected one of ${SESSION_STEP_KINDS.join(", ")}`,
|
|
691
|
+
TEST_SCOPES.includes(scope) || isTextList(scope) ? null : `${at}.scope is ${describeValue2(scope)}, expected one of ${TEST_SCOPES.join(", ")} or a list of paths`,
|
|
692
|
+
isTextList(command) && command.length > 0 ? null : `${at}.command is ${describeValue2(command)}, expected a non-empty list of arguments`,
|
|
693
|
+
typeof exitCode === "number" && Number.isSafeInteger(exitCode) ? null : `${at}.exitCode is ${describeValue2(exitCode)}, expected a whole number`,
|
|
694
|
+
summary === null || typeof summary === "string" ? null : `${at}.summary is ${describeValue2(summary)}, expected null or a string`,
|
|
695
|
+
stepFailuresProblem(step, at),
|
|
696
|
+
stepInterruptedProblem(step, at)
|
|
697
|
+
];
|
|
698
|
+
return problems.filter((problem) => problem !== null);
|
|
699
|
+
}
|
|
700
|
+
function isLineIndex(value) {
|
|
701
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
702
|
+
}
|
|
703
|
+
function taskRefProblems(ref, at) {
|
|
704
|
+
if (!isObject(ref))
|
|
705
|
+
return [`${at} is ${describeValue2(ref)}, expected a task`];
|
|
706
|
+
const lineNum = field(ref, "lineNum");
|
|
707
|
+
const task = field(ref, "task");
|
|
708
|
+
const ordinal = field(ref, "ordinal");
|
|
709
|
+
return [
|
|
710
|
+
isLineIndex(lineNum) ? null : `${at}.lineNum is ${describeValue2(lineNum)}, expected a whole number from 0`,
|
|
711
|
+
isText(task) ? null : `${at}.task is ${describeValue2(task)}, expected a non-empty string`,
|
|
712
|
+
ordinal === undefined || isPositiveWhole(ordinal) ? null : `${at}.ordinal is ${describeValue2(ordinal)}, expected a whole number from 1`
|
|
713
|
+
].filter((problem) => problem !== null);
|
|
714
|
+
}
|
|
715
|
+
function afterProblems(decision, strategy, at) {
|
|
716
|
+
const after = field(decision, "after");
|
|
717
|
+
if (strategy === "defer") {
|
|
718
|
+
return after === undefined ? [`${at}.after is missing, expected the task a defer waits on`] : taskRefProblems(after, `${at}.after`);
|
|
719
|
+
}
|
|
720
|
+
return after === undefined ? [] : [`${at}.after is ${describeValue2(after)}, expected no key on a jump`];
|
|
721
|
+
}
|
|
722
|
+
function decisionProblems(decision, at) {
|
|
723
|
+
if (!isObject(decision))
|
|
724
|
+
return [`${at} is ${describeValue2(decision)}, expected a decision`];
|
|
725
|
+
const strategy = field(decision, "strategy");
|
|
726
|
+
const reason = field(decision, "reason");
|
|
727
|
+
const problems = [
|
|
728
|
+
...taskRefProblems(field(decision, "task"), `${at}.task`),
|
|
729
|
+
SESSION_DECISION_STRATEGIES.includes(strategy) ? null : `${at}.strategy is ${describeValue2(strategy)}, expected one of ${SESSION_DECISION_STRATEGIES.join(", ")}`,
|
|
730
|
+
isText(reason) ? null : `${at}.reason is ${describeValue2(reason)}, expected a non-empty string`,
|
|
731
|
+
...afterProblems(decision, strategy, at)
|
|
732
|
+
];
|
|
733
|
+
return problems.filter((problem) => problem !== null);
|
|
734
|
+
}
|
|
735
|
+
function decisionsProblems(fields) {
|
|
736
|
+
if (!Object.hasOwn(fields, "decisions"))
|
|
737
|
+
return [];
|
|
738
|
+
const decisions = field(fields, "decisions");
|
|
739
|
+
if (!Array.isArray(decisions))
|
|
740
|
+
return [`decisions is ${describeValue2(decisions)}, expected a list of decisions`];
|
|
741
|
+
return decisions.flatMap((decision, index) => decisionProblems(decision, `decisions[${index}]`));
|
|
742
|
+
}
|
|
743
|
+
function stepsProblems(fields) {
|
|
744
|
+
if (!Object.hasOwn(fields, "steps"))
|
|
745
|
+
return [];
|
|
746
|
+
const steps = field(fields, "steps");
|
|
747
|
+
if (!Array.isArray(steps))
|
|
748
|
+
return [`steps is ${describeValue2(steps)}, expected a list of steps`];
|
|
749
|
+
return steps.flatMap((step, index) => stepProblems(step, `steps[${index}]`));
|
|
750
|
+
}
|
|
751
|
+
function sessionIdProblem(sessionId, file) {
|
|
752
|
+
if (typeof sessionId !== "string" || !isSessionId(sessionId)) {
|
|
753
|
+
return `sessionId is ${describeValue2(sessionId)}, expected a plain file name`;
|
|
754
|
+
}
|
|
755
|
+
return basename(file) === `${sessionId}${RECORD_EXTENSION}` ? null : `sessionId ${JSON.stringify(sessionId)} is not the file's name`;
|
|
756
|
+
}
|
|
757
|
+
function planStubProblem(planStub) {
|
|
758
|
+
return planStub === null || typeof planStub === "string" && isStampableStub(planStub) ? null : `planStub is ${describeValue2(planStub)}, expected null or a plan stub`;
|
|
759
|
+
}
|
|
760
|
+
function textProblems(fields) {
|
|
761
|
+
return ["plan", "branch"].filter((key) => !isText(field(fields, key))).map((key) => `${key} is ${describeValue2(field(fields, key))}, expected a non-empty string`);
|
|
762
|
+
}
|
|
763
|
+
function recordProblems(fields, file) {
|
|
764
|
+
const pid = field(fields, "pid");
|
|
765
|
+
const startedAt = field(fields, "startedAt");
|
|
766
|
+
const state = field(fields, "state");
|
|
767
|
+
const problems = [
|
|
768
|
+
sessionIdProblem(field(fields, "sessionId"), file),
|
|
769
|
+
planStubProblem(field(fields, "planStub")),
|
|
770
|
+
...textProblems(fields),
|
|
771
|
+
isPositiveWhole(pid) ? null : `pid is ${describeValue2(pid)}, expected a whole number from 1`,
|
|
772
|
+
typeof startedAt === "string" && !Number.isNaN(Date.parse(startedAt)) ? null : `startedAt is ${describeValue2(startedAt)}, expected a timestamp`,
|
|
773
|
+
SESSION_STATES.includes(state) ? null : `state is ${describeValue2(state)}, expected one of ${SESSION_STATES.join(", ")}`,
|
|
774
|
+
taskProblem(field(fields, "task")),
|
|
775
|
+
hopProblem(fields),
|
|
776
|
+
worktreeProblem(fields),
|
|
777
|
+
...stepsProblems(fields),
|
|
778
|
+
...decisionsProblems(fields)
|
|
779
|
+
];
|
|
780
|
+
return problems.filter((problem) => problem !== null);
|
|
781
|
+
}
|
|
782
|
+
function isSessionPhase(value) {
|
|
783
|
+
return SESSION_PHASES.includes(value);
|
|
784
|
+
}
|
|
785
|
+
function phaseEntry(phase) {
|
|
786
|
+
return isSessionPhase(phase) ? { phase } : {};
|
|
787
|
+
}
|
|
788
|
+
function freezeHop(value) {
|
|
789
|
+
const hop = asHopRecord(value);
|
|
790
|
+
return Object.freeze({ ...hop, home: Object.freeze(hop.home), from: Object.freeze(hop.from) });
|
|
791
|
+
}
|
|
792
|
+
function errorLinesEntry(errorLines) {
|
|
793
|
+
const kept = Array.isArray(errorLines) && errorLines.length > 0 && errorLines.every((line) => typeof line === "string");
|
|
794
|
+
return kept ? { errorLines: Object.freeze([...errorLines]) } : {};
|
|
795
|
+
}
|
|
796
|
+
function freezeFailures(failures) {
|
|
797
|
+
return Object.freeze(failures.map((failure) => Object.freeze({
|
|
798
|
+
file: failure.file,
|
|
799
|
+
name: failure.name,
|
|
800
|
+
...errorLinesEntry(field(failure, "errorLines"))
|
|
801
|
+
})));
|
|
802
|
+
}
|
|
803
|
+
function isStringList(value) {
|
|
804
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
805
|
+
}
|
|
806
|
+
function isStepOnly(value) {
|
|
807
|
+
if (!isObject(value))
|
|
808
|
+
return false;
|
|
809
|
+
const position = field(value, "position");
|
|
810
|
+
return isText(field(value, "file")) && isTextList(field(value, "tests")) && isStringList(field(value, "errorLines")) && (position === null || isPositiveWhole(position)) && isTextList(field(value, "before"));
|
|
811
|
+
}
|
|
812
|
+
function stepOnlyEntry(stepOnly) {
|
|
813
|
+
const whole = Array.isArray(stepOnly) ? stepOnly.filter(isStepOnly) : [];
|
|
814
|
+
if (whole.length === 0)
|
|
815
|
+
return {};
|
|
816
|
+
return { stepOnly: Object.freeze(whole.map((entry) => Object.freeze({
|
|
817
|
+
file: entry.file,
|
|
818
|
+
tests: Object.freeze([...entry.tests]),
|
|
819
|
+
errorLines: Object.freeze([...entry.errorLines]),
|
|
820
|
+
position: entry.position,
|
|
821
|
+
before: Object.freeze([...entry.before])
|
|
822
|
+
}))) };
|
|
823
|
+
}
|
|
824
|
+
function outsideCaseEntry(outsideCase, outputTail) {
|
|
825
|
+
if (outsideCase !== true)
|
|
826
|
+
return {};
|
|
827
|
+
const tail = isStringList(outputTail) ? outputTail : [];
|
|
828
|
+
return { outsideCase: true, outputTail: Object.freeze([...tail]) };
|
|
829
|
+
}
|
|
830
|
+
function isSessionStepReason(value) {
|
|
831
|
+
return SESSION_STEP_REASONS.includes(value);
|
|
832
|
+
}
|
|
833
|
+
function reasonEntry(reason) {
|
|
834
|
+
return isSessionStepReason(reason) ? { reason } : {};
|
|
835
|
+
}
|
|
836
|
+
function freezeStep(step) {
|
|
837
|
+
return Object.freeze({
|
|
838
|
+
kind: step.kind,
|
|
839
|
+
scope: typeof step.scope === "string" ? step.scope : Object.freeze([...step.scope]),
|
|
840
|
+
...reasonEntry(field(step, "reason")),
|
|
841
|
+
command: Object.freeze([...step.command]),
|
|
842
|
+
exitCode: step.exitCode,
|
|
843
|
+
summary: step.summary,
|
|
844
|
+
failures: freezeFailures(step.failures),
|
|
845
|
+
newFailures: freezeFailures(step.newFailures),
|
|
846
|
+
...stepOnlyEntry(field(step, "stepOnly")),
|
|
847
|
+
...outsideCaseEntry(field(step, "outsideCase"), field(step, "outputTail")),
|
|
848
|
+
...step.interrupted === true ? { interrupted: true } : {}
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
function stepsEntry(steps) {
|
|
852
|
+
if (!Array.isArray(steps) || steps.length === 0)
|
|
853
|
+
return {};
|
|
854
|
+
return { steps: Object.freeze(steps.map(freezeStep)) };
|
|
855
|
+
}
|
|
856
|
+
function freezeTaskRef(ref) {
|
|
857
|
+
return Object.freeze({
|
|
858
|
+
lineNum: ref.lineNum,
|
|
859
|
+
task: ref.task,
|
|
860
|
+
...ref.ordinal === undefined ? {} : { ordinal: ref.ordinal }
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
function freezeDecision(decision) {
|
|
864
|
+
return Object.freeze({
|
|
865
|
+
task: freezeTaskRef(decision.task),
|
|
866
|
+
strategy: decision.strategy,
|
|
867
|
+
reason: decision.reason,
|
|
868
|
+
...decision.after === undefined ? {} : { after: freezeTaskRef(decision.after) }
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
function decisionsEntry(decisions) {
|
|
872
|
+
if (!Array.isArray(decisions) || decisions.length === 0)
|
|
873
|
+
return {};
|
|
874
|
+
return { decisions: Object.freeze(decisions.map(freezeDecision)) };
|
|
875
|
+
}
|
|
876
|
+
function freezeRecord(fields) {
|
|
877
|
+
const task = field(fields, "task");
|
|
878
|
+
const hop = field(fields, "hop");
|
|
879
|
+
const worktree = field(fields, "worktree");
|
|
880
|
+
return Object.freeze({
|
|
881
|
+
sessionId: field(fields, "sessionId"),
|
|
882
|
+
planStub: field(fields, "planStub"),
|
|
883
|
+
plan: field(fields, "plan"),
|
|
884
|
+
branch: field(fields, "branch"),
|
|
885
|
+
pid: field(fields, "pid"),
|
|
886
|
+
startedAt: field(fields, "startedAt"),
|
|
887
|
+
state: field(fields, "state"),
|
|
888
|
+
task: isObject(task) ? Object.freeze({ line: field(task, "line"), text: field(task, "text") }) : null,
|
|
889
|
+
...phaseEntry(field(fields, "phase")),
|
|
890
|
+
...hop === undefined ? {} : { hop: freezeHop(hop) },
|
|
891
|
+
...worktree === undefined ? {} : { worktree },
|
|
892
|
+
...stepsEntry(field(fields, "steps")),
|
|
893
|
+
...decisionsEntry(field(fields, "decisions"))
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
function sessionSteps(record) {
|
|
897
|
+
return record.steps ?? [];
|
|
898
|
+
}
|
|
899
|
+
function sessionDecisions(record) {
|
|
900
|
+
return record.decisions ?? [];
|
|
901
|
+
}
|
|
902
|
+
function parseSessionRecord(text, file) {
|
|
903
|
+
let parsed;
|
|
904
|
+
try {
|
|
905
|
+
parsed = JSON.parse(text);
|
|
906
|
+
} catch (error) {
|
|
907
|
+
throw new SessionRecordError(file, `holds no JSON: ${messageOf(error)}`);
|
|
908
|
+
}
|
|
909
|
+
if (!isObject(parsed))
|
|
910
|
+
throw new SessionRecordError(file, "holds no JSON object");
|
|
911
|
+
const problems = recordProblems(parsed, file);
|
|
912
|
+
if (problems.length > 0)
|
|
913
|
+
throw new SessionRecordError(file, problems.join("; "));
|
|
914
|
+
return freezeRecord(parsed);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// packages/rafa/src/core/loop/sessions.ts
|
|
918
|
+
var RUNS_DIR = "runs";
|
|
919
|
+
|
|
920
|
+
class SessionConflictError extends Error {
|
|
921
|
+
conflicts;
|
|
922
|
+
constructor(conflicts) {
|
|
923
|
+
const named = conflicts.map((conflict) => `${conflict.record.sessionId} (${conflict.reason})`);
|
|
924
|
+
super(`the plan's sessions refuse the run: ${named.join(", ")}`);
|
|
925
|
+
this.name = "SessionConflictError";
|
|
926
|
+
this.conflicts = conflicts;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
class SessionStateError extends Error {
|
|
931
|
+
file;
|
|
932
|
+
state;
|
|
933
|
+
constructor(file, state, onlyFrom) {
|
|
934
|
+
const acted = onlyFrom.length === 0 ? "no state" : onlyFrom.join(" or ");
|
|
935
|
+
super(`session record ${file}: stores ${state}, and the change acts on ${acted} alone`);
|
|
936
|
+
this.name = "SessionStateError";
|
|
937
|
+
this.file = file;
|
|
938
|
+
this.state = state;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
function runsDir(root) {
|
|
942
|
+
return join(scopeAt(root).dir, RUNS_DIR);
|
|
943
|
+
}
|
|
944
|
+
function sessionFilePath(root, sessionId) {
|
|
945
|
+
if (!isSessionId(sessionId)) {
|
|
946
|
+
throw new Error(`session record: unusable session id ${JSON.stringify(sessionId)}`);
|
|
947
|
+
}
|
|
948
|
+
return join(runsDir(root), `${sessionId}${RECORD_EXTENSION}`);
|
|
949
|
+
}
|
|
950
|
+
function errorCode(error) {
|
|
951
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : null;
|
|
952
|
+
}
|
|
953
|
+
function isPidAlive(pid) {
|
|
954
|
+
if (!isPositiveWhole(pid))
|
|
955
|
+
return false;
|
|
956
|
+
try {
|
|
957
|
+
process.kill(pid, 0);
|
|
958
|
+
return true;
|
|
959
|
+
} catch (error) {
|
|
960
|
+
return errorCode(error) === "EPERM";
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
function readState(record, isAlive = isPidAlive) {
|
|
964
|
+
if (record.state === "stopped" || record.state === "done")
|
|
965
|
+
return record.state;
|
|
966
|
+
return isAlive(record.pid) ? record.state : "stopped";
|
|
967
|
+
}
|
|
968
|
+
function readRecordFile(file) {
|
|
969
|
+
let text;
|
|
970
|
+
try {
|
|
971
|
+
text = readFileSync(file, "utf8");
|
|
972
|
+
} catch (error) {
|
|
973
|
+
throw new SessionRecordError(file, `cannot be read: ${messageOf(error)}`);
|
|
974
|
+
}
|
|
975
|
+
return parseSessionRecord(text, file);
|
|
976
|
+
}
|
|
977
|
+
function recordNames(dir) {
|
|
978
|
+
try {
|
|
979
|
+
return readdirSync(dir);
|
|
980
|
+
} catch (error) {
|
|
981
|
+
if (errorCode(error) === "ENOENT")
|
|
982
|
+
return [];
|
|
983
|
+
throw new SessionRecordError(dir, `cannot be listed: ${messageOf(error)}`);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
function byStart(a, b) {
|
|
987
|
+
return Date.parse(a.startedAt) - Date.parse(b.startedAt) || a.sessionId.localeCompare(b.sessionId);
|
|
988
|
+
}
|
|
989
|
+
function readSessions(root, seams = {}) {
|
|
990
|
+
const dir = runsDir(root);
|
|
991
|
+
const isAlive = seams.isAlive ?? isPidAlive;
|
|
992
|
+
const records = recordNames(dir).filter((name) => name.endsWith(RECORD_EXTENSION)).map((name) => readRecordFile(join(dir, name))).map((record) => Object.freeze({ ...record, state: readState(record, isAlive) }));
|
|
993
|
+
return Object.freeze(records.sort(byStart));
|
|
994
|
+
}
|
|
995
|
+
function readSession(root, sessionId, seams = {}) {
|
|
996
|
+
const record = readRecordFile(sessionFilePath(root, sessionId));
|
|
997
|
+
return Object.freeze({ ...record, state: readState(record, seams.isAlive ?? isPidAlive) });
|
|
998
|
+
}
|
|
999
|
+
function samePlan(a, b) {
|
|
1000
|
+
if (a.planStub !== null || b.planStub !== null)
|
|
1001
|
+
return a.planStub === b.planStub;
|
|
1002
|
+
return a.plan === b.plan;
|
|
1003
|
+
}
|
|
1004
|
+
function sessionConflicts(records, candidate) {
|
|
1005
|
+
return records.filter((record) => samePlan(record, candidate)).flatMap((record) => {
|
|
1006
|
+
if (record.branch !== candidate.branch)
|
|
1007
|
+
return [{ reason: "branch", record }];
|
|
1008
|
+
return record.state === "running" || record.state === "paused" ? [{ reason: "live", record }] : [];
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
function checkRecord(record, file) {
|
|
1012
|
+
const problems = recordProblems(record, file);
|
|
1013
|
+
if (problems.length > 0)
|
|
1014
|
+
throw new SessionRecordError(file, `not written: ${problems.join("; ")}`);
|
|
1015
|
+
}
|
|
1016
|
+
function writeRecordFile(file, record, isNew) {
|
|
1017
|
+
checkRecord(record, file);
|
|
1018
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
1019
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
1020
|
+
writeFileSync(temporary, `${JSON.stringify(record, null, 2)}
|
|
1021
|
+
`);
|
|
1022
|
+
try {
|
|
1023
|
+
if (isNew)
|
|
1024
|
+
linkSync(temporary, file);
|
|
1025
|
+
else
|
|
1026
|
+
renameSync(temporary, file);
|
|
1027
|
+
} finally {
|
|
1028
|
+
rmSync(temporary, { force: true });
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
function beginSession(root, draft, seams = {}) {
|
|
1032
|
+
const file = sessionFilePath(root, draft.sessionId);
|
|
1033
|
+
const record = freezeRecord({ ...draft, state: "running", task: null });
|
|
1034
|
+
const conflicts = sessionConflicts(readSessions(root, seams), record);
|
|
1035
|
+
if (conflicts.length > 0)
|
|
1036
|
+
throw new SessionConflictError(conflicts);
|
|
1037
|
+
writeRecordFile(file, record, true);
|
|
1038
|
+
return record;
|
|
1039
|
+
}
|
|
1040
|
+
function updateSession(root, sessionId, change) {
|
|
1041
|
+
const file = sessionFilePath(root, sessionId);
|
|
1042
|
+
const stored = readRecordFile(file);
|
|
1043
|
+
if (change.onlyFrom !== undefined && !change.onlyFrom.includes(stored.state)) {
|
|
1044
|
+
throw new SessionStateError(file, stored.state, change.onlyFrom);
|
|
1045
|
+
}
|
|
1046
|
+
const appended = change.appendStep === undefined ? [] : stepProblems(change.appendStep, `steps[${sessionSteps(stored).length}]`);
|
|
1047
|
+
if (appended.length > 0)
|
|
1048
|
+
throw new SessionRecordError(file, `not written: ${appended.join("; ")}`);
|
|
1049
|
+
const record = freezeRecord({
|
|
1050
|
+
...stored,
|
|
1051
|
+
state: change.state ?? stored.state,
|
|
1052
|
+
task: change.task === undefined ? stored.task : change.task,
|
|
1053
|
+
phase: change.phase ?? stored.phase,
|
|
1054
|
+
steps: change.appendStep === undefined ? sessionSteps(stored) : [...sessionSteps(stored), change.appendStep],
|
|
1055
|
+
decisions: change.decisions ?? sessionDecisions(stored)
|
|
1056
|
+
});
|
|
1057
|
+
writeRecordFile(file, record, false);
|
|
1058
|
+
return record;
|
|
1059
|
+
}
|
|
1060
|
+
// packages/rafa/src/core/loop/session-env.ts
|
|
1061
|
+
import { delimiter } from "path";
|
|
1062
|
+
function sessionPath(base, binDir) {
|
|
1063
|
+
return base === undefined || base === "" ? binDir : `${binDir}${delimiter}${base}`;
|
|
1064
|
+
}
|
|
1065
|
+
function sessionSpawnEnv(base, binDir = bundledBinDirectory()) {
|
|
1066
|
+
return {
|
|
1067
|
+
...base,
|
|
1068
|
+
PATH: sessionPath(base["PATH"], binDir),
|
|
1069
|
+
CLAUDE_CODE_ENTRYPOINT: "cli"
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// packages/rafa/src/core/loop/claude.ts
|
|
1074
|
+
var CLAUDE_BIN = "claude";
|
|
1075
|
+
var CLAUDE_BASE_ARGS = [
|
|
1076
|
+
"-p",
|
|
1077
|
+
"--dangerously-skip-permissions"
|
|
1078
|
+
];
|
|
1079
|
+
var SETTING_SOURCES_FLAG = "--setting-sources";
|
|
1080
|
+
function claudeArgs(settingSources, flags = [], served = []) {
|
|
1081
|
+
return [...CLAUDE_BASE_ARGS, SETTING_SOURCES_FLAG, settingSources.join(","), ...served, ...flags];
|
|
1082
|
+
}
|
|
1083
|
+
var liveSessions = new Set;
|
|
1084
|
+
function interruptClaudeSessions() {
|
|
1085
|
+
for (const session of liveSessions)
|
|
1086
|
+
session.kill("SIGINT");
|
|
1087
|
+
return liveSessions.size;
|
|
1088
|
+
}
|
|
1089
|
+
function carriesFlag(flags, flag) {
|
|
1090
|
+
const name = flag.replace(/^--/, "");
|
|
1091
|
+
const value = flags[name];
|
|
1092
|
+
if (value !== undefined && value !== false)
|
|
1093
|
+
return true;
|
|
1094
|
+
return name.startsWith("no-") && flags[name.slice(3)] === false;
|
|
1095
|
+
}
|
|
1096
|
+
function spendWarning(running2) {
|
|
1097
|
+
if (running2 === null)
|
|
1098
|
+
return null;
|
|
1099
|
+
const { command, flags } = running2;
|
|
1100
|
+
const spend = command.spends;
|
|
1101
|
+
const named = `rafa ${commandSpelling(command)}`;
|
|
1102
|
+
const remedy = "declare spends on the command to cover this run";
|
|
1103
|
+
if (spend === undefined) {
|
|
1104
|
+
return `${named} started a Claude session but declares no spends: ${remedy}`;
|
|
1105
|
+
}
|
|
1106
|
+
if (spend.when === "with" && !carriesFlag(flags, spend.flag)) {
|
|
1107
|
+
return `${named} started a Claude session without ${spend.flag}, and its spends covers only a run with ${spend.flag}: ${remedy}`;
|
|
1108
|
+
}
|
|
1109
|
+
if (spend.when === "unless" && carriesFlag(flags, spend.flag)) {
|
|
1110
|
+
return `${named} started a Claude session with ${spend.flag}, and its spends covers no run with ${spend.flag}: ${remedy}`;
|
|
1111
|
+
}
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
function guardSpend() {
|
|
1115
|
+
const warning = spendWarning(runningCommand());
|
|
1116
|
+
if (warning !== null)
|
|
1117
|
+
activeOutput().warn(`\u26A0\uFE0F ${warning}`);
|
|
1118
|
+
}
|
|
1119
|
+
async function spawnClaude(args, prompt, options = {}) {
|
|
1120
|
+
guardSpend();
|
|
1121
|
+
if (activeOutputMode() !== "text") {
|
|
1122
|
+
const { exitCode } = await spawnCaptured(args, prompt, options);
|
|
1123
|
+
return exitCode;
|
|
1124
|
+
}
|
|
1125
|
+
const proc = Bun.spawn([CLAUDE_BIN, ...args], {
|
|
1126
|
+
stdin: new TextEncoder().encode(prompt),
|
|
1127
|
+
stdout: "inherit",
|
|
1128
|
+
stderr: "inherit",
|
|
1129
|
+
env: sessionSpawnEnv(process.env),
|
|
1130
|
+
...cwdOption(options)
|
|
1131
|
+
});
|
|
1132
|
+
liveSessions.add(proc);
|
|
1133
|
+
try {
|
|
1134
|
+
return await proc.exited ?? 1;
|
|
1135
|
+
} finally {
|
|
1136
|
+
liveSessions.delete(proc);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
function runClaude(prompt, settingSources, flags = [], spawn = spawnClaude, served = [], options = {}) {
|
|
1140
|
+
return spawn(claudeArgs(settingSources, flags, served), prompt, options);
|
|
1141
|
+
}
|
|
1142
|
+
function cwdOption(options) {
|
|
1143
|
+
return options.cwd === undefined ? {} : { cwd: options.cwd };
|
|
1144
|
+
}
|
|
1145
|
+
function writeWholeLines(text, write) {
|
|
1146
|
+
const lines = text.split(`
|
|
1147
|
+
`);
|
|
1148
|
+
const rest = lines.pop() ?? "";
|
|
1149
|
+
for (const line of lines)
|
|
1150
|
+
write(line);
|
|
1151
|
+
return rest;
|
|
1152
|
+
}
|
|
1153
|
+
async function teeToOperator(stream) {
|
|
1154
|
+
const decoder = new TextDecoder;
|
|
1155
|
+
const routed = activeOutputMode() === "text" ? null : activeOutput();
|
|
1156
|
+
const writeLine = (line) => {
|
|
1157
|
+
routed?.info(line);
|
|
1158
|
+
};
|
|
1159
|
+
let text = "";
|
|
1160
|
+
let pending = "";
|
|
1161
|
+
for await (const chunk of stream) {
|
|
1162
|
+
if (routed === null)
|
|
1163
|
+
process.stdout.write(chunk);
|
|
1164
|
+
const decoded = decoder.decode(chunk, { stream: true });
|
|
1165
|
+
text += decoded;
|
|
1166
|
+
if (routed !== null)
|
|
1167
|
+
pending = writeWholeLines(pending + decoded, writeLine);
|
|
1168
|
+
}
|
|
1169
|
+
const tail = decoder.decode();
|
|
1170
|
+
if (routed !== null) {
|
|
1171
|
+
const rest = writeWholeLines(pending + tail, writeLine);
|
|
1172
|
+
if (rest !== "")
|
|
1173
|
+
writeLine(rest);
|
|
1174
|
+
}
|
|
1175
|
+
return text + tail;
|
|
1176
|
+
}
|
|
1177
|
+
async function spawnClaudeCaptured(args, prompt, options = {}) {
|
|
1178
|
+
guardSpend();
|
|
1179
|
+
return spawnCaptured(args, prompt, options);
|
|
1180
|
+
}
|
|
1181
|
+
async function spawnCaptured(args, prompt, options = {}) {
|
|
1182
|
+
const proc = Bun.spawn([CLAUDE_BIN, ...args], {
|
|
1183
|
+
stdin: new TextEncoder().encode(prompt),
|
|
1184
|
+
stdout: "pipe",
|
|
1185
|
+
stderr: "inherit",
|
|
1186
|
+
env: sessionSpawnEnv(process.env),
|
|
1187
|
+
...cwdOption(options)
|
|
1188
|
+
});
|
|
1189
|
+
liveSessions.add(proc);
|
|
1190
|
+
let stdout;
|
|
1191
|
+
try {
|
|
1192
|
+
stdout = await teeToOperator(proc.stdout);
|
|
1193
|
+
} finally {
|
|
1194
|
+
await proc.exited;
|
|
1195
|
+
liveSessions.delete(proc);
|
|
1196
|
+
}
|
|
1197
|
+
return { exitCode: await proc.exited, stdout };
|
|
1198
|
+
}
|
|
1199
|
+
function runClaudeCaptured(prompt, settingSources, flags = [], spawn = spawnClaudeCaptured, served = [], options = {}) {
|
|
1200
|
+
return spawn(claudeArgs(settingSources, flags, served), prompt, options);
|
|
1201
|
+
}
|
|
1202
|
+
// packages/rafa/src/core/runtime/identity.ts
|
|
1203
|
+
import { existsSync, readFileSync as readFileSync2, realpathSync } from "fs";
|
|
1204
|
+
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
1205
|
+
var RAFA_PACKAGE_NAME = "@open-tomato/rafa";
|
|
1206
|
+
var MANIFEST = "package.json";
|
|
1207
|
+
var CHECKOUT_ENTRY = join2("src", "rafa.ts");
|
|
1208
|
+
function realOrResolved(path) {
|
|
1209
|
+
const resolved = resolve(path);
|
|
1210
|
+
return existsSync(resolved) ? realpathSync(resolved) : resolved;
|
|
1211
|
+
}
|
|
1212
|
+
function manifestName(file) {
|
|
1213
|
+
let parsed;
|
|
1214
|
+
try {
|
|
1215
|
+
parsed = JSON.parse(readFileSync2(file, "utf8"));
|
|
1216
|
+
} catch {
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
1219
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
1220
|
+
return null;
|
|
1221
|
+
const name = parsed["name"];
|
|
1222
|
+
return typeof name === "string" ? name : null;
|
|
1223
|
+
}
|
|
1224
|
+
function nearestRafaPackage(start) {
|
|
1225
|
+
let dir = start;
|
|
1226
|
+
for (;; ) {
|
|
1227
|
+
const manifest = join2(dir, MANIFEST);
|
|
1228
|
+
if (existsSync(manifest) && manifestName(manifest) === RAFA_PACKAGE_NAME)
|
|
1229
|
+
return dir;
|
|
1230
|
+
const parent = dirname2(dir);
|
|
1231
|
+
if (parent === dir)
|
|
1232
|
+
return null;
|
|
1233
|
+
dir = parent;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
function readRuntimeIdentity(entry = Bun.main) {
|
|
1237
|
+
const real = realOrResolved(entry);
|
|
1238
|
+
const packageDir = nearestRafaPackage(dirname2(real));
|
|
1239
|
+
if (packageDir !== null && existsSync(join2(packageDir, CHECKOUT_ENTRY))) {
|
|
1240
|
+
return { kind: "development", entry: real, checkout: packageDir };
|
|
1241
|
+
}
|
|
1242
|
+
return { kind: "installed", entry: real };
|
|
1243
|
+
}
|
|
1244
|
+
// packages/rafa/src/effort/store/location.ts
|
|
1245
|
+
import { existsSync as existsSync3, realpathSync as realpathSync2 } from "fs";
|
|
1246
|
+
import { tmpdir } from "os";
|
|
1247
|
+
import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute2, join as join4, relative, resolve as resolve2, sep } from "path";
|
|
1248
|
+
|
|
1249
|
+
// packages/rafa/src/effort/store/ndjson-files.ts
|
|
1250
|
+
import {
|
|
1251
|
+
appendFileSync,
|
|
1252
|
+
existsSync as existsSync2,
|
|
1253
|
+
mkdirSync as mkdirSync2,
|
|
1254
|
+
readFileSync as readFileSync3
|
|
1255
|
+
} from "fs";
|
|
1256
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
1257
|
+
var EFFORT_STORE_DIR = join3(".rafa", "effort");
|
|
1258
|
+
var STORE_FILE_NAMES = {
|
|
1259
|
+
sessions: "sessions.ndjson",
|
|
1260
|
+
commits: "commits.ndjson"
|
|
1261
|
+
};
|
|
1262
|
+
function asObject(value) {
|
|
1263
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
1264
|
+
}
|
|
1265
|
+
function normaliseKey(key) {
|
|
1266
|
+
return typeof key === "string" && key.length > 0 ? key : null;
|
|
1267
|
+
}
|
|
1268
|
+
function effortStoreFileName(kind) {
|
|
1269
|
+
return STORE_FILE_NAMES[kind];
|
|
1270
|
+
}
|
|
1271
|
+
function readStoreText(path) {
|
|
1272
|
+
return existsSync2(path) ? readFileSync3(path, "utf8") : null;
|
|
1273
|
+
}
|
|
1274
|
+
function parseStoreText(text) {
|
|
1275
|
+
const rows = [];
|
|
1276
|
+
if (text === null || text.length === 0) {
|
|
1277
|
+
return { rows, lineCount: 0, unparsedLineCount: 0 };
|
|
1278
|
+
}
|
|
1279
|
+
const lines = text.split(/\r?\n/);
|
|
1280
|
+
if (lines[lines.length - 1] === "")
|
|
1281
|
+
lines.pop();
|
|
1282
|
+
let unparsedLineCount = 0;
|
|
1283
|
+
for (const line of lines) {
|
|
1284
|
+
const record = parseRow(line);
|
|
1285
|
+
if (record === null) {
|
|
1286
|
+
unparsedLineCount += 1;
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
rows.push(record);
|
|
1290
|
+
}
|
|
1291
|
+
return { rows, lineCount: lines.length, unparsedLineCount };
|
|
1292
|
+
}
|
|
1293
|
+
function parseRow(line) {
|
|
1294
|
+
if (line.trim().length === 0)
|
|
1295
|
+
return null;
|
|
1296
|
+
try {
|
|
1297
|
+
return asObject(JSON.parse(line));
|
|
1298
|
+
} catch {
|
|
1299
|
+
return null;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
function readStoreRows(path) {
|
|
1303
|
+
return parseStoreText(readStoreText(path));
|
|
1304
|
+
}
|
|
1305
|
+
function keysOfRows(rows, keyOf) {
|
|
1306
|
+
const keys = new Set;
|
|
1307
|
+
for (const row of rows) {
|
|
1308
|
+
const key = normaliseKey(keyOf(row));
|
|
1309
|
+
if (key !== null)
|
|
1310
|
+
keys.add(key);
|
|
1311
|
+
}
|
|
1312
|
+
return keys;
|
|
1313
|
+
}
|
|
1314
|
+
function collectedKeys(path, keyOf) {
|
|
1315
|
+
return keysOfRows(readStoreRows(path).rows, keyOf);
|
|
1316
|
+
}
|
|
1317
|
+
function appendRows(path, rows, keyOf) {
|
|
1318
|
+
const keys = rows.map((row) => normaliseKey(keyOf(row)));
|
|
1319
|
+
const keylessAt = keys.indexOf(null);
|
|
1320
|
+
if (keylessAt !== -1) {
|
|
1321
|
+
const at = `row ${keylessAt} of ${rows.length}`;
|
|
1322
|
+
throw new Error(`effort store: ${at} carries no key; batch refused`);
|
|
1323
|
+
}
|
|
1324
|
+
const text = readStoreText(path);
|
|
1325
|
+
const held = keysOfRows(parseStoreText(text).rows, keyOf);
|
|
1326
|
+
const lines = [];
|
|
1327
|
+
let skipped = 0;
|
|
1328
|
+
for (const [index, row] of rows.entries()) {
|
|
1329
|
+
const key = keys[index] ?? null;
|
|
1330
|
+
if (key === null || held.has(key)) {
|
|
1331
|
+
skipped += 1;
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
held.add(key);
|
|
1335
|
+
lines.push(JSON.stringify(row));
|
|
1336
|
+
}
|
|
1337
|
+
if (lines.length === 0) {
|
|
1338
|
+
return { path, appended: 0, skipped, separatedPartialLine: false };
|
|
1339
|
+
}
|
|
1340
|
+
const separatedPartialLine = text !== null && text.length > 0 && !text.endsWith(`
|
|
1341
|
+
`);
|
|
1342
|
+
const lead = separatedPartialLine ? `
|
|
1343
|
+
` : "";
|
|
1344
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
1345
|
+
appendFileSync(path, `${lead}${lines.join(`
|
|
1346
|
+
`)}
|
|
1347
|
+
`, "utf8");
|
|
1348
|
+
return {
|
|
1349
|
+
path,
|
|
1350
|
+
appended: lines.length,
|
|
1351
|
+
skipped,
|
|
1352
|
+
separatedPartialLine
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// packages/rafa/src/effort/store/location.ts
|
|
1357
|
+
var EFFORT_DIR_VARIABLE = "RAFA_EFFORT_DIR";
|
|
1358
|
+
var TEST_PROCESS_VARIABLE = "RAFA_TEST";
|
|
1359
|
+
var TEST_ENTRY_SUFFIX = ".test.ts";
|
|
1360
|
+
function canonical(path) {
|
|
1361
|
+
const resolved = resolve2(path);
|
|
1362
|
+
return existsSync3(resolved) ? realpathSync2(resolved) : resolved;
|
|
1363
|
+
}
|
|
1364
|
+
function realPathOf(path) {
|
|
1365
|
+
const resolved = resolve2(path);
|
|
1366
|
+
if (existsSync3(resolved))
|
|
1367
|
+
return realpathSync2(resolved);
|
|
1368
|
+
const parent = dirname4(resolved);
|
|
1369
|
+
return parent === resolved ? resolved : join4(realPathOf(parent), basename2(resolved));
|
|
1370
|
+
}
|
|
1371
|
+
function isWithin(path, dir) {
|
|
1372
|
+
const between = relative(resolve2(dir), resolve2(path));
|
|
1373
|
+
const climbsOut = between === ".." || between.startsWith(`..${sep}`);
|
|
1374
|
+
return !climbsOut && !isAbsolute2(between);
|
|
1375
|
+
}
|
|
1376
|
+
function readEffortDirOverride(repoRoot, env = process.env) {
|
|
1377
|
+
const value = env[EFFORT_DIR_VARIABLE];
|
|
1378
|
+
if (value === undefined || value === "")
|
|
1379
|
+
return null;
|
|
1380
|
+
if (!isAbsolute2(value)) {
|
|
1381
|
+
throw new Error(`effort store: ${EFFORT_DIR_VARIABLE} is not an absolute path (${value}); run this command again` + ` with ${EFFORT_DIR_VARIABLE} set to the absolute path 'rafa effort copy' printed.`);
|
|
1382
|
+
}
|
|
1383
|
+
if (canonical(value) === canonical(join4(repoRoot, EFFORT_STORE_DIR))) {
|
|
1384
|
+
throw new Error(`effort store: ${EFFORT_DIR_VARIABLE} names this project's own store (${value}); it points a command` + " at a copy only. Copy the store with 'rafa effort copy' and run this command with" + ` ${EFFORT_DIR_VARIABLE}=<the copy>.`);
|
|
1385
|
+
}
|
|
1386
|
+
return value;
|
|
1387
|
+
}
|
|
1388
|
+
function effortStoreDir(repoRoot, env = process.env) {
|
|
1389
|
+
return readEffortDirOverride(repoRoot, env) ?? join4(repoRoot, EFFORT_STORE_DIR);
|
|
1390
|
+
}
|
|
1391
|
+
function isTestProcess(main = Bun.main, env = process.env) {
|
|
1392
|
+
return main.endsWith(TEST_ENTRY_SUFFIX) || env[TEST_PROCESS_VARIABLE] === "1";
|
|
1393
|
+
}
|
|
1394
|
+
function isUnderTempDir(path, tempDir = tmpdir()) {
|
|
1395
|
+
return isWithin(path, tempDir) || isWithin(realPathOf(path), realPathOf(tempDir));
|
|
1396
|
+
}
|
|
1397
|
+
function currentProbe() {
|
|
1398
|
+
return { main: Bun.main, env: process.env, tempDir: tmpdir() };
|
|
1399
|
+
}
|
|
1400
|
+
function guardTestProcess(path, probe = currentProbe()) {
|
|
1401
|
+
if (!isTestProcess(probe.main, probe.env))
|
|
1402
|
+
return;
|
|
1403
|
+
if (isUnderTempDir(path, probe.tempDir))
|
|
1404
|
+
return;
|
|
1405
|
+
throw new Error(`effort store: a test opened ${resolve2(path)}, outside the temp directory ${probe.tempDir};` + " a test opens stores under tmpdir() only");
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// packages/rafa/src/effort/store/development-build.ts
|
|
1409
|
+
var DEVELOPMENT_NEXT_STEP = "rafa effort copy";
|
|
1410
|
+
var ADOPTION_NAME = "schema_migrations";
|
|
1411
|
+
var EFFORT_DIR_NAME = "effort";
|
|
1412
|
+
var DEVELOPMENT_MARK = "+dev:";
|
|
1413
|
+
function appliedByName(identity) {
|
|
1414
|
+
return identity.kind === "development" ? `${RAFA_VERSION}${DEVELOPMENT_MARK}${identity.checkout}` : RAFA_VERSION;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
class DevelopmentBuildRefusedError extends Error {
|
|
1418
|
+
path;
|
|
1419
|
+
nextStep;
|
|
1420
|
+
constructor(path, message) {
|
|
1421
|
+
super(message);
|
|
1422
|
+
this.name = "DevelopmentBuildRefusedError";
|
|
1423
|
+
this.path = path;
|
|
1424
|
+
this.nextStep = DEVELOPMENT_NEXT_STEP;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
function storeProjectRoot(path) {
|
|
1428
|
+
const storeDir = dirname5(path);
|
|
1429
|
+
const scopeDir = dirname5(storeDir);
|
|
1430
|
+
if (basename3(storeDir) !== EFFORT_DIR_NAME || basename3(scopeDir) !== SCOPE_DIR)
|
|
1431
|
+
return null;
|
|
1432
|
+
return dirname5(scopeDir);
|
|
1433
|
+
}
|
|
1434
|
+
function isOwnedStore(path, env, tempDir) {
|
|
1435
|
+
if (isUnderTempDir(path, tempDir))
|
|
1436
|
+
return true;
|
|
1437
|
+
const override = env[EFFORT_DIR_VARIABLE];
|
|
1438
|
+
if (override === undefined || override === "" || !isAbsolute3(override))
|
|
1439
|
+
return false;
|
|
1440
|
+
return storeProjectRoot(path) === null && isUnderTempDir(path, override);
|
|
1441
|
+
}
|
|
1442
|
+
function loopSentence(record) {
|
|
1443
|
+
return ` Loop ${record.sessionId} (pid ${String(record.pid)}, plan ${record.planStub ?? record.plan}) is running on this store.`;
|
|
1444
|
+
}
|
|
1445
|
+
function liveLoopSentences(path, isAlive) {
|
|
1446
|
+
const root = storeProjectRoot(path);
|
|
1447
|
+
if (root === null)
|
|
1448
|
+
return "";
|
|
1449
|
+
try {
|
|
1450
|
+
return readSessions(root, { isAlive }).filter((record) => record.state === "running" || record.state === "paused").map(loopSentence).join("");
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
return ` The loop records under ${runsDir(root)} could not be read (${messageOf(error)}).`;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
function developmentRefusalText(path, needs, checkout, loops) {
|
|
1456
|
+
return `effort store: ${path} needs migration ${needs.join(", ")} and this rafa is a development build (${checkout});` + ` a development build migrates only a store under the temp directory or ${EFFORT_DIR_VARIABLE}.` + ` Copy it with '${DEVELOPMENT_NEXT_STEP}' and run this command with ${EFFORT_DIR_VARIABLE}=<the copy>.` + loops;
|
|
1457
|
+
}
|
|
1458
|
+
function refuseUnownedDevelopmentWrite(path, needs, probe = {}) {
|
|
1459
|
+
const identity = probe.identity ?? readRuntimeIdentity();
|
|
1460
|
+
if (identity.kind !== "development")
|
|
1461
|
+
return;
|
|
1462
|
+
if (isOwnedStore(path, probe.env ?? process.env, probe.tempDir ?? tmpdir2()))
|
|
1463
|
+
return;
|
|
1464
|
+
const loops = liveLoopSentences(path, probe.isAlive ?? isPidAlive);
|
|
1465
|
+
throw new DevelopmentBuildRefusedError(path, developmentRefusalText(path, needs, identity.checkout, loops));
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// packages/rafa/src/effort/store/migrations.ts
|
|
1469
|
+
import { createHash } from "crypto";
|
|
1470
|
+
var LEGACY_GATE_OPEN = 13;
|
|
1471
|
+
var LEGACY_GATE_CLOSED = 1000;
|
|
1472
|
+
var SQLITE_MIGRATIONS = [
|
|
1473
|
+
{
|
|
1474
|
+
id: "kind-tables",
|
|
1475
|
+
breaks: [],
|
|
1476
|
+
sql: `
|
|
1477
|
+
CREATE TABLE sessions (
|
|
1478
|
+
seq INTEGER PRIMARY KEY,
|
|
1479
|
+
session_id TEXT NOT NULL UNIQUE CHECK (session_id <> ''),
|
|
1480
|
+
row_json TEXT NOT NULL
|
|
1481
|
+
);
|
|
1482
|
+
|
|
1483
|
+
CREATE TABLE commits (
|
|
1484
|
+
seq INTEGER PRIMARY KEY,
|
|
1485
|
+
sha TEXT NOT NULL UNIQUE CHECK (sha <> ''),
|
|
1486
|
+
row_json TEXT NOT NULL
|
|
1487
|
+
);
|
|
1488
|
+
`
|
|
1489
|
+
},
|
|
1490
|
+
{
|
|
1491
|
+
id: "findings",
|
|
1492
|
+
breaks: [],
|
|
1493
|
+
sql: `
|
|
1494
|
+
CREATE TABLE findings (
|
|
1495
|
+
seq INTEGER PRIMARY KEY,
|
|
1496
|
+
id TEXT NOT NULL UNIQUE CHECK (id <> ''),
|
|
1497
|
+
session_id TEXT NOT NULL CHECK (session_id <> ''),
|
|
1498
|
+
plan_stub TEXT,
|
|
1499
|
+
task_line TEXT NOT NULL,
|
|
1500
|
+
kind TEXT
|
|
1501
|
+
CHECK (kind IN ('gotcha', 'pattern', 'location', 'skill-suggestion')),
|
|
1502
|
+
trigger TEXT CHECK (trigger <> ''),
|
|
1503
|
+
what TEXT CHECK (what <> ''),
|
|
1504
|
+
cause TEXT,
|
|
1505
|
+
resolution TEXT,
|
|
1506
|
+
artifact TEXT CHECK (artifact <> ''),
|
|
1507
|
+
signal TEXT CHECK (signal IN ('loud', 'silent')),
|
|
1508
|
+
outcome TEXT NOT NULL,
|
|
1509
|
+
tracker_ref TEXT,
|
|
1510
|
+
collected_at TEXT NOT NULL,
|
|
1511
|
+
CHECK (artifact IS NOT NULL OR (trigger IS NOT NULL AND what IS NOT NULL))
|
|
1512
|
+
);
|
|
1513
|
+
|
|
1514
|
+
CREATE UNIQUE INDEX findings_by_artifact
|
|
1515
|
+
ON findings (session_id, artifact)
|
|
1516
|
+
WHERE artifact IS NOT NULL;
|
|
1517
|
+
|
|
1518
|
+
CREATE UNIQUE INDEX findings_by_trigger_what
|
|
1519
|
+
ON findings (session_id, trigger, what)
|
|
1520
|
+
WHERE artifact IS NULL;
|
|
1521
|
+
`
|
|
1522
|
+
},
|
|
1523
|
+
{
|
|
1524
|
+
id: "blockers-and-bugs",
|
|
1525
|
+
breaks: [],
|
|
1526
|
+
sql: `
|
|
1527
|
+
CREATE TABLE blockers (
|
|
1528
|
+
seq INTEGER PRIMARY KEY,
|
|
1529
|
+
id TEXT NOT NULL UNIQUE CHECK (id <> ''),
|
|
1530
|
+
session_id TEXT NOT NULL CHECK (session_id <> ''),
|
|
1531
|
+
plan_stub TEXT,
|
|
1532
|
+
task_line TEXT NOT NULL,
|
|
1533
|
+
what TEXT NOT NULL CHECK (what <> ''),
|
|
1534
|
+
artifact TEXT CHECK (artifact <> ''),
|
|
1535
|
+
outcome TEXT NOT NULL,
|
|
1536
|
+
collected_at TEXT NOT NULL
|
|
1537
|
+
);
|
|
1538
|
+
|
|
1539
|
+
CREATE UNIQUE INDEX blockers_by_entry
|
|
1540
|
+
ON blockers (session_id, what, ifnull(artifact, ''));
|
|
1541
|
+
|
|
1542
|
+
CREATE TABLE out_of_scope_bugs (
|
|
1543
|
+
seq INTEGER PRIMARY KEY,
|
|
1544
|
+
id TEXT NOT NULL UNIQUE CHECK (id <> ''),
|
|
1545
|
+
session_id TEXT NOT NULL CHECK (session_id <> ''),
|
|
1546
|
+
plan_stub TEXT,
|
|
1547
|
+
task_line TEXT NOT NULL,
|
|
1548
|
+
what TEXT NOT NULL CHECK (what <> ''),
|
|
1549
|
+
artifact TEXT CHECK (artifact <> ''),
|
|
1550
|
+
security INTEGER CHECK (security IN (0, 1)),
|
|
1551
|
+
outcome TEXT NOT NULL,
|
|
1552
|
+
collected_at TEXT NOT NULL
|
|
1553
|
+
);
|
|
1554
|
+
|
|
1555
|
+
CREATE UNIQUE INDEX out_of_scope_bugs_by_entry
|
|
1556
|
+
ON out_of_scope_bugs (session_id, what, ifnull(artifact, ''), ifnull(security, -1));
|
|
1557
|
+
`
|
|
1558
|
+
},
|
|
1559
|
+
{
|
|
1560
|
+
id: "report-absences",
|
|
1561
|
+
breaks: [],
|
|
1562
|
+
sql: `
|
|
1563
|
+
CREATE TABLE report_absences (
|
|
1564
|
+
seq INTEGER PRIMARY KEY,
|
|
1565
|
+
id TEXT NOT NULL UNIQUE CHECK (id <> ''),
|
|
1566
|
+
session_id TEXT NOT NULL UNIQUE CHECK (session_id <> ''),
|
|
1567
|
+
plan_stub TEXT,
|
|
1568
|
+
task_line TEXT NOT NULL,
|
|
1569
|
+
reason TEXT NOT NULL CHECK (reason <> ''),
|
|
1570
|
+
detail TEXT NOT NULL CHECK (detail <> ''),
|
|
1571
|
+
block_body TEXT,
|
|
1572
|
+
outcome TEXT NOT NULL,
|
|
1573
|
+
collected_at TEXT NOT NULL
|
|
1574
|
+
);
|
|
1575
|
+
`
|
|
1576
|
+
},
|
|
1577
|
+
{
|
|
1578
|
+
id: "task-reports",
|
|
1579
|
+
breaks: [],
|
|
1580
|
+
sql: `
|
|
1581
|
+
CREATE TABLE task_reports (
|
|
1582
|
+
seq INTEGER PRIMARY KEY,
|
|
1583
|
+
id TEXT NOT NULL UNIQUE CHECK (id <> ''),
|
|
1584
|
+
session_id TEXT NOT NULL UNIQUE CHECK (session_id <> ''),
|
|
1585
|
+
plan_stub TEXT,
|
|
1586
|
+
task_line TEXT NOT NULL,
|
|
1587
|
+
status TEXT CHECK (status IN ('done', 'blocked')),
|
|
1588
|
+
outcome TEXT NOT NULL,
|
|
1589
|
+
collected_at TEXT NOT NULL
|
|
1590
|
+
);
|
|
1591
|
+
`
|
|
1592
|
+
},
|
|
1593
|
+
{
|
|
1594
|
+
id: "preflight",
|
|
1595
|
+
breaks: [],
|
|
1596
|
+
sql: `
|
|
1597
|
+
CREATE TABLE preflight (
|
|
1598
|
+
seq INTEGER PRIMARY KEY,
|
|
1599
|
+
run_id TEXT NOT NULL CHECK (run_id <> ''),
|
|
1600
|
+
position INTEGER NOT NULL CHECK (typeof(position) = 'integer' AND position >= 0),
|
|
1601
|
+
tier TEXT NOT NULL CHECK (tier IN ('required', 'optional')),
|
|
1602
|
+
kind TEXT NOT NULL CHECK (kind <> ''),
|
|
1603
|
+
item TEXT NOT NULL CHECK (item <> ''),
|
|
1604
|
+
probe TEXT CHECK (probe <> ''),
|
|
1605
|
+
outcome TEXT NOT NULL CHECK (outcome IN ('pass', 'fail', 'timeout')),
|
|
1606
|
+
duration_ms INTEGER NOT NULL CHECK (typeof(duration_ms) = 'integer' AND duration_ms >= 0),
|
|
1607
|
+
failure TEXT CHECK (failure <> ''),
|
|
1608
|
+
collected_at TEXT NOT NULL,
|
|
1609
|
+
UNIQUE (run_id, position),
|
|
1610
|
+
CHECK ((outcome = 'pass') = (failure IS NULL))
|
|
1611
|
+
);
|
|
1612
|
+
`
|
|
1613
|
+
},
|
|
1614
|
+
{
|
|
1615
|
+
id: "dispatches",
|
|
1616
|
+
breaks: [],
|
|
1617
|
+
sql: `
|
|
1618
|
+
CREATE TABLE dispatches (
|
|
1619
|
+
seq INTEGER PRIMARY KEY,
|
|
1620
|
+
session_id TEXT NOT NULL UNIQUE CHECK (session_id <> ''),
|
|
1621
|
+
plan_stub TEXT,
|
|
1622
|
+
task_line TEXT NOT NULL,
|
|
1623
|
+
declaration TEXT CHECK (declaration <> ''),
|
|
1624
|
+
agent TEXT CHECK (agent <> ''),
|
|
1625
|
+
model TEXT CHECK (model <> ''),
|
|
1626
|
+
effort TEXT CHECK (effort <> ''),
|
|
1627
|
+
budget_usd REAL CHECK (budget_usd IS NULL OR (typeof(budget_usd) IN ('integer', 'real') AND budget_usd > 0)),
|
|
1628
|
+
tools TEXT CHECK (tools <> ''),
|
|
1629
|
+
flags TEXT NOT NULL,
|
|
1630
|
+
collected_at TEXT NOT NULL
|
|
1631
|
+
);
|
|
1632
|
+
`
|
|
1633
|
+
},
|
|
1634
|
+
{
|
|
1635
|
+
id: "changes",
|
|
1636
|
+
breaks: [],
|
|
1637
|
+
sql: `
|
|
1638
|
+
CREATE TABLE changes (
|
|
1639
|
+
seq INTEGER PRIMARY KEY,
|
|
1640
|
+
id TEXT NOT NULL UNIQUE CHECK (id <> ''),
|
|
1641
|
+
session_id TEXT NOT NULL CHECK (session_id <> ''),
|
|
1642
|
+
plan_stub TEXT,
|
|
1643
|
+
task_line TEXT NOT NULL,
|
|
1644
|
+
level TEXT NOT NULL
|
|
1645
|
+
CHECK (level IN ('patch', 'minor', 'major', 'none')),
|
|
1646
|
+
area TEXT CHECK (area <> ''),
|
|
1647
|
+
summary TEXT NOT NULL CHECK (summary <> ''),
|
|
1648
|
+
collected_at TEXT NOT NULL
|
|
1649
|
+
);
|
|
1650
|
+
|
|
1651
|
+
CREATE UNIQUE INDEX changes_by_entry
|
|
1652
|
+
ON changes (session_id, level, ifnull(area, ''), summary);
|
|
1653
|
+
`
|
|
1654
|
+
},
|
|
1655
|
+
{
|
|
1656
|
+
id: "out-of-scope-bug-scope",
|
|
1657
|
+
breaks: [],
|
|
1658
|
+
sql: `
|
|
1659
|
+
ALTER TABLE out_of_scope_bugs
|
|
1660
|
+
ADD COLUMN scope TEXT CHECK (scope IN ('machine', 'rafa'));
|
|
1661
|
+
`
|
|
1662
|
+
},
|
|
1663
|
+
{
|
|
1664
|
+
id: "dispatch-skills",
|
|
1665
|
+
breaks: [],
|
|
1666
|
+
sql: `
|
|
1667
|
+
ALTER TABLE dispatches
|
|
1668
|
+
ADD COLUMN resolver TEXT CHECK (resolver IN ('planner', 'tag', 'none'));
|
|
1669
|
+
ALTER TABLE dispatches
|
|
1670
|
+
ADD COLUMN skills_offered TEXT CHECK (skills_offered IS NULL OR json_type(skills_offered) = 'array');
|
|
1671
|
+
ALTER TABLE dispatches
|
|
1672
|
+
ADD COLUMN lessons_offered TEXT CHECK (lessons_offered IS NULL OR json_type(lessons_offered) = 'array');
|
|
1673
|
+
`
|
|
1674
|
+
},
|
|
1675
|
+
{
|
|
1676
|
+
id: "skill-invocations",
|
|
1677
|
+
breaks: [],
|
|
1678
|
+
sql: `
|
|
1679
|
+
CREATE TABLE skill_invocations (
|
|
1680
|
+
seq INTEGER PRIMARY KEY,
|
|
1681
|
+
session_id TEXT NOT NULL CHECK (session_id <> ''),
|
|
1682
|
+
name TEXT CHECK (name <> ''),
|
|
1683
|
+
sidechain INTEGER CHECK (sidechain IN (0, 1)),
|
|
1684
|
+
count INTEGER CHECK (count IS NULL OR (typeof(count) = 'integer' AND count > 0)),
|
|
1685
|
+
CHECK ((name IS NULL) = (count IS NULL) AND (name IS NULL) = (sidechain IS NULL))
|
|
1686
|
+
);
|
|
1687
|
+
|
|
1688
|
+
CREATE UNIQUE INDEX skill_invocations_by_use
|
|
1689
|
+
ON skill_invocations (session_id, ifnull(name, ''), ifnull(sidechain, -1));
|
|
1690
|
+
`
|
|
1691
|
+
},
|
|
1692
|
+
{
|
|
1693
|
+
id: "task-report-skills",
|
|
1694
|
+
breaks: [],
|
|
1695
|
+
sql: `
|
|
1696
|
+
ALTER TABLE task_reports
|
|
1697
|
+
ADD COLUMN skills_used TEXT CHECK (skills_used IS NULL OR json_type(skills_used) = 'array');
|
|
1698
|
+
`
|
|
1699
|
+
},
|
|
1700
|
+
{
|
|
1701
|
+
id: "plan-ci",
|
|
1702
|
+
breaks: [],
|
|
1703
|
+
sql: `
|
|
1704
|
+
CREATE TABLE plan_ci (
|
|
1705
|
+
seq INTEGER PRIMARY KEY,
|
|
1706
|
+
plan_stub TEXT NOT NULL CHECK (plan_stub <> ''),
|
|
1707
|
+
pr INTEGER NOT NULL CHECK (typeof(pr) = 'integer' AND pr > 0),
|
|
1708
|
+
head_sha TEXT NOT NULL CHECK (head_sha <> ''),
|
|
1709
|
+
verdict TEXT NOT NULL CHECK (verdict IN ('green', 'red', 'none')),
|
|
1710
|
+
failing TEXT NOT NULL CHECK (json_valid(failing) AND json_type(failing) = 'array'),
|
|
1711
|
+
read_at TEXT NOT NULL CHECK (read_at <> ''),
|
|
1712
|
+
UNIQUE (pr, head_sha, read_at),
|
|
1713
|
+
CHECK ((verdict = 'red') = (json_array_length(failing) > 0))
|
|
1714
|
+
);
|
|
1715
|
+
`
|
|
1716
|
+
},
|
|
1717
|
+
{
|
|
1718
|
+
id: "row-origins",
|
|
1719
|
+
breaks: [],
|
|
1720
|
+
sql: `
|
|
1721
|
+
ALTER TABLE sessions ADD COLUMN origin_store TEXT;
|
|
1722
|
+
ALTER TABLE sessions ADD COLUMN origin_seq INTEGER;
|
|
1723
|
+
CREATE UNIQUE INDEX sessions_by_origin
|
|
1724
|
+
ON sessions (origin_store, origin_seq)
|
|
1725
|
+
WHERE origin_store IS NOT NULL;
|
|
1726
|
+
|
|
1727
|
+
ALTER TABLE commits ADD COLUMN origin_store TEXT;
|
|
1728
|
+
ALTER TABLE commits ADD COLUMN origin_seq INTEGER;
|
|
1729
|
+
CREATE UNIQUE INDEX commits_by_origin
|
|
1730
|
+
ON commits (origin_store, origin_seq)
|
|
1731
|
+
WHERE origin_store IS NOT NULL;
|
|
1732
|
+
|
|
1733
|
+
ALTER TABLE findings ADD COLUMN origin_store TEXT;
|
|
1734
|
+
ALTER TABLE findings ADD COLUMN origin_seq INTEGER;
|
|
1735
|
+
CREATE UNIQUE INDEX findings_by_origin
|
|
1736
|
+
ON findings (origin_store, origin_seq)
|
|
1737
|
+
WHERE origin_store IS NOT NULL;
|
|
1738
|
+
|
|
1739
|
+
ALTER TABLE blockers ADD COLUMN origin_store TEXT;
|
|
1740
|
+
ALTER TABLE blockers ADD COLUMN origin_seq INTEGER;
|
|
1741
|
+
CREATE UNIQUE INDEX blockers_by_origin
|
|
1742
|
+
ON blockers (origin_store, origin_seq)
|
|
1743
|
+
WHERE origin_store IS NOT NULL;
|
|
1744
|
+
|
|
1745
|
+
ALTER TABLE out_of_scope_bugs ADD COLUMN origin_store TEXT;
|
|
1746
|
+
ALTER TABLE out_of_scope_bugs ADD COLUMN origin_seq INTEGER;
|
|
1747
|
+
CREATE UNIQUE INDEX out_of_scope_bugs_by_origin
|
|
1748
|
+
ON out_of_scope_bugs (origin_store, origin_seq)
|
|
1749
|
+
WHERE origin_store IS NOT NULL;
|
|
1750
|
+
|
|
1751
|
+
ALTER TABLE report_absences ADD COLUMN origin_store TEXT;
|
|
1752
|
+
ALTER TABLE report_absences ADD COLUMN origin_seq INTEGER;
|
|
1753
|
+
CREATE UNIQUE INDEX report_absences_by_origin
|
|
1754
|
+
ON report_absences (origin_store, origin_seq)
|
|
1755
|
+
WHERE origin_store IS NOT NULL;
|
|
1756
|
+
|
|
1757
|
+
ALTER TABLE task_reports ADD COLUMN origin_store TEXT;
|
|
1758
|
+
ALTER TABLE task_reports ADD COLUMN origin_seq INTEGER;
|
|
1759
|
+
CREATE UNIQUE INDEX task_reports_by_origin
|
|
1760
|
+
ON task_reports (origin_store, origin_seq)
|
|
1761
|
+
WHERE origin_store IS NOT NULL;
|
|
1762
|
+
|
|
1763
|
+
ALTER TABLE preflight ADD COLUMN origin_store TEXT;
|
|
1764
|
+
ALTER TABLE preflight ADD COLUMN origin_seq INTEGER;
|
|
1765
|
+
CREATE UNIQUE INDEX preflight_by_origin
|
|
1766
|
+
ON preflight (origin_store, origin_seq)
|
|
1767
|
+
WHERE origin_store IS NOT NULL;
|
|
1768
|
+
|
|
1769
|
+
ALTER TABLE dispatches ADD COLUMN origin_store TEXT;
|
|
1770
|
+
ALTER TABLE dispatches ADD COLUMN origin_seq INTEGER;
|
|
1771
|
+
CREATE UNIQUE INDEX dispatches_by_origin
|
|
1772
|
+
ON dispatches (origin_store, origin_seq)
|
|
1773
|
+
WHERE origin_store IS NOT NULL;
|
|
1774
|
+
|
|
1775
|
+
ALTER TABLE changes ADD COLUMN origin_store TEXT;
|
|
1776
|
+
ALTER TABLE changes ADD COLUMN origin_seq INTEGER;
|
|
1777
|
+
CREATE UNIQUE INDEX changes_by_origin
|
|
1778
|
+
ON changes (origin_store, origin_seq)
|
|
1779
|
+
WHERE origin_store IS NOT NULL;
|
|
1780
|
+
|
|
1781
|
+
ALTER TABLE skill_invocations ADD COLUMN origin_store TEXT;
|
|
1782
|
+
ALTER TABLE skill_invocations ADD COLUMN origin_seq INTEGER;
|
|
1783
|
+
CREATE UNIQUE INDEX skill_invocations_by_origin
|
|
1784
|
+
ON skill_invocations (origin_store, origin_seq)
|
|
1785
|
+
WHERE origin_store IS NOT NULL;
|
|
1786
|
+
|
|
1787
|
+
ALTER TABLE plan_ci ADD COLUMN origin_store TEXT;
|
|
1788
|
+
ALTER TABLE plan_ci ADD COLUMN origin_seq INTEGER;
|
|
1789
|
+
CREATE UNIQUE INDEX plan_ci_by_origin
|
|
1790
|
+
ON plan_ci (origin_store, origin_seq)
|
|
1791
|
+
WHERE origin_store IS NOT NULL;
|
|
1792
|
+
`
|
|
1793
|
+
},
|
|
1794
|
+
{
|
|
1795
|
+
id: "store-meta",
|
|
1796
|
+
breaks: [],
|
|
1797
|
+
sql: `
|
|
1798
|
+
CREATE TABLE store_meta (
|
|
1799
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
1800
|
+
store_id TEXT NOT NULL CHECK (store_id <> ''),
|
|
1801
|
+
project_root_commit TEXT NOT NULL CHECK (project_root_commit <> ''),
|
|
1802
|
+
project_remote TEXT CHECK (project_remote <> ''),
|
|
1803
|
+
host_id TEXT NOT NULL CHECK (host_id <> ''),
|
|
1804
|
+
store_path TEXT NOT NULL CHECK (store_path <> ''),
|
|
1805
|
+
file_dev INTEGER NOT NULL CHECK (typeof(file_dev) = 'integer'),
|
|
1806
|
+
file_ino INTEGER NOT NULL CHECK (typeof(file_ino) = 'integer'),
|
|
1807
|
+
minted_at TEXT NOT NULL CHECK (minted_at <> '')
|
|
1808
|
+
);
|
|
1809
|
+
|
|
1810
|
+
CREATE TABLE merges (
|
|
1811
|
+
seq INTEGER PRIMARY KEY,
|
|
1812
|
+
id TEXT NOT NULL UNIQUE CHECK (id <> ''),
|
|
1813
|
+
other_store TEXT CHECK (other_store <> ''),
|
|
1814
|
+
merged_at TEXT NOT NULL CHECK (merged_at <> ''),
|
|
1815
|
+
rows_added INTEGER NOT NULL CHECK (typeof(rows_added) = 'integer' AND rows_added >= 0),
|
|
1816
|
+
rows_skipped INTEGER NOT NULL CHECK (typeof(rows_skipped) = 'integer' AND rows_skipped >= 0),
|
|
1817
|
+
rows_in_conflict INTEGER NOT NULL CHECK (typeof(rows_in_conflict) = 'integer' AND rows_in_conflict >= 0)
|
|
1818
|
+
);
|
|
1819
|
+
|
|
1820
|
+
CREATE TABLE merge_conflicts (
|
|
1821
|
+
seq INTEGER PRIMARY KEY,
|
|
1822
|
+
merge_id TEXT NOT NULL CHECK (merge_id <> ''),
|
|
1823
|
+
table_name TEXT NOT NULL CHECK (table_name <> ''),
|
|
1824
|
+
local_seq INTEGER NOT NULL CHECK (typeof(local_seq) = 'integer'),
|
|
1825
|
+
field TEXT CHECK (field <> ''),
|
|
1826
|
+
incoming TEXT NOT NULL CHECK (json_valid(incoming) AND json_type(incoming) = 'object'),
|
|
1827
|
+
recorded_at TEXT NOT NULL CHECK (recorded_at <> '')
|
|
1828
|
+
);
|
|
1829
|
+
`
|
|
1830
|
+
},
|
|
1831
|
+
{
|
|
1832
|
+
id: "store-meta-generation",
|
|
1833
|
+
breaks: [],
|
|
1834
|
+
sql: `
|
|
1835
|
+
ALTER TABLE store_meta ADD COLUMN generation TEXT CHECK (generation <> '');
|
|
1836
|
+
`
|
|
1837
|
+
},
|
|
1838
|
+
{
|
|
1839
|
+
id: "session-worktree",
|
|
1840
|
+
breaks: [],
|
|
1841
|
+
sql: `
|
|
1842
|
+
ALTER TABLE sessions ADD COLUMN worktree TEXT;
|
|
1843
|
+
`
|
|
1844
|
+
},
|
|
1845
|
+
{
|
|
1846
|
+
id: "store-replaced",
|
|
1847
|
+
breaks: [],
|
|
1848
|
+
sql: `
|
|
1849
|
+
CREATE TABLE store_replaced (
|
|
1850
|
+
store_id TEXT NOT NULL PRIMARY KEY CHECK (store_id <> ''),
|
|
1851
|
+
replaced_at TEXT NOT NULL CHECK (replaced_at <> '')
|
|
1852
|
+
);
|
|
1853
|
+
`
|
|
1854
|
+
},
|
|
1855
|
+
{
|
|
1856
|
+
id: "stale-docs",
|
|
1857
|
+
breaks: [],
|
|
1858
|
+
sql: `
|
|
1859
|
+
CREATE TABLE stale_docs (
|
|
1860
|
+
seq INTEGER PRIMARY KEY,
|
|
1861
|
+
id TEXT NOT NULL UNIQUE CHECK (id <> ''),
|
|
1862
|
+
session_id TEXT NOT NULL CHECK (session_id <> ''),
|
|
1863
|
+
plan_stub TEXT,
|
|
1864
|
+
task_line TEXT NOT NULL,
|
|
1865
|
+
kind TEXT NOT NULL CHECK (kind IN ('sighting', 'unsettled')),
|
|
1866
|
+
where_at TEXT CHECK (where_at <> ''),
|
|
1867
|
+
what TEXT NOT NULL CHECK (what <> ''),
|
|
1868
|
+
outcome TEXT NOT NULL,
|
|
1869
|
+
collected_at TEXT NOT NULL,
|
|
1870
|
+
origin_store TEXT,
|
|
1871
|
+
origin_seq INTEGER
|
|
1872
|
+
);
|
|
1873
|
+
|
|
1874
|
+
CREATE UNIQUE INDEX stale_docs_by_entry
|
|
1875
|
+
ON stale_docs (session_id, kind, ifnull(where_at, ''), what);
|
|
1876
|
+
|
|
1877
|
+
CREATE UNIQUE INDEX stale_docs_by_origin
|
|
1878
|
+
ON stale_docs (origin_store, origin_seq)
|
|
1879
|
+
WHERE origin_store IS NOT NULL;
|
|
1880
|
+
`
|
|
1881
|
+
},
|
|
1882
|
+
{
|
|
1883
|
+
id: "bug-routes",
|
|
1884
|
+
breaks: [],
|
|
1885
|
+
sql: `
|
|
1886
|
+
CREATE TABLE bug_routes (
|
|
1887
|
+
seq INTEGER PRIMARY KEY,
|
|
1888
|
+
id TEXT NOT NULL UNIQUE CHECK (id <> ''),
|
|
1889
|
+
session_id TEXT NOT NULL CHECK (session_id <> ''),
|
|
1890
|
+
plan_stub TEXT,
|
|
1891
|
+
task_line TEXT NOT NULL,
|
|
1892
|
+
entry_index INTEGER NOT NULL CHECK (entry_index >= 0),
|
|
1893
|
+
what TEXT NOT NULL CHECK (what <> ''),
|
|
1894
|
+
said TEXT CHECK (said IN ('bug', 'own-fallout', 'own-gap', 'convention', 'docs', 'machine')),
|
|
1895
|
+
kind TEXT NOT NULL CHECK (kind IN ('bug', 'own-fallout', 'own-gap', 'convention', 'docs', 'machine')),
|
|
1896
|
+
route TEXT NOT NULL CHECK (route IN ('filed', 'private', 'machine', 'inherited', 'repair', 'follow-up', 'pr-body', 'docs')),
|
|
1897
|
+
collected_at TEXT NOT NULL,
|
|
1898
|
+
origin_store TEXT,
|
|
1899
|
+
origin_seq INTEGER
|
|
1900
|
+
);
|
|
1901
|
+
|
|
1902
|
+
CREATE UNIQUE INDEX bug_routes_by_entry
|
|
1903
|
+
ON bug_routes (session_id, entry_index, what);
|
|
1904
|
+
|
|
1905
|
+
CREATE UNIQUE INDEX bug_routes_by_origin
|
|
1906
|
+
ON bug_routes (origin_store, origin_seq)
|
|
1907
|
+
WHERE origin_store IS NOT NULL;
|
|
1908
|
+
`
|
|
1909
|
+
}
|
|
1910
|
+
];
|
|
1911
|
+
var SQLITE_SCHEMA_VERSION = SQLITE_MIGRATIONS.length;
|
|
1912
|
+
function legacyGate(applied) {
|
|
1913
|
+
return applied.every(({ breaks }) => breaks.length === 0) ? LEGACY_GATE_OPEN : LEGACY_GATE_CLOSED;
|
|
1914
|
+
}
|
|
1915
|
+
function migrationChecksum(migration) {
|
|
1916
|
+
return createHash("sha256").update(migration.sql, "utf8").digest("hex");
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// packages/rafa/src/effort/store/schema-plan.ts
|
|
1920
|
+
var FIX_SCHEMA_STEP = "rafa effort fix-schema --dry-run";
|
|
1921
|
+
function sqliteCatalogue(migrations) {
|
|
1922
|
+
return migrations.map(({ sql, ...spec }) => ({ ...spec, sha256: migrationChecksum({ sql }) }));
|
|
1923
|
+
}
|
|
1924
|
+
function planSchema(store, catalogue, access, caller) {
|
|
1925
|
+
const { path, userVersion, log } = store;
|
|
1926
|
+
if (log === null && (userVersion < 0 || userVersion > LEGACY_GATE_OPEN)) {
|
|
1927
|
+
return refuse("pre-log-unreleased", FIX_SCHEMA_STEP, `effort store: ${path} is at schema version` + ` ${String(userVersion)} with no migration log; no released rafa wrote that.` + " Refusing to read or write it.");
|
|
1928
|
+
}
|
|
1929
|
+
if (log !== null && userVersion !== LEGACY_GATE_OPEN && userVersion !== LEGACY_GATE_CLOSED) {
|
|
1930
|
+
return refuse("gate-mismatch", FIX_SCHEMA_STEP, `effort store: ${path} has a migration log and` + ` schema version ${String(userVersion)}; a build older than the log migrated it after the` + " log was made. Refusing to read or write it.");
|
|
1931
|
+
}
|
|
1932
|
+
const adopted = log === null ? adoptedEntries(catalogue, userVersion) : [];
|
|
1933
|
+
const held = log ?? [];
|
|
1934
|
+
const logRefusal = judgeLog(path, held, catalogue);
|
|
1935
|
+
if (logRefusal !== null)
|
|
1936
|
+
return logRefusal;
|
|
1937
|
+
const heldIds = new Set([...adopted, ...held].map(({ id }) => id));
|
|
1938
|
+
const pending = catalogue.filter(({ id }) => !heldIds.has(id));
|
|
1939
|
+
const unknown = held.filter(({ id }) => !catalogue.some((entry) => entry.id === id));
|
|
1940
|
+
const writes = access === "write" || pending.length > 0;
|
|
1941
|
+
const writersBroken = unknown.find(({ breaks }) => breaks.includes("writers"));
|
|
1942
|
+
if (writes && writersBroken !== undefined) {
|
|
1943
|
+
return refuseUnknown("unknown-breaks-writers", path, writersBroken);
|
|
1944
|
+
}
|
|
1945
|
+
const orderRefusal = judgePending(path, catalogue, heldIds, pending, caller);
|
|
1946
|
+
if (orderRefusal !== null)
|
|
1947
|
+
return orderRefusal;
|
|
1948
|
+
return {
|
|
1949
|
+
verdict: "use",
|
|
1950
|
+
adopted,
|
|
1951
|
+
pending,
|
|
1952
|
+
unknown,
|
|
1953
|
+
gate: legacyGate([...adopted, ...held, ...pending])
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
function adoptedEntries(catalogue, count) {
|
|
1957
|
+
const adopted = catalogue.slice(0, count);
|
|
1958
|
+
if (adopted.length < count) {
|
|
1959
|
+
throw new Error(`schema plan: the catalogue holds ${String(catalogue.length)} migrations,` + ` fewer than the ${String(count)} legacy entries a store at that version holds`);
|
|
1960
|
+
}
|
|
1961
|
+
return adopted;
|
|
1962
|
+
}
|
|
1963
|
+
function judgeLog(path, log, catalogue) {
|
|
1964
|
+
const byId = new Map(catalogue.map((entry) => [entry.id, entry]));
|
|
1965
|
+
for (const row of log) {
|
|
1966
|
+
const known = byId.get(row.id);
|
|
1967
|
+
if (known !== undefined && known.sha256 !== row.sha256) {
|
|
1968
|
+
return refuse("edited", FIX_SCHEMA_STEP, `effort store: ${path} recorded migration ${row.id}` + ` as sha256 ${row.sha256}, and this rafa holds it as ${known.sha256}; one was edited` + " after it ran. Refusing to read or write the store.");
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
const readersBroken = log.find(({ id, breaks }) => !byId.has(id) && breaks.some((word) => word !== "writers"));
|
|
1972
|
+
return readersBroken === undefined ? null : refuseUnknown("unknown-breaks-readers", path, readersBroken);
|
|
1973
|
+
}
|
|
1974
|
+
function judgePending(path, catalogue, heldIds, pending, caller) {
|
|
1975
|
+
const breaking = pending.filter(({ breaks }) => breaks.length > 0);
|
|
1976
|
+
for (const entry of breaking) {
|
|
1977
|
+
const later = catalogue.slice(catalogue.indexOf(entry) + 1).find(({ id }) => heldIds.has(id));
|
|
1978
|
+
if (later !== undefined) {
|
|
1979
|
+
return refuse("breaking-out-of-order", "rafa effort schema", `effort store: ${path} needs` + ` migration ${entry.id} ahead of ${later.id}, which is already applied; a migration that` + " breaks older runtimes applies only in order. Refusing to read or write the store.");
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
const [first] = breaking;
|
|
1983
|
+
if (first === undefined || caller === "migrate")
|
|
1984
|
+
return null;
|
|
1985
|
+
return refuse("breaking-pending", "rafa effort migrate --dry-run", `effort store: ${path} needs` + ` migration ${first.id}, which breaks older runtimes and is applied only by` + " 'rafa effort migrate'; refusing to read or write the store until then.");
|
|
1986
|
+
}
|
|
1987
|
+
function refuseUnknown(reason, path, row) {
|
|
1988
|
+
const ending = reason === "unknown-breaks-readers" ? "which breaks older readers; refusing to read or write it." : "which breaks older writers; refusing to write it.";
|
|
1989
|
+
return refuse(reason, `install a rafa that knows ${row.id}, or ${FIX_SCHEMA_STEP}`, "effort store:" + ` ${path} holds migration ${row.id} (applied by ${row.appliedBy} on ${dateOf(row.appliedAt)}),` + ` which this rafa does not know and ${ending}`);
|
|
1990
|
+
}
|
|
1991
|
+
function dateOf(appliedAt) {
|
|
1992
|
+
return /^\d{4}-\d{2}-\d{2}/.test(appliedAt) ? appliedAt.slice(0, 10) : appliedAt;
|
|
1993
|
+
}
|
|
1994
|
+
function refuse(reason, nextStep, text) {
|
|
1995
|
+
return { verdict: "refuse", reason, message: `${text} Next safe step: ${nextStep}`, nextStep };
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// packages/rafa/src/effort/store/bring-forward.ts
|
|
1999
|
+
var MIGRATION_LOG_TABLE = "schema_migrations";
|
|
2000
|
+
var CREATE_MIGRATION_LOG = `
|
|
2001
|
+
CREATE TABLE IF NOT EXISTS ${MIGRATION_LOG_TABLE} (
|
|
2002
|
+
seq INTEGER PRIMARY KEY,
|
|
2003
|
+
id TEXT NOT NULL UNIQUE CHECK (id <> ''),
|
|
2004
|
+
sha256 TEXT NOT NULL CHECK (length(sha256) = 64),
|
|
2005
|
+
breaks TEXT NOT NULL,
|
|
2006
|
+
applied_at TEXT NOT NULL,
|
|
2007
|
+
applied_by TEXT NOT NULL CHECK (applied_by <> '')
|
|
2008
|
+
);
|
|
2009
|
+
`;
|
|
2010
|
+
|
|
2011
|
+
class SchemaRefusedError extends Error {
|
|
2012
|
+
reason;
|
|
2013
|
+
nextStep;
|
|
2014
|
+
constructor(refusal) {
|
|
2015
|
+
super(refusal.message);
|
|
2016
|
+
this.name = "SchemaRefusedError";
|
|
2017
|
+
this.reason = refusal.reason;
|
|
2018
|
+
this.nextStep = refusal.nextStep;
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
function bringForward(db, path, access, caller, options = {}) {
|
|
2022
|
+
const migrations = options.migrations ?? SQLITE_MIGRATIONS;
|
|
2023
|
+
const catalogue = sqliteCatalogue(migrations);
|
|
2024
|
+
const first = usablePlan(readStoreSchema(db, path), catalogue, access, caller);
|
|
2025
|
+
if (!hasWork(first))
|
|
2026
|
+
return nothingWritten(db);
|
|
2027
|
+
if (options.builtAside !== true)
|
|
2028
|
+
refuseUnownedDevelopmentWrite(path, writeNames(first), options);
|
|
2029
|
+
const apply = db.transaction(() => {
|
|
2030
|
+
const store = readStoreSchema(db, path);
|
|
2031
|
+
const plan = usablePlan(store, catalogue, access, caller);
|
|
2032
|
+
if (!hasWork(plan))
|
|
2033
|
+
return nothingWritten(db);
|
|
2034
|
+
const stamp = {
|
|
2035
|
+
appliedAt: (options.now ?? (() => new Date))().toISOString(),
|
|
2036
|
+
appliedBy: options.appliedBy ?? RAFA_VERSION
|
|
2037
|
+
};
|
|
2038
|
+
db.run(CREATE_MIGRATION_LOG);
|
|
2039
|
+
for (const entry of plan.adopted)
|
|
2040
|
+
logMigration(db, entry, stamp);
|
|
2041
|
+
for (const entry of plan.pending) {
|
|
2042
|
+
db.run(bodyOf(migrations, entry.id));
|
|
2043
|
+
logMigration(db, entry, stamp);
|
|
2044
|
+
}
|
|
2045
|
+
db.run(`PRAGMA user_version = ${String(plan.gate)}`);
|
|
2046
|
+
return {
|
|
2047
|
+
adopted: plan.adopted.map(({ id }) => id),
|
|
2048
|
+
applied: plan.pending.map(({ id }) => id),
|
|
2049
|
+
userVersion: plan.gate
|
|
2050
|
+
};
|
|
2051
|
+
});
|
|
2052
|
+
return apply.immediate();
|
|
2053
|
+
}
|
|
2054
|
+
function readStoreSchema(db, path) {
|
|
2055
|
+
return { path, userVersion: readUserVersion(db), log: readMigrationLog(db, path) };
|
|
2056
|
+
}
|
|
2057
|
+
function usablePlan(store, catalogue, access, caller) {
|
|
2058
|
+
const plan = planSchema(store, catalogue, access, caller);
|
|
2059
|
+
if (plan.verdict === "refuse")
|
|
2060
|
+
throw new SchemaRefusedError(plan);
|
|
2061
|
+
return plan;
|
|
2062
|
+
}
|
|
2063
|
+
function hasWork(plan) {
|
|
2064
|
+
return plan.adopted.length > 0 || plan.pending.length > 0;
|
|
2065
|
+
}
|
|
2066
|
+
function writeNames(plan) {
|
|
2067
|
+
const adoption = plan.adopted.length > 0 ? [ADOPTION_NAME] : [];
|
|
2068
|
+
return [...adoption, ...plan.pending.map(({ id }) => id)];
|
|
2069
|
+
}
|
|
2070
|
+
function nothingWritten(db) {
|
|
2071
|
+
return { adopted: [], applied: [], userVersion: readUserVersion(db) };
|
|
2072
|
+
}
|
|
2073
|
+
function readUserVersion(db) {
|
|
2074
|
+
const row = db.query("PRAGMA user_version").get();
|
|
2075
|
+
return row?.user_version ?? 0;
|
|
2076
|
+
}
|
|
2077
|
+
function readMigrationLog(db, path) {
|
|
2078
|
+
const table = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(MIGRATION_LOG_TABLE);
|
|
2079
|
+
if (table === null)
|
|
2080
|
+
return null;
|
|
2081
|
+
return db.query(`SELECT id, sha256, breaks, applied_at, applied_by FROM ${MIGRATION_LOG_TABLE} ORDER BY seq`).all().map((row) => ({
|
|
2082
|
+
id: row.id,
|
|
2083
|
+
sha256: row.sha256,
|
|
2084
|
+
breaks: parseBreaks(row.breaks, row.id, path),
|
|
2085
|
+
appliedAt: row.applied_at,
|
|
2086
|
+
appliedBy: row.applied_by
|
|
2087
|
+
}));
|
|
2088
|
+
}
|
|
2089
|
+
function parseBreaks(text, id, path) {
|
|
2090
|
+
const refused = new Error(`effort store: ${path} logs migration ${id} with breaks ${text},` + " which is not a JSON array of words; refusing to read or write it");
|
|
2091
|
+
let parsed;
|
|
2092
|
+
try {
|
|
2093
|
+
parsed = JSON.parse(text);
|
|
2094
|
+
} catch (cause) {
|
|
2095
|
+
throw new Error(refused.message, { cause });
|
|
2096
|
+
}
|
|
2097
|
+
if (!Array.isArray(parsed) || !parsed.every((word) => typeof word === "string"))
|
|
2098
|
+
throw refused;
|
|
2099
|
+
return parsed;
|
|
2100
|
+
}
|
|
2101
|
+
function bodyOf(migrations, id) {
|
|
2102
|
+
const migration = migrations.find((candidate) => candidate.id === id);
|
|
2103
|
+
if (migration === undefined)
|
|
2104
|
+
throw new Error(`bring forward: no migration ${id} in the catalogue it planned from`);
|
|
2105
|
+
return migration.sql;
|
|
2106
|
+
}
|
|
2107
|
+
function logMigration(db, entry, stamp) {
|
|
2108
|
+
db.run(`INSERT INTO ${MIGRATION_LOG_TABLE} (id, sha256, breaks, applied_at, applied_by) VALUES (?, ?, ?, ?, ?)`, [entry.id, entry.sha256, JSON.stringify(entry.breaks), stamp.appliedAt, stamp.appliedBy]);
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
// packages/rafa/src/effort/store/origins.ts
|
|
2112
|
+
var ACROSS_STORES_ORDER = "collected_at, origin_store, origin_seq, seq";
|
|
2113
|
+
var STAMPED_COLUMNS = "seq, origin_store, origin_seq";
|
|
2114
|
+
var STORE_ID = "(SELECT store_id FROM store_meta WHERE id = 1)";
|
|
2115
|
+
function stampedValues(table) {
|
|
2116
|
+
const nextSeq = `(SELECT COALESCE(MAX(seq), 0) + 1 FROM ${table})`;
|
|
2117
|
+
return `${nextSeq}, ${STORE_ID}, CASE WHEN ${STORE_ID} IS NULL THEN NULL ELSE ${nextSeq} END`;
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
// packages/rafa/src/effort/store/settings.ts
|
|
2121
|
+
var DEFAULT_STORE_SETTINGS = Object.freeze({
|
|
2122
|
+
busyTimeoutMs: CORE_SECTION.defaults.effortBusyTimeoutMs
|
|
2123
|
+
});
|
|
2124
|
+
var active2 = DEFAULT_STORE_SETTINGS;
|
|
2125
|
+
function setActiveStoreSettings(settings) {
|
|
2126
|
+
if (settings === null) {
|
|
2127
|
+
active2 = DEFAULT_STORE_SETTINGS;
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
2130
|
+
if (!isBusyTimeoutMs(settings.busyTimeoutMs)) {
|
|
2131
|
+
throw new RangeError(`effort store: busy timeout ${String(settings.busyTimeoutMs)} is not a whole number ` + `of milliseconds from ${String(BUSY_TIMEOUT_MIN_MS)} to ${String(BUSY_TIMEOUT_MAX_MS)}`);
|
|
2132
|
+
}
|
|
2133
|
+
active2 = Object.freeze({ busyTimeoutMs: settings.busyTimeoutMs });
|
|
2134
|
+
}
|
|
2135
|
+
function activeStoreSettings() {
|
|
2136
|
+
return active2;
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
// packages/rafa/src/effort/store/store-meta.ts
|
|
2140
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2141
|
+
import { dirname as dirname6 } from "path";
|
|
2142
|
+
import { Database } from "bun:sqlite";
|
|
2143
|
+
|
|
2144
|
+
// packages/rafa/src/effort/store/store-generation.ts
|
|
2145
|
+
import { randomUUID } from "crypto";
|
|
2146
|
+
import { readFileSync as readFileSync4, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2147
|
+
var STORE_GENERATION_SUFFIX = ".generation";
|
|
2148
|
+
function storeGenerationPath(storePath) {
|
|
2149
|
+
return `${storePath}${STORE_GENERATION_SUFFIX}`;
|
|
2150
|
+
}
|
|
2151
|
+
function isMissingFile(error) {
|
|
2152
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
2153
|
+
}
|
|
2154
|
+
function readStoreGeneration(storePath) {
|
|
2155
|
+
let text;
|
|
2156
|
+
try {
|
|
2157
|
+
text = readFileSync4(storeGenerationPath(storePath), "utf8");
|
|
2158
|
+
} catch (error) {
|
|
2159
|
+
if (isMissingFile(error))
|
|
2160
|
+
return null;
|
|
2161
|
+
throw error;
|
|
2162
|
+
}
|
|
2163
|
+
const generation = text.trim();
|
|
2164
|
+
return generation === "" ? null : generation;
|
|
2165
|
+
}
|
|
2166
|
+
function assertWritableGeneration(generation) {
|
|
2167
|
+
if (generation === "" || generation !== generation.trim() || /[\r\n]/.test(generation)) {
|
|
2168
|
+
throw new Error(`store generation must be non-empty, unpadded and on one line, got ${JSON.stringify(generation)}`);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
function writeStoreGeneration(storePath, generation) {
|
|
2172
|
+
assertWritableGeneration(generation);
|
|
2173
|
+
const file = storeGenerationPath(storePath);
|
|
2174
|
+
const temporary = `${file}.${String(process.pid)}.${randomUUID()}.tmp`;
|
|
2175
|
+
try {
|
|
2176
|
+
writeFileSync2(temporary, `${generation}
|
|
2177
|
+
`, { encoding: "utf8", flag: "wx" });
|
|
2178
|
+
renameSync2(temporary, file);
|
|
2179
|
+
} catch (error) {
|
|
2180
|
+
rmSync2(temporary, { force: true });
|
|
2181
|
+
throw error;
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
// packages/rafa/src/effort/store/store-identity.ts
|
|
2186
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
2187
|
+
import { createHmac } from "crypto";
|
|
2188
|
+
import { readFileSync as readFileSync5, realpathSync as realpathSync3, statSync } from "fs";
|
|
2189
|
+
import { hostname, platform as currentPlatform } from "os";
|
|
2190
|
+
|
|
2191
|
+
// packages/rafa/src/schema/project-id.ts
|
|
2192
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
2193
|
+
var PROJECT_ID_LENGTH = 12;
|
|
2194
|
+
var PROJECT_ID_PATTERN = new RegExp(`^[0-9a-f]{${PROJECT_ID_LENGTH}}$`);
|
|
2195
|
+
function hasColonAfterAt(url) {
|
|
2196
|
+
const at = url.indexOf("@");
|
|
2197
|
+
return at !== -1 && url.lastIndexOf(":") > at;
|
|
2198
|
+
}
|
|
2199
|
+
function normalizeRemote(url) {
|
|
2200
|
+
const stripped = url.replace(/:\/\/[^@]+@/, "://");
|
|
2201
|
+
const network = !stripped.startsWith("file://") && (stripped.includes("://") || hasColonAfterAt(stripped));
|
|
2202
|
+
const trimmed = stripped.replace(/^[A-Za-z][A-Za-z0-9+.-]*:\/\//, "").replace(/^[^@/:]+@([^:/]+):/, "$1/").replace(/\.git\/?$/, "").replace(/\/+$/, "");
|
|
2203
|
+
return network ? trimmed.toLowerCase() : trimmed;
|
|
2204
|
+
}
|
|
2205
|
+
function gitRemoteUrl(dir) {
|
|
2206
|
+
const result = spawnSync3("git", ["remote", "get-url", "origin"], {
|
|
2207
|
+
cwd: dir,
|
|
2208
|
+
encoding: "utf8",
|
|
2209
|
+
env: { ...process.env, LC_ALL: "C" }
|
|
2210
|
+
});
|
|
2211
|
+
if (result.error !== undefined || result.status !== 0)
|
|
2212
|
+
return null;
|
|
2213
|
+
const url = result.stdout.trim();
|
|
2214
|
+
return url === "" ? null : url;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
// packages/rafa/src/effort/store/store-identity.ts
|
|
2218
|
+
var HOST_ID_KEY = "rafa effort store host id";
|
|
2219
|
+
var MACHINE_ID_PATH = "/etc/machine-id";
|
|
2220
|
+
var IOREG_PATH = "/usr/sbin/ioreg";
|
|
2221
|
+
var IOREG_ARGS = ["-rd1", "-c", "IOPlatformExpertDevice"];
|
|
2222
|
+
function hostIdFrom(source, value) {
|
|
2223
|
+
return createHmac("sha256", HOST_ID_KEY).update(`${source}:${value}`).digest("hex");
|
|
2224
|
+
}
|
|
2225
|
+
function readMachineIdFile() {
|
|
2226
|
+
try {
|
|
2227
|
+
const value = readFileSync5(MACHINE_ID_PATH, "utf8").trim();
|
|
2228
|
+
return value === "" ? null : value;
|
|
2229
|
+
} catch {
|
|
2230
|
+
return null;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
function parseIoregPlatformUuid(output) {
|
|
2234
|
+
const match = /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(output);
|
|
2235
|
+
return match?.[1] ?? null;
|
|
2236
|
+
}
|
|
2237
|
+
function readIoregPlatformUuid() {
|
|
2238
|
+
const result = spawnSync4(IOREG_PATH, [...IOREG_ARGS], { encoding: "utf8" });
|
|
2239
|
+
if (result.error !== undefined || result.status !== 0)
|
|
2240
|
+
return null;
|
|
2241
|
+
return parseIoregPlatformUuid(result.stdout);
|
|
2242
|
+
}
|
|
2243
|
+
function readHostId(seams = {}) {
|
|
2244
|
+
const platform = seams.platform ?? currentPlatform();
|
|
2245
|
+
if (platform === "linux") {
|
|
2246
|
+
const machineId = (seams.readMachineId ?? readMachineIdFile)();
|
|
2247
|
+
if (machineId !== null)
|
|
2248
|
+
return hostIdFrom("machine-id", machineId);
|
|
2249
|
+
}
|
|
2250
|
+
if (platform === "darwin") {
|
|
2251
|
+
const platformUuid = (seams.readPlatformUuid ?? readIoregPlatformUuid)();
|
|
2252
|
+
if (platformUuid !== null)
|
|
2253
|
+
return hostIdFrom("platform-uuid", platformUuid);
|
|
2254
|
+
}
|
|
2255
|
+
return hostIdFrom("hostname", (seams.readHostname ?? hostname)());
|
|
2256
|
+
}
|
|
2257
|
+
function readStoreFileFacts(path) {
|
|
2258
|
+
const storePath = realpathSync3(path);
|
|
2259
|
+
const stats = statSync(storePath, { bigint: true });
|
|
2260
|
+
return { storePath, fileDev: stats.dev, fileIno: stats.ino };
|
|
2261
|
+
}
|
|
2262
|
+
function observeStore(path, readHost = readHostId) {
|
|
2263
|
+
const fileFacts = readStoreFileFacts(path);
|
|
2264
|
+
return { hostId: readHost(), ...fileFacts, generation: readStoreGeneration(fileFacts.storePath) };
|
|
2265
|
+
}
|
|
2266
|
+
function staleGeneration(recorded, facts) {
|
|
2267
|
+
return recorded.generation !== null && recorded.generation !== facts.generation;
|
|
2268
|
+
}
|
|
2269
|
+
function movedFacts(recorded, facts) {
|
|
2270
|
+
const sameFile = BigInt(recorded.fileDev) === facts.fileDev && BigInt(recorded.fileIno) === facts.fileIno;
|
|
2271
|
+
const moved = [
|
|
2272
|
+
["host", recorded.hostId !== facts.hostId],
|
|
2273
|
+
["path", recorded.storePath !== facts.storePath],
|
|
2274
|
+
["file", !sameFile],
|
|
2275
|
+
["generation", staleGeneration(recorded, facts)]
|
|
2276
|
+
];
|
|
2277
|
+
return moved.filter(([, differs]) => differs).map(([reason]) => reason);
|
|
2278
|
+
}
|
|
2279
|
+
function decideStoreIdentity(access, recorded, observe) {
|
|
2280
|
+
if (access === "read")
|
|
2281
|
+
return { action: "none" };
|
|
2282
|
+
const facts = observe();
|
|
2283
|
+
if (recorded === null)
|
|
2284
|
+
return { action: "mint", reasons: ["unminted"], facts };
|
|
2285
|
+
const reasons = movedFacts(recorded, facts);
|
|
2286
|
+
return reasons.length === 0 ? { action: "keep", storeId: recorded.storeId } : { action: "mint", reasons, facts };
|
|
2287
|
+
}
|
|
2288
|
+
function smallestRootCommit(git) {
|
|
2289
|
+
const result = git(["rev-list", "--max-parents=0", "HEAD"]);
|
|
2290
|
+
if (!result.ok)
|
|
2291
|
+
return null;
|
|
2292
|
+
const roots = result.stdout.split(`
|
|
2293
|
+
`).map((line) => line.trim()).filter((line) => line !== "");
|
|
2294
|
+
return [...roots].sort()[0] ?? null;
|
|
2295
|
+
}
|
|
2296
|
+
function normalisedRemote(git) {
|
|
2297
|
+
const result = git(["remote", "get-url", "origin"]);
|
|
2298
|
+
if (!result.ok)
|
|
2299
|
+
return null;
|
|
2300
|
+
const remote = normalizeRemote(result.stdout.trim());
|
|
2301
|
+
return remote === "" ? null : remote;
|
|
2302
|
+
}
|
|
2303
|
+
function readProjectIdentity(dir, git = createGitRunner(dir)) {
|
|
2304
|
+
return { rootCommit: smallestRootCommit(git), remote: normalisedRemote(git) };
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
// packages/rafa/src/effort/store/store-replaced.ts
|
|
2308
|
+
var STORE_REPLACED_TABLE = "store_replaced";
|
|
2309
|
+
function hasStoreReplacedTable(db) {
|
|
2310
|
+
return db.query("SELECT count(*) AS n FROM sqlite_master WHERE type = 'table' AND name = ?").get(STORE_REPLACED_TABLE)?.n === 1;
|
|
2311
|
+
}
|
|
2312
|
+
function readReplacedStoreIds(db) {
|
|
2313
|
+
if (!hasStoreReplacedTable(db))
|
|
2314
|
+
return [];
|
|
2315
|
+
return db.query("SELECT store_id, replaced_at FROM store_replaced ORDER BY replaced_at, store_id").all().map((row) => ({ storeId: row.store_id, replacedAt: row.replaced_at }));
|
|
2316
|
+
}
|
|
2317
|
+
function recordReplacedStoreId(db, storeId, replacedAt) {
|
|
2318
|
+
if (storeId === "")
|
|
2319
|
+
throw new Error("replaced store id must be non-empty");
|
|
2320
|
+
if (Number.isNaN(replacedAt.getTime())) {
|
|
2321
|
+
throw new Error(`replaced time of store ${storeId} is not a valid date`);
|
|
2322
|
+
}
|
|
2323
|
+
db.query("INSERT INTO store_replaced (store_id, replaced_at) VALUES (?, ?) ON CONFLICT (store_id) DO NOTHING").run(storeId, replacedAt.toISOString());
|
|
2324
|
+
}
|
|
2325
|
+
function clearReplacedStoreIds(db) {
|
|
2326
|
+
if (!hasStoreReplacedTable(db))
|
|
2327
|
+
return;
|
|
2328
|
+
db.run("DELETE FROM store_replaced");
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
// packages/rafa/src/effort/store/store-meta.ts
|
|
2332
|
+
var STORED_INTEGER_BITS = 64;
|
|
2333
|
+
var COPY_REASONS = ["host", "path"];
|
|
2334
|
+
function toStoredInteger(value) {
|
|
2335
|
+
return BigInt.asIntN(STORED_INTEGER_BITS, value);
|
|
2336
|
+
}
|
|
2337
|
+
function fromStoredInteger(text) {
|
|
2338
|
+
return BigInt.asUintN(STORED_INTEGER_BITS, BigInt(text));
|
|
2339
|
+
}
|
|
2340
|
+
function hasGenerationColumn(db) {
|
|
2341
|
+
return db.query("SELECT count(*) AS n FROM pragma_table_info('store_meta') WHERE name = 'generation'").get()?.n === 1;
|
|
2342
|
+
}
|
|
2343
|
+
function readStoreMeta(db) {
|
|
2344
|
+
const generation = hasGenerationColumn(db) ? "generation" : "NULL AS generation";
|
|
2345
|
+
const row = db.query("SELECT store_id, project_root_commit, project_remote, host_id, store_path," + " CAST(file_dev AS TEXT) AS file_dev, CAST(file_ino AS TEXT) AS file_ino, minted_at," + ` ${generation} FROM store_meta WHERE id = 1`).get();
|
|
2346
|
+
if (row === null)
|
|
2347
|
+
return null;
|
|
2348
|
+
return {
|
|
2349
|
+
storeId: row.store_id,
|
|
2350
|
+
projectRootCommit: row.project_root_commit,
|
|
2351
|
+
projectRemote: row.project_remote,
|
|
2352
|
+
hostId: row.host_id,
|
|
2353
|
+
storePath: row.store_path,
|
|
2354
|
+
fileDev: fromStoredInteger(row.file_dev),
|
|
2355
|
+
fileIno: fromStoredInteger(row.file_ino),
|
|
2356
|
+
mintedAt: row.minted_at,
|
|
2357
|
+
generation: row.generation
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
2360
|
+
function projectFor(storePath, recorded, seams) {
|
|
2361
|
+
const read = (seams.readProject ?? readProjectIdentity)(dirname6(storePath));
|
|
2362
|
+
if (read.rootCommit !== null)
|
|
2363
|
+
return { rootCommit: read.rootCommit, remote: read.remote };
|
|
2364
|
+
if (recorded === null)
|
|
2365
|
+
return null;
|
|
2366
|
+
return { rootCommit: recorded.projectRootCommit, remote: recorded.projectRemote };
|
|
2367
|
+
}
|
|
2368
|
+
function writeStoreMeta(db, facts, project, generation, mintedAt, seams) {
|
|
2369
|
+
const storeId = (seams.newStoreId ?? randomUUID2)();
|
|
2370
|
+
db.query("INSERT OR REPLACE INTO store_meta (id, store_id, project_root_commit, project_remote," + " host_id, store_path, file_dev, file_ino, minted_at, generation) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(storeId, project.rootCommit, project.remote, facts.hostId, facts.storePath, toStoredInteger(facts.fileDev), toStoredInteger(facts.fileIno), mintedAt.toISOString(), generation);
|
|
2371
|
+
return storeId;
|
|
2372
|
+
}
|
|
2373
|
+
function settleReplacedIds(db, outgoing, reasons, mintedAt) {
|
|
2374
|
+
if (reasons.some((reason) => COPY_REASONS.includes(reason))) {
|
|
2375
|
+
clearReplacedStoreIds(db);
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2378
|
+
if (outgoing === null)
|
|
2379
|
+
return;
|
|
2380
|
+
recordReplacedStoreId(db, outgoing.storeId, mintedAt);
|
|
2381
|
+
}
|
|
2382
|
+
function settleUnderLock(db, observe, prefetched, seams) {
|
|
2383
|
+
const current = readStoreMeta(db);
|
|
2384
|
+
const facts = observe();
|
|
2385
|
+
const decision = decideStoreIdentity("write", current, () => facts);
|
|
2386
|
+
if (decision.action === "none")
|
|
2387
|
+
return { action: "none" };
|
|
2388
|
+
const generation = (seams.newGeneration ?? randomUUID2)();
|
|
2389
|
+
if (decision.action === "keep") {
|
|
2390
|
+
db.query("UPDATE store_meta SET generation = ? WHERE id = 1").run(generation);
|
|
2391
|
+
writeStoreGeneration(facts.storePath, generation);
|
|
2392
|
+
return { action: "keep", storeId: decision.storeId };
|
|
2393
|
+
}
|
|
2394
|
+
const project = prefetched ?? projectFor(facts.storePath, current, seams);
|
|
2395
|
+
if (project === null)
|
|
2396
|
+
return { action: "no-project", reasons: decision.reasons };
|
|
2397
|
+
const mintedAt = seams.now?.() ?? new Date;
|
|
2398
|
+
settleReplacedIds(db, current, decision.reasons, mintedAt);
|
|
2399
|
+
const storeId = writeStoreMeta(db, facts, project, generation, mintedAt, seams);
|
|
2400
|
+
writeStoreGeneration(facts.storePath, generation);
|
|
2401
|
+
return { action: "mint", storeId, reasons: decision.reasons };
|
|
2402
|
+
}
|
|
2403
|
+
function settleStoreIdentity(db, path, access, seams = {}) {
|
|
2404
|
+
if (access === "read")
|
|
2405
|
+
return { action: "none" };
|
|
2406
|
+
const observe = () => observeStore(path, seams.readHostId ?? readHostId);
|
|
2407
|
+
const recorded = readStoreMeta(db);
|
|
2408
|
+
const first = decideStoreIdentity(access, recorded, observe);
|
|
2409
|
+
const prefetched = first.action === "mint" ? projectFor(first.facts.storePath, recorded, seams) : undefined;
|
|
2410
|
+
if (first.action === "mint" && prefetched === null)
|
|
2411
|
+
return { action: "no-project", reasons: first.reasons };
|
|
2412
|
+
const underLock = db.transaction(() => settleUnderLock(db, observe, prefetched ?? undefined, seams));
|
|
2413
|
+
return underLock.immediate();
|
|
2414
|
+
}
|
|
2415
|
+
function hasStoreMetaTable(db) {
|
|
2416
|
+
return db.query("SELECT count(*) AS n FROM sqlite_master WHERE type = 'table' AND name = 'store_meta'").get()?.n === 1;
|
|
2417
|
+
}
|
|
2418
|
+
function decideLiveWrite(livePath, readHost) {
|
|
2419
|
+
const live = new Database(livePath, { readonly: true });
|
|
2420
|
+
let recorded;
|
|
2421
|
+
try {
|
|
2422
|
+
if (!hasStoreMetaTable(live))
|
|
2423
|
+
return { action: "no-table" };
|
|
2424
|
+
recorded = readStoreMeta(live);
|
|
2425
|
+
} finally {
|
|
2426
|
+
live.close();
|
|
2427
|
+
}
|
|
2428
|
+
if (recorded === null)
|
|
2429
|
+
return { action: "no-row" };
|
|
2430
|
+
const facts = observeStore(livePath, readHost);
|
|
2431
|
+
const decision = decideStoreIdentity("write", recorded, () => facts);
|
|
2432
|
+
if (decision.action === "keep")
|
|
2433
|
+
return { action: "keep", storeId: decision.storeId };
|
|
2434
|
+
const reasons = decision.action === "mint" ? decision.reasons : [];
|
|
2435
|
+
return { action: "mint", reasons };
|
|
2436
|
+
}
|
|
2437
|
+
function holdsRowOf(db, storeId) {
|
|
2438
|
+
return hasStoreMetaTable(db) && readStoreMeta(db)?.storeId === storeId;
|
|
2439
|
+
}
|
|
2440
|
+
function spoilGeneration(db, newGeneration) {
|
|
2441
|
+
if (!hasStoreMetaTable(db) || !hasGenerationColumn(db))
|
|
2442
|
+
return null;
|
|
2443
|
+
const rows = db.query("SELECT count(*) AS n FROM store_meta WHERE id = 1").get()?.n;
|
|
2444
|
+
if (rows !== 1)
|
|
2445
|
+
return null;
|
|
2446
|
+
const generation = newGeneration();
|
|
2447
|
+
db.query("UPDATE store_meta SET generation = ? WHERE id = 1").run(generation);
|
|
2448
|
+
return generation;
|
|
2449
|
+
}
|
|
2450
|
+
function writeCarried(db, storeId, parallel, newGeneration) {
|
|
2451
|
+
const dev = toStoredInteger(parallel.fileDev);
|
|
2452
|
+
const ino = toStoredInteger(parallel.fileIno);
|
|
2453
|
+
if (hasGenerationColumn(db)) {
|
|
2454
|
+
const generation = newGeneration();
|
|
2455
|
+
db.query("UPDATE store_meta SET file_dev = ?, file_ino = ?, generation = ? WHERE id = 1").run(dev, ino, generation);
|
|
2456
|
+
return { action: "carried", storeId, generation };
|
|
2457
|
+
}
|
|
2458
|
+
db.query("UPDATE store_meta SET file_dev = ?, file_ino = ? WHERE id = 1").run(dev, ino);
|
|
2459
|
+
return { action: "carried", storeId, generation: null };
|
|
2460
|
+
}
|
|
2461
|
+
function carryStoreIdentity(livePath, parallelPath, seams = {}) {
|
|
2462
|
+
const parallel = readStoreFileFacts(parallelPath);
|
|
2463
|
+
const live = decideLiveWrite(livePath, seams.readHostId ?? readHostId);
|
|
2464
|
+
const newGeneration = seams.newGeneration ?? randomUUID2;
|
|
2465
|
+
const db = new Database(parallelPath, { readwrite: true });
|
|
2466
|
+
try {
|
|
2467
|
+
if (live.action === "keep" && holdsRowOf(db, live.storeId)) {
|
|
2468
|
+
return writeCarried(db, live.storeId, parallel, newGeneration);
|
|
2469
|
+
}
|
|
2470
|
+
const spoiled = spoilGeneration(db, newGeneration);
|
|
2471
|
+
return live.action === "keep" ? { action: "other-row", spoiled } : { ...live, spoiled };
|
|
2472
|
+
} finally {
|
|
2473
|
+
db.close();
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
// packages/rafa/src/effort/store/sqlite.ts
|
|
2478
|
+
var SQLITE_STORE_FILE_NAME = "effort.sqlite";
|
|
2479
|
+
function sqliteStorePath(repoRoot) {
|
|
2480
|
+
return join5(effortStoreDir(repoRoot), SQLITE_STORE_FILE_NAME);
|
|
2481
|
+
}
|
|
2482
|
+
var KIND_TABLES = {
|
|
2483
|
+
sessions: {
|
|
2484
|
+
name: "sessions",
|
|
2485
|
+
keyColumn: "session_id",
|
|
2486
|
+
fieldColumns: [{ column: "worktree", value: (row) => row.worktree ?? null }]
|
|
2487
|
+
},
|
|
2488
|
+
commits: { name: "commits", keyColumn: "sha", fieldColumns: [] }
|
|
2489
|
+
};
|
|
2490
|
+
var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
2491
|
+
function hasKey(entry) {
|
|
2492
|
+
return entry.key !== null;
|
|
2493
|
+
}
|
|
2494
|
+
function refused(index, length, reason) {
|
|
2495
|
+
return new Error(`effort store: row ${index} of ${length} ${reason}; batch refused`);
|
|
2496
|
+
}
|
|
2497
|
+
function batchEntries(kind, rows) {
|
|
2498
|
+
const keyOf = EFFORT_KEY_PROJECTIONS[kind];
|
|
2499
|
+
const fieldColumns = KIND_TABLES[kind].fieldColumns;
|
|
2500
|
+
const projected = rows.map((row) => ({ key: keyOf(row), row }));
|
|
2501
|
+
const keyed = projected.filter(hasKey);
|
|
2502
|
+
if (keyed.length < projected.length) {
|
|
2503
|
+
const keylessAt = projected.findIndex((entry) => !hasKey(entry));
|
|
2504
|
+
throw refused(keylessAt, rows.length, "carries no key");
|
|
2505
|
+
}
|
|
2506
|
+
const unholdableAt = keyed.findIndex(({ key }) => LONE_SURROGATE.test(key));
|
|
2507
|
+
if (unholdableAt !== -1) {
|
|
2508
|
+
const reason = "carries a key with a lone UTF-16 surrogate," + " which SQLite text cannot hold";
|
|
2509
|
+
throw refused(unholdableAt, rows.length, reason);
|
|
2510
|
+
}
|
|
2511
|
+
return keyed.map(({ key, row }) => ({
|
|
2512
|
+
key,
|
|
2513
|
+
body: JSON.stringify(row),
|
|
2514
|
+
fields: fieldColumns.map(({ value }) => value(row))
|
|
2515
|
+
}));
|
|
2516
|
+
}
|
|
2517
|
+
function withSqliteStore(path, access, create, use, identity = {}) {
|
|
2518
|
+
guardTestProcess(path);
|
|
2519
|
+
if (create)
|
|
2520
|
+
mkdirSync3(dirname7(path), { recursive: true });
|
|
2521
|
+
const db = new Database2(path, { readwrite: true, create });
|
|
2522
|
+
try {
|
|
2523
|
+
db.run(`PRAGMA busy_timeout = ${String(activeStoreSettings().busyTimeoutMs)}`);
|
|
2524
|
+
bringForward(db, path, access, "open");
|
|
2525
|
+
settleStoreIdentity(db, path, access, identity);
|
|
2526
|
+
return use(db);
|
|
2527
|
+
} finally {
|
|
2528
|
+
db.close();
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
function insertBatch(db, table, entries) {
|
|
2532
|
+
const fieldNames = table.fieldColumns.map(({ column }) => column);
|
|
2533
|
+
const columns = [table.keyColumn, "row_json", ...fieldNames].join(", ");
|
|
2534
|
+
const placeholders = ["?", "?", ...fieldNames.map(() => "?")].join(", ");
|
|
2535
|
+
const insert = db.query(`INSERT INTO ${table.name} (${columns}, ${STAMPED_COLUMNS})` + ` VALUES (${placeholders}, ${stampedValues(table.name)})` + ` ON CONFLICT (${table.keyColumn}) DO NOTHING`);
|
|
2536
|
+
const insertAll = db.transaction(() => entries.reduce((appended, { key, body, fields }) => appended + insert.run(key, body, ...fields).changes, 0));
|
|
2537
|
+
return insertAll.immediate();
|
|
2538
|
+
}
|
|
2539
|
+
function writeSqliteStore(path, rowCount, empty, insert) {
|
|
2540
|
+
const exists = existsSync4(path);
|
|
2541
|
+
if (rowCount > 0)
|
|
2542
|
+
return withSqliteStore(path, "write", !exists, insert);
|
|
2543
|
+
if (exists)
|
|
2544
|
+
withSqliteStore(path, "write", false, () => {
|
|
2545
|
+
return;
|
|
2546
|
+
});
|
|
2547
|
+
return empty;
|
|
2548
|
+
}
|
|
2549
|
+
function appendKind(path, kind, rows) {
|
|
2550
|
+
const entries = batchEntries(kind, rows);
|
|
2551
|
+
const table = KIND_TABLES[kind];
|
|
2552
|
+
const appended = writeSqliteStore(path, entries.length, 0, (db) => insertBatch(db, table, entries));
|
|
2553
|
+
return { path, appended, skipped: entries.length - appended };
|
|
2554
|
+
}
|
|
2555
|
+
function keysOfKind(path, kind) {
|
|
2556
|
+
if (!existsSync4(path))
|
|
2557
|
+
return new Set;
|
|
2558
|
+
const { name, keyColumn } = KIND_TABLES[kind];
|
|
2559
|
+
const keys = withSqliteStore(path, "read", false, (db) => db.query(`SELECT ${keyColumn} AS key FROM ${name} ORDER BY seq`).all());
|
|
2560
|
+
return new Set(keys.map(({ key }) => key));
|
|
2561
|
+
}
|
|
2562
|
+
function parsedObject(body) {
|
|
2563
|
+
try {
|
|
2564
|
+
const value = JSON.parse(body);
|
|
2565
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
2566
|
+
} catch {
|
|
2567
|
+
return null;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
function rowOf(body, where) {
|
|
2571
|
+
const value = parsedObject(body);
|
|
2572
|
+
if (value === null) {
|
|
2573
|
+
throw new Error(`effort store: ${where} does not hold a JSON object;` + " no append writes one");
|
|
2574
|
+
}
|
|
2575
|
+
return value;
|
|
2576
|
+
}
|
|
2577
|
+
function readKind(path, kind) {
|
|
2578
|
+
if (!existsSync4(path))
|
|
2579
|
+
return [];
|
|
2580
|
+
const { name } = KIND_TABLES[kind];
|
|
2581
|
+
const stored = withSqliteStore(path, "read", false, (db) => db.query(`SELECT seq, row_json AS body FROM ${name} ORDER BY seq`).all());
|
|
2582
|
+
return stored.map(({ seq, body }) => rowOf(body, `${path}: ${name} row ${seq}`));
|
|
2583
|
+
}
|
|
2584
|
+
function openSqliteStore(repoRoot) {
|
|
2585
|
+
const path = sqliteStorePath(repoRoot);
|
|
2586
|
+
return {
|
|
2587
|
+
append: (kind, rows) => appendKind(path, kind, rows),
|
|
2588
|
+
keys: (kind) => keysOfKind(path, kind),
|
|
2589
|
+
read: (kind) => readKind(path, kind),
|
|
2590
|
+
path: () => path
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
// packages/rafa/src/effort/store/trusted-permissions.ts
|
|
2595
|
+
var TRUSTED_PERMISSIONS = ["admin", "maintain", "write"];
|
|
2596
|
+
|
|
2597
|
+
export { PORT_VERSIONS, createAdapterRegistry, registerAdapters, restoreAdapters, activeAdapterRegistry, CORE_ADAPTER_REGISTRY, effortStoreFileName, readStoreRows, collectedKeys, appendRows, EFFORT_DIR_VARIABLE, effortStoreDir, guardTestProcess, EFFORT_KEY_PROJECTIONS, SPENDS_GLYPH, CommandExit, commandSpelling, createLinePrompter, lazyPrompter, version, RAFA_VERSION, isRecord, asPlace, asHopRecord, SessionRecordError, isSessionId, sessionSteps, sessionDecisions, SessionConflictError, runsDir, sessionFilePath, errorCode, isPidAlive, readSessions, readSession, samePlan, beginSession, updateSession, claudeArgs, interruptClaudeSessions, spawnClaude, runClaude, spawnClaudeCaptured, runClaudeCaptured, readRuntimeIdentity, DEVELOPMENT_NEXT_STEP, appliedByName, DevelopmentBuildRefusedError, storeProjectRoot, refuseUnownedDevelopmentWrite, SQLITE_MIGRATIONS, sqliteCatalogue, planSchema, MIGRATION_LOG_TABLE, bringForward, readStoreSchema, writeNames, ACROSS_STORES_ORDER, STAMPED_COLUMNS, stampedValues, setActiveStoreSettings, activeStoreSettings, writeStoreGeneration, createGitRunner, gitSaid, commitTaskWork, PROJECT_ID_LENGTH, PROJECT_ID_PATTERN, normalizeRemote, gitRemoteUrl, readProjectIdentity, readReplacedStoreIds, readStoreMeta, carryStoreIdentity, SQLITE_STORE_FILE_NAME, sqliteStorePath, LONE_SURROGATE, withSqliteStore, writeSqliteStore, openSqliteStore, TRUSTED_PERMISSIONS };
|