loreforge 0.1.0-alpha.1

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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -0
  3. package/dist/cli/args.js +287 -0
  4. package/dist/cli/args.js.map +1 -0
  5. package/dist/cli/detect.js +94 -0
  6. package/dist/cli/detect.js.map +1 -0
  7. package/dist/cli/home.js +36 -0
  8. package/dist/cli/home.js.map +1 -0
  9. package/dist/cli/init.js +309 -0
  10. package/dist/cli/init.js.map +1 -0
  11. package/dist/cli/input.js +72 -0
  12. package/dist/cli/input.js.map +1 -0
  13. package/dist/cli/instructions.js +83 -0
  14. package/dist/cli/instructions.js.map +1 -0
  15. package/dist/cli/main.js +250 -0
  16. package/dist/cli/main.js.map +1 -0
  17. package/dist/cli/output.js +108 -0
  18. package/dist/cli/output.js.map +1 -0
  19. package/dist/context/render.js +297 -0
  20. package/dist/context/render.js.map +1 -0
  21. package/dist/core/contracts.js +49 -0
  22. package/dist/core/contracts.js.map +1 -0
  23. package/dist/core/dispatch.js +109 -0
  24. package/dist/core/dispatch.js.map +1 -0
  25. package/dist/core/errors.js +36 -0
  26. package/dist/core/errors.js.map +1 -0
  27. package/dist/core/index.js +40 -0
  28. package/dist/core/index.js.map +1 -0
  29. package/dist/core/operations/claims.js +91 -0
  30. package/dist/core/operations/claims.js.map +1 -0
  31. package/dist/core/operations/common.js +108 -0
  32. package/dist/core/operations/common.js.map +1 -0
  33. package/dist/core/operations/context.js +130 -0
  34. package/dist/core/operations/context.js.map +1 -0
  35. package/dist/core/operations/decisions.js +44 -0
  36. package/dist/core/operations/decisions.js.map +1 -0
  37. package/dist/core/operations/handoffs.js +147 -0
  38. package/dist/core/operations/handoffs.js.map +1 -0
  39. package/dist/core/operations/messages.js +117 -0
  40. package/dist/core/operations/messages.js.map +1 -0
  41. package/dist/core/operations/register.js +51 -0
  42. package/dist/core/operations/register.js.map +1 -0
  43. package/dist/core/operations/tasks.js +54 -0
  44. package/dist/core/operations/tasks.js.map +1 -0
  45. package/dist/core/schemas/common.js +81 -0
  46. package/dist/core/schemas/common.js.map +1 -0
  47. package/dist/core/schemas/context.js +10 -0
  48. package/dist/core/schemas/context.js.map +1 -0
  49. package/dist/core/schemas/decisions.js +11 -0
  50. package/dist/core/schemas/decisions.js.map +1 -0
  51. package/dist/core/schemas/handoffs.js +38 -0
  52. package/dist/core/schemas/handoffs.js.map +1 -0
  53. package/dist/core/schemas/questions.js +22 -0
  54. package/dist/core/schemas/questions.js.map +1 -0
  55. package/dist/core/schemas/registration.js +16 -0
  56. package/dist/core/schemas/registration.js.map +1 -0
  57. package/dist/core/schemas/request.js +103 -0
  58. package/dist/core/schemas/request.js.map +1 -0
  59. package/dist/core/schemas/tasks.js +35 -0
  60. package/dist/core/schemas/tasks.js.map +1 -0
  61. package/dist/evidence/git.js +119 -0
  62. package/dist/evidence/git.js.map +1 -0
  63. package/dist/integrations/index.js +32 -0
  64. package/dist/integrations/index.js.map +1 -0
  65. package/dist/storage/db.js +239 -0
  66. package/dist/storage/db.js.map +1 -0
  67. package/dist/storage/faults.js +12 -0
  68. package/dist/storage/faults.js.map +1 -0
  69. package/dist/storage/receipts.js +82 -0
  70. package/dist/storage/receipts.js.map +1 -0
  71. package/dist/storage/schema.js +119 -0
  72. package/dist/storage/schema.js.map +1 -0
  73. package/docs/integrations.md +71 -0
  74. package/docs/usage.md +405 -0
  75. package/package.json +54 -0
  76. package/templates/instructions.template.md +42 -0
  77. package/templates/session-rule.md +34 -0
@@ -0,0 +1,119 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { realpathSync } from "node:fs";
3
+ import { isAbsolute, resolve } from "node:path";
4
+ import { OpError } from "../core/errors.js";
5
+ // Read-only Git discovery for project registration. Every subprocess is an
6
+ // argument-array call with a controlled cwd and a timeout; user-supplied
7
+ // paths are never executed as code and no shell is involved.
8
+ export const GIT_TIMEOUT_MS = 10000;
9
+ // Accepted Git HEAD shape: 40-hex SHA-1 or 64-hex SHA-256. Must stay
10
+ // identical to HEAD_RE in src/core/schemas/common.ts (CONTRACT.md requires
11
+ // "full 40- or 64-hex commit ID"); observation must not reject what the
12
+ // schema accepts.
13
+ const GIT_HEAD_RE = /^(?:[0-9a-fA-F]{40}|[0-9a-fA-F]{64})$/;
14
+ function runGit(cwd, args) {
15
+ try {
16
+ // --no-optional-locks keeps read-only observation from refreshing the
17
+ // user's index (e.g. after a mere mtime touch): status still reports
18
+ // correctly but never writes. This changes no user configuration.
19
+ const result = spawnSync("git", ["--no-optional-locks", ...args], {
20
+ cwd,
21
+ timeout: GIT_TIMEOUT_MS,
22
+ encoding: "utf8",
23
+ maxBuffer: 64 * 1024,
24
+ stdio: ["ignore", "pipe", "pipe"],
25
+ });
26
+ if (result.error)
27
+ throw OpError.io("git subprocess failed");
28
+ return { status: result.status, stdout: typeof result.stdout === "string" ? result.stdout : "" };
29
+ }
30
+ catch (error) {
31
+ if (error instanceof OpError)
32
+ throw error;
33
+ throw OpError.io("git subprocess failed");
34
+ }
35
+ }
36
+ // Observe a checkout without modifying it. Any collection failure is CONFLICT
37
+ // (the reported evidence cannot be accepted), never a completed task: callers
38
+ // run this outside the write transaction and only persist inside it.
39
+ export function observeCheckout(checkoutRoot, nowMs) {
40
+ let canonical;
41
+ try {
42
+ canonical = realpathSync(checkoutRoot);
43
+ }
44
+ catch {
45
+ throw OpError.conflict("could not collect Git observation for reported checkout");
46
+ }
47
+ const headProbe = runGit(canonical, ["rev-parse", "HEAD"]);
48
+ const head = headProbe.stdout.trim();
49
+ if (headProbe.status !== 0 || !GIT_HEAD_RE.test(head)) {
50
+ throw OpError.conflict("could not collect Git observation for reported checkout");
51
+ }
52
+ const statusProbe = runGit(canonical, ["status", "--porcelain"]);
53
+ if (statusProbe.status !== 0) {
54
+ throw OpError.conflict("could not collect Git observation for reported checkout");
55
+ }
56
+ return {
57
+ checkoutRoot: canonical,
58
+ head: head.toLowerCase(),
59
+ dirty: statusProbe.stdout.length > 0,
60
+ collectedAt: new Date(nowMs).toISOString(),
61
+ };
62
+ }
63
+ // Best-effort read of the registered project root for context snapshots. An
64
+ // absent or unreadable root is null head/dirty plus a sanitized error, never
65
+ // a failed context request. Never throws.
66
+ export function observeCurrentGit(root, nowMs) {
67
+ const collectedAt = new Date(nowMs).toISOString();
68
+ const failure = { head: null, dirty: null, collectedAt, error: "could not read project Git state" };
69
+ let canonical;
70
+ try {
71
+ canonical = realpathSync(root);
72
+ }
73
+ catch {
74
+ return failure;
75
+ }
76
+ let headProbe;
77
+ let statusProbe;
78
+ try {
79
+ headProbe = runGit(canonical, ["rev-parse", "HEAD"]);
80
+ statusProbe = runGit(canonical, ["status", "--porcelain"]);
81
+ }
82
+ catch {
83
+ return failure;
84
+ }
85
+ const head = headProbe.stdout.trim();
86
+ if (headProbe.status !== 0 || !GIT_HEAD_RE.test(head) || statusProbe.status !== 0) {
87
+ return failure;
88
+ }
89
+ return { head: head.toLowerCase(), dirty: statusProbe.stdout.length > 0, collectedAt, error: null };
90
+ }
91
+ // Resolve a checkout root to its canonical project identity. Worktrees of one
92
+ // repository share the same realpath Git common directory and therefore the
93
+ // same project; distinct clones differ. Non-Git roots are VALIDATION, not IO:
94
+ // the input path is well-formed but unacceptable in v0.1.
95
+ export function discoverProject(root) {
96
+ let canonicalRoot;
97
+ try {
98
+ canonicalRoot = realpathSync(root);
99
+ }
100
+ catch {
101
+ throw OpError.validation("project root is not an accessible directory");
102
+ }
103
+ const probe = runGit(canonicalRoot, ["rev-parse", "--git-common-dir"]);
104
+ if (probe.status !== 0) {
105
+ throw OpError.validation("project root is not inside a Git repository");
106
+ }
107
+ const reported = probe.stdout.trim();
108
+ if (reported.length === 0) {
109
+ throw OpError.validation("project root is not inside a Git repository");
110
+ }
111
+ const joined = isAbsolute(reported) ? reported : resolve(canonicalRoot, reported);
112
+ try {
113
+ return { root: canonicalRoot, gitCommonDir: realpathSync(joined) };
114
+ }
115
+ catch {
116
+ throw OpError.validation("project root is not inside a Git repository");
117
+ }
118
+ }
119
+ //# sourceMappingURL=git.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/evidence/git.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Integrations module for Loreforge v0.1.
3
+ * Provides generic instruction snippet exports and provider metadata.
4
+ */
5
+ export { generateInstructions, quoteShell } from "../cli/instructions.js";
6
+ export const PROVIDER_INTEGRATIONS = [
7
+ {
8
+ provider: "Google Antigravity (AGY)",
9
+ modelReported: "Gemini 3.8 Flash (High)",
10
+ genericSnippetSupported: true,
11
+ nativeHooksStatus: "deferred",
12
+ localCliExecution: "exercised",
13
+ notes: "Local CLI execution verified via Node subprocess and development scripts."
14
+ },
15
+ {
16
+ provider: "Meta Muse",
17
+ modelReported: "Muse Spark 1.3 (xhigh requested)",
18
+ genericSnippetSupported: true,
19
+ nativeHooksStatus: "deferred",
20
+ localCliExecution: "exercised",
21
+ notes: "Core storage and schemas implemented concurrently."
22
+ },
23
+ {
24
+ provider: "Other AI CLIs (Codex, Claude, etc.)",
25
+ modelReported: "Unspecified / User-supplied",
26
+ genericSnippetSupported: true,
27
+ nativeHooksStatus: "unverified",
28
+ localCliExecution: "pending",
29
+ notes: "Callable from any standard POSIX shell using the lore CLI."
30
+ }
31
+ ];
32
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/integrations/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAW1E,MAAM,CAAC,MAAM,qBAAqB,GAAuC;IACvE;QACE,QAAQ,EAAE,0BAA0B;QACpC,aAAa,EAAE,yBAAyB;QACxC,uBAAuB,EAAE,IAAI;QAC7B,iBAAiB,EAAE,UAAU;QAC7B,iBAAiB,EAAE,WAAW;QAC9B,KAAK,EAAE,2EAA2E;KACnF;IACD;QACE,QAAQ,EAAE,WAAW;QACrB,aAAa,EAAE,kCAAkC;QACjD,uBAAuB,EAAE,IAAI;QAC7B,iBAAiB,EAAE,UAAU;QAC7B,iBAAiB,EAAE,WAAW;QAC9B,KAAK,EAAE,oDAAoD;KAC5D;IACD;QACE,QAAQ,EAAE,qCAAqC;QAC/C,aAAa,EAAE,6BAA6B;QAC5C,uBAAuB,EAAE,IAAI;QAC7B,iBAAiB,EAAE,YAAY;QAC/B,iBAAiB,EAAE,SAAS;QAC5B,KAAK,EAAE,4DAA4D;KACpE;CACF,CAAC"}
@@ -0,0 +1,239 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ import { chmodSync, existsSync, mkdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { CoreOpenError } from "../core/contracts.js";
5
+ import { OpError } from "../core/errors.js";
6
+ import { MIGRATIONS } from "./schema.js";
7
+ // Lifecycle: open, pragma setup, versioned migrations, short synchronous
8
+ // transactions. SQLite work never spans an await or a subprocess.
9
+ export const DB_FILENAME = "loreforge.sqlite3";
10
+ export const LEGACY_DB_FILENAME = "company.sqlite3";
11
+ export const BUSY_TIMEOUT_MS = 5000;
12
+ // New homes use loreforge.sqlite3. An existing company.sqlite3 in the same
13
+ // directory is opened as-is so the rename does not orphan prior state.
14
+ export function resolveDbPath(home) {
15
+ const next = join(home, DB_FILENAME);
16
+ const legacy = join(home, LEGACY_DB_FILENAME);
17
+ if (existsSync(next))
18
+ return next;
19
+ if (existsSync(legacy))
20
+ return legacy;
21
+ return next;
22
+ }
23
+ export function databaseExists(home) {
24
+ return existsSync(join(home, DB_FILENAME)) || existsSync(join(home, LEGACY_DB_FILENAME));
25
+ }
26
+ function errcodeOf(error) {
27
+ if (typeof error === "object" && error !== null && "errcode" in error) {
28
+ const code = error.errcode;
29
+ return typeof code === "number" ? code : null;
30
+ }
31
+ return null;
32
+ }
33
+ function codeOf(error) {
34
+ if (typeof error === "object" && error !== null && "code" in error) {
35
+ const code = error.code;
36
+ return typeof code === "string" ? code : null;
37
+ }
38
+ return null;
39
+ }
40
+ // SQLITE_BUSY (5) and SQLITE_LOCKED (6) surface with a generic code, so match
41
+ // the numeric errcode first and fall back to the lock message text.
42
+ export function isBusyError(error) {
43
+ const errcode = errcodeOf(error);
44
+ if (errcode === 5 || errcode === 6)
45
+ return true;
46
+ if (codeOf(error) === "ERR_SQLITE_BUSY")
47
+ return true;
48
+ const message = error instanceof Error ? error.message : String(error);
49
+ return /database is (locked|busy)|database table is locked/i.test(message);
50
+ }
51
+ export function isSqliteError(error) {
52
+ if (errcodeOf(error) !== null)
53
+ return true;
54
+ const code = codeOf(error);
55
+ return code !== null && code.startsWith("ERR_SQLITE_");
56
+ }
57
+ // Convert a storage-layer throw into an operation error. OpError and
58
+ // CoreOpenError pass through; lock contention becomes BUSY; other SQLite
59
+ // faults become IO with a sanitized message; anything else rethrows for the
60
+ // dispatcher's INTERNAL mapping.
61
+ export function toOpError(error) {
62
+ if (error instanceof OpError || error instanceof CoreOpenError)
63
+ return error;
64
+ if (isBusyError(error))
65
+ return OpError.busy("database is locked by another process");
66
+ if (isSqliteError(error))
67
+ return OpError.io("database access failed");
68
+ return error instanceof Error ? error : new Error("unknown storage failure");
69
+ }
70
+ export function beginImmediate(db) {
71
+ try {
72
+ db.exec("BEGIN IMMEDIATE");
73
+ }
74
+ catch (error) {
75
+ throw toOpError(error);
76
+ }
77
+ }
78
+ // Short synchronous read transaction for multi-query snapshots. No writes,
79
+ // no subprocesses, no awaits inside.
80
+ export function withRead(db, work) {
81
+ try {
82
+ db.exec("BEGIN");
83
+ }
84
+ catch (error) {
85
+ throw toOpError(error);
86
+ }
87
+ try {
88
+ const result = work();
89
+ try {
90
+ db.exec("COMMIT");
91
+ }
92
+ catch (error) {
93
+ try {
94
+ db.exec("ROLLBACK");
95
+ }
96
+ catch {
97
+ // Best-effort; the COMMIT failure below is what matters.
98
+ }
99
+ throw toOpError(error);
100
+ }
101
+ return result;
102
+ }
103
+ catch (error) {
104
+ try {
105
+ db.exec("ROLLBACK");
106
+ }
107
+ catch {
108
+ // Best-effort; the original error below is what matters.
109
+ }
110
+ throw toOpError(error);
111
+ }
112
+ }
113
+ // Short synchronous write transaction. Any error rolls back every effect;
114
+ // the receipt row is written by the caller inside the same transaction.
115
+ export function withWrite(db, work) {
116
+ beginImmediate(db);
117
+ try {
118
+ const result = work();
119
+ try {
120
+ db.exec("COMMIT");
121
+ }
122
+ catch (error) {
123
+ try {
124
+ db.exec("ROLLBACK");
125
+ }
126
+ catch {
127
+ // Rollback best-effort; the COMMIT failure below explains the state.
128
+ }
129
+ throw toOpError(error);
130
+ }
131
+ return result;
132
+ }
133
+ catch (error) {
134
+ try {
135
+ db.exec("ROLLBACK");
136
+ }
137
+ catch {
138
+ // Best-effort; the original error below is what matters.
139
+ }
140
+ throw toOpError(error);
141
+ }
142
+ }
143
+ function migrate(db) {
144
+ beginImmediate(db);
145
+ try {
146
+ db.exec("CREATE TABLE IF NOT EXISTS schema_migrations (version INTEGER PRIMARY KEY, applied_at INTEGER NOT NULL) STRICT");
147
+ const rows = db
148
+ .prepare("SELECT version FROM schema_migrations ORDER BY version")
149
+ .all();
150
+ const applied = new Set(rows.map((row) => row.version));
151
+ for (const migration of MIGRATIONS) {
152
+ if (!applied.has(migration.version)) {
153
+ db.exec(migration.sql);
154
+ db.prepare("INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)").run(migration.version, Date.now());
155
+ }
156
+ }
157
+ db.exec("COMMIT");
158
+ }
159
+ catch (error) {
160
+ try {
161
+ db.exec("ROLLBACK");
162
+ }
163
+ catch {
164
+ // Best-effort; the migration error below is what matters.
165
+ }
166
+ if (error instanceof CoreOpenError)
167
+ throw error;
168
+ if (error instanceof OpError && error.code === "BUSY") {
169
+ throw new CoreOpenError("BUSY", "database is locked by another process");
170
+ }
171
+ throw new CoreOpenError("IO", "database migration failed");
172
+ }
173
+ }
174
+ // Short synchronous pause between startup retries. openDatabase is
175
+ // synchronous, so this is a bounded Date.now wait, never an await and never
176
+ // a subprocess.
177
+ function sleepSync(ms) {
178
+ const end = Date.now() + ms;
179
+ while (Date.now() < end) {
180
+ // Intentional bounded wait for a racing peer to finish startup.
181
+ }
182
+ }
183
+ // Open (creating if needed) the state directory database. Throws CoreOpenError
184
+ // with IO for access failures or BUSY when the startup lock cannot be
185
+ // acquired within the busy timeout.
186
+ //
187
+ // Concurrent first opens race inside setup: PRAGMA journal_mode = WAL does
188
+ // not honor the connection busy timeout, so the loser sees SQLITE_BUSY while
189
+ // the winner migrates. Retry the whole setup on lock contention within the
190
+ // busy budget; only exhausted contention becomes BUSY, never IO. Genuine
191
+ // failures (unreadable directory, unopenable file, failed migration) still
192
+ // fail fast with IO.
193
+ export function openDatabase(home) {
194
+ try {
195
+ mkdirSync(home, { recursive: true, mode: 0o700 });
196
+ chmodSync(home, 0o700);
197
+ }
198
+ catch {
199
+ throw new CoreOpenError("IO", "cannot create state directory");
200
+ }
201
+ const path = resolveDbPath(home);
202
+ const deadline = Date.now() + BUSY_TIMEOUT_MS;
203
+ for (;;) {
204
+ let db;
205
+ try {
206
+ db = new DatabaseSync(path);
207
+ }
208
+ catch {
209
+ throw new CoreOpenError("IO", "cannot open database file");
210
+ }
211
+ try {
212
+ db.exec("PRAGMA foreign_keys = ON");
213
+ db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
214
+ db.exec("PRAGMA journal_mode = WAL");
215
+ chmodSync(path, 0o600);
216
+ migrate(db);
217
+ return db;
218
+ }
219
+ catch (error) {
220
+ try {
221
+ db.close();
222
+ }
223
+ catch {
224
+ // Best-effort close before retrying or reporting the open failure.
225
+ }
226
+ const retryable = (error instanceof CoreOpenError && error.code === "BUSY") || isBusyError(error);
227
+ if (!retryable) {
228
+ if (error instanceof CoreOpenError)
229
+ throw error;
230
+ throw new CoreOpenError("IO", "cannot initialize database");
231
+ }
232
+ if (Date.now() >= deadline) {
233
+ throw new CoreOpenError("BUSY", "database is locked by another process");
234
+ }
235
+ sleepSync(25);
236
+ }
237
+ }
238
+ }
239
+ //# sourceMappingURL=db.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/storage/db.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ // Private test seam for transaction-recovery tests (A08). The injector runs
2
+ // at named write boundaries inside the handoff transaction and throws to
3
+ // simulate a crash between writes. Set only from tests; never wired to an
4
+ // environment variable, CLI flag, or normal operation.
5
+ let injector = null;
6
+ export function setFaultInjector(next) {
7
+ injector = next;
8
+ }
9
+ export function faultBoundary(boundary) {
10
+ injector?.(boundary);
11
+ }
12
+ //# sourceMappingURL=faults.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"faults.js","sourceRoot":"","sources":["../../src/storage/faults.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,yEAAyE;AACzE,0EAA0E;AAC1E,uDAAuD;AAIvD,IAAI,QAAQ,GAAyB,IAAI,CAAC;AAE1C,MAAM,UAAU,gBAAgB,CAAC,IAA0B;IACzD,QAAQ,GAAG,IAAI,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,QAAgB;IAC5C,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC;AACvB,CAAC"}
@@ -0,0 +1,82 @@
1
+ import { createHash } from "node:crypto";
2
+ import { OpError } from "../core/errors.js";
3
+ export function receiptIdentity(request) {
4
+ if (request.operation === "project.register" || request.operation === "agent.register") {
5
+ return { scope: "global", actorKey: "registration" };
6
+ }
7
+ const scoped = request;
8
+ return { scope: scoped.projectId, actorKey: scoped.actorId };
9
+ }
10
+ function stableStringify(value) {
11
+ if (value === null || value === undefined)
12
+ return "null";
13
+ if (Array.isArray(value))
14
+ return `[${value.map(stableStringify).join(",")}]`;
15
+ if (typeof value === "object") {
16
+ const record = value;
17
+ const keys = Object.keys(record).sort();
18
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
19
+ }
20
+ const text = JSON.stringify(value);
21
+ if (typeof text !== "string")
22
+ throw new Error("unserializable receipt payload");
23
+ return text;
24
+ }
25
+ // Canonical request hash over operation, project scope, actor, and validated
26
+ // payload (defaults applied, object keys recursively sorted, array order
27
+ // preserved). Computed before any observation or timestamp is added.
28
+ export function canonicalHash(request) {
29
+ const identity = receiptIdentity(request);
30
+ return createHash("sha256")
31
+ .update(stableStringify({
32
+ actor: identity.actorKey,
33
+ operation: request.operation,
34
+ payload: request.payload,
35
+ scope: identity.scope,
36
+ }))
37
+ .digest("hex");
38
+ }
39
+ // Control-flow signal: our receipt insert lost a cross-process race. The
40
+ // surrounding helper rolls back and retries once, then replays or conflicts
41
+ // from a fresh read. Never surfaces as IO.
42
+ export class ReceiptClashError extends Error {
43
+ constructor() {
44
+ super("mutation receipt already recorded");
45
+ this.name = "ReceiptClashError";
46
+ }
47
+ }
48
+ export function isReceiptPkClash(error) {
49
+ const message = error instanceof Error ? error.message : String(error);
50
+ return /UNIQUE constraint failed: mutation_receipts/.test(message);
51
+ }
52
+ export function findReceipt(db, scope, actorKey, requestId) {
53
+ const row = db
54
+ .prepare("SELECT operation, request_hash, response_json FROM mutation_receipts WHERE scope = ? AND actor_key = ? AND request_id = ?")
55
+ .get(scope, actorKey, requestId);
56
+ return row;
57
+ }
58
+ export function insertReceipt(db, scope, actorKey, requestId, operation, requestHash, responseJson, nowMs) {
59
+ db.prepare("INSERT INTO mutation_receipts (scope, actor_key, request_id, operation, request_hash, response_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)").run(scope, actorKey, requestId, operation, requestHash, responseJson, nowMs);
60
+ }
61
+ function replayOrConflict(row, request, hash) {
62
+ if (row.operation === request.operation && row.request_hash === hash) {
63
+ return JSON.parse(row.response_json);
64
+ }
65
+ throw OpError.conflict("requestId was already used with a different operation or payload");
66
+ }
67
+ // Pre-transaction receipt check plus transactional recheck. Simple mutations
68
+ // (no outside-transaction work) run entirely through withMutation; handlers
69
+ // with outside work (Git collection) call readReceipt early and storeReceipt
70
+ // inside their own transaction.
71
+ export function readReceipt(db, request) {
72
+ const identity = receiptIdentity(request);
73
+ const row = findReceipt(db, identity.scope, identity.actorKey, request.requestId);
74
+ if (!row)
75
+ return undefined;
76
+ return replayOrConflict(row, request, canonicalHash(request));
77
+ }
78
+ export function storeReceipt(db, request, data, nowMs) {
79
+ const identity = receiptIdentity(request);
80
+ insertReceipt(db, identity.scope, identity.actorKey, request.requestId, request.operation, canonicalHash(request), JSON.stringify(data), nowMs);
81
+ }
82
+ //# sourceMappingURL=receipts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"receipts.js","sourceRoot":"","sources":["../../src/storage/receipts.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,119 @@
1
+ // Versioned additive migrations. Never DROP existing state; each version
2
+ // applies once inside the startup transaction in db.ts.
3
+ // v1 creates every logical table from CONTRACT.md in one additive step so
4
+ // later milestones add behavior, not schema. Domains are still implemented
5
+ // and tested one milestone at a time.
6
+ const V001 = `
7
+ CREATE TABLE projects (
8
+ id TEXT PRIMARY KEY,
9
+ name TEXT NOT NULL,
10
+ root TEXT NOT NULL,
11
+ git_common_dir TEXT NOT NULL UNIQUE,
12
+ created_at INTEGER NOT NULL
13
+ ) STRICT;
14
+
15
+ CREATE TABLE agents (
16
+ id TEXT PRIMARY KEY,
17
+ display_name TEXT NOT NULL,
18
+ created_at INTEGER NOT NULL
19
+ ) STRICT;
20
+
21
+ CREATE TABLE tasks (
22
+ id TEXT PRIMARY KEY,
23
+ project_id TEXT NOT NULL REFERENCES projects (id),
24
+ title TEXT NOT NULL,
25
+ description TEXT NOT NULL,
26
+ status TEXT NOT NULL,
27
+ owner_id TEXT NULL REFERENCES agents (id),
28
+ claim_token TEXT NULL,
29
+ attempt INTEGER NOT NULL,
30
+ lease_until INTEGER NULL,
31
+ created_at INTEGER NOT NULL,
32
+ updated_at INTEGER NOT NULL
33
+ ) STRICT;
34
+ CREATE INDEX tasks_by_project ON tasks (project_id, created_at DESC, id ASC);
35
+
36
+ CREATE TABLE task_dependencies (
37
+ task_id TEXT NOT NULL REFERENCES tasks (id),
38
+ depends_on_task_id TEXT NOT NULL REFERENCES tasks (id),
39
+ PRIMARY KEY (task_id, depends_on_task_id)
40
+ ) STRICT;
41
+ CREATE INDEX task_dependencies_by_parent ON task_dependencies (depends_on_task_id);
42
+
43
+ CREATE TABLE handoffs (
44
+ id TEXT PRIMARY KEY,
45
+ project_id TEXT NOT NULL REFERENCES projects (id),
46
+ task_id TEXT NOT NULL REFERENCES tasks (id),
47
+ actor_id TEXT NOT NULL REFERENCES agents (id),
48
+ attempt INTEGER NOT NULL,
49
+ outcome TEXT NOT NULL,
50
+ summary TEXT NOT NULL,
51
+ evidence_json TEXT NOT NULL,
52
+ unresolved_json TEXT NOT NULL,
53
+ next_steps_json TEXT NOT NULL,
54
+ blocking_ids_json TEXT NOT NULL,
55
+ observed_checkout_root TEXT NOT NULL,
56
+ observed_head TEXT NOT NULL,
57
+ observed_dirty INTEGER NOT NULL,
58
+ observed_collected_at INTEGER NOT NULL,
59
+ created_at INTEGER NOT NULL,
60
+ UNIQUE (task_id, attempt)
61
+ ) STRICT;
62
+ CREATE INDEX handoffs_by_task ON handoffs (task_id, created_at DESC, id ASC);
63
+
64
+ CREATE TABLE questions (
65
+ id TEXT PRIMARY KEY,
66
+ project_id TEXT NOT NULL REFERENCES projects (id),
67
+ task_id TEXT NOT NULL REFERENCES tasks (id),
68
+ from_agent_id TEXT NOT NULL REFERENCES agents (id),
69
+ to_agent_id TEXT NOT NULL REFERENCES agents (id),
70
+ body TEXT NOT NULL,
71
+ created_at INTEGER NOT NULL
72
+ ) STRICT;
73
+ CREATE INDEX questions_by_task ON questions (task_id, created_at DESC, id ASC);
74
+
75
+ CREATE TABLE answers (
76
+ question_id TEXT PRIMARY KEY REFERENCES questions (id),
77
+ id TEXT NOT NULL,
78
+ actor_id TEXT NOT NULL REFERENCES agents (id),
79
+ body TEXT NOT NULL,
80
+ created_at INTEGER NOT NULL
81
+ ) STRICT;
82
+
83
+ CREATE TABLE decisions (
84
+ id TEXT PRIMARY KEY,
85
+ project_id TEXT NOT NULL REFERENCES projects (id),
86
+ actor_id TEXT NOT NULL REFERENCES agents (id),
87
+ body TEXT NOT NULL,
88
+ paths_json TEXT NOT NULL,
89
+ supersedes_id TEXT NULL REFERENCES decisions (id),
90
+ superseded_by_id TEXT NULL REFERENCES decisions (id),
91
+ created_at INTEGER NOT NULL
92
+ ) STRICT;
93
+ CREATE INDEX decisions_by_project ON decisions (project_id, created_at DESC, id ASC);
94
+
95
+ CREATE TABLE inbox_events (
96
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
97
+ project_id TEXT NOT NULL REFERENCES projects (id),
98
+ recipient_id TEXT NOT NULL REFERENCES agents (id),
99
+ kind TEXT NOT NULL,
100
+ question_id TEXT NOT NULL REFERENCES questions (id),
101
+ task_id TEXT NOT NULL REFERENCES tasks (id),
102
+ body TEXT NOT NULL,
103
+ created_at INTEGER NOT NULL
104
+ ) STRICT;
105
+ CREATE INDEX inbox_by_recipient ON inbox_events (project_id, recipient_id, id ASC);
106
+
107
+ CREATE TABLE mutation_receipts (
108
+ scope TEXT NOT NULL,
109
+ actor_key TEXT NOT NULL,
110
+ request_id TEXT NOT NULL,
111
+ operation TEXT NOT NULL,
112
+ request_hash TEXT NOT NULL,
113
+ response_json TEXT NOT NULL,
114
+ created_at INTEGER NOT NULL,
115
+ PRIMARY KEY (scope, actor_key, request_id)
116
+ ) STRICT;
117
+ `;
118
+ export const MIGRATIONS = [{ version: 1, sql: V001 }];
119
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/storage/schema.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,wDAAwD;AAOxD,0EAA0E;AAC1E,2EAA2E;AAC3E,sCAAsC;AACtC,MAAM,IAAI,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+GZ,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAgB,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC"}