trajectories 0.0.0 → 0.1.2
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/CHANGELOG.md +36 -0
- package/LICENSE +201 -0
- package/README.md +288 -2
- package/dist/cli.js +30210 -0
- package/dist/convert.d.ts +31 -0
- package/dist/convert.js +431 -0
- package/dist/deepagents_checkpoint.py +362 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8402 -0
- package/dist/log-baseline.d.ts +26 -0
- package/dist/log-baseline.js +104 -0
- package/dist/log-content.d.ts +16 -0
- package/dist/log-content.js +80 -0
- package/dist/log-io.d.ts +10 -0
- package/dist/log-io.js +121 -0
- package/dist/log-read.d.ts +24 -0
- package/dist/log-read.js +117 -0
- package/dist/log-reconcile.d.ts +5 -0
- package/dist/log-reconcile.js +38 -0
- package/dist/log-records.d.ts +35 -0
- package/dist/log-records.js +85 -0
- package/dist/log-recovery.d.ts +18 -0
- package/dist/log-recovery.js +123 -0
- package/dist/log-stage.d.ts +14 -0
- package/dist/log-stage.js +138 -0
- package/dist/log-state.d.ts +89 -0
- package/dist/log-state.js +92 -0
- package/dist/log-switch.d.ts +3 -0
- package/dist/log-switch.js +98 -0
- package/dist/log-types.d.ts +129 -0
- package/dist/log-types.js +8 -0
- package/dist/log.d.ts +8 -0
- package/dist/log.js +62 -0
- package/dist/migrate.d.ts +29 -0
- package/dist/migrate.js +173 -0
- package/dist/normalization-runtime.d.ts +1 -0
- package/dist/normalization-runtime.js +3150 -0
- package/dist/portable.d.ts +9 -0
- package/dist/portable.js +26 -0
- package/dist/readers/canonical.d.ts +13 -0
- package/dist/readers/canonical.js +54 -0
- package/dist/readers/cline.d.ts +10 -0
- package/dist/readers/cline.js +107 -0
- package/dist/readers/cursor.d.ts +20 -0
- package/dist/readers/cursor.js +374 -0
- package/dist/readers/deepagents.d.ts +41 -0
- package/dist/readers/deepagents.js +163 -0
- package/dist/readers/fx.d.ts +11 -0
- package/dist/readers/fx.js +573 -0
- package/dist/readers/github-copilot.d.ts +8 -0
- package/dist/readers/github-copilot.js +216 -0
- package/dist/readers/grok-build.d.ts +12 -0
- package/dist/readers/grok-build.js +391 -0
- package/dist/readers/guards.d.ts +4 -0
- package/dist/readers/guards.js +121 -0
- package/dist/readers/pi-compaction.d.ts +7 -0
- package/dist/readers/pi-compaction.js +112 -0
- package/dist/readers/pi-context.d.ts +13 -0
- package/dist/readers/pi-context.js +64 -0
- package/dist/store.d.ts +25 -0
- package/dist/store.js +218 -0
- package/dist/types.d.ts +91 -0
- package/dist/types.js +1 -0
- package/dist/writers/claude-code.d.ts +5 -0
- package/dist/writers/claude-code.js +51 -0
- package/dist/writers/cline.d.ts +3 -0
- package/dist/writers/cline.js +77 -0
- package/dist/writers/codex.d.ts +3 -0
- package/dist/writers/codex.js +32 -0
- package/dist/writers/cursor.d.ts +3 -0
- package/dist/writers/cursor.js +197 -0
- package/dist/writers/deepagents.d.ts +3 -0
- package/dist/writers/deepagents.js +116 -0
- package/dist/writers/entropy.d.ts +5 -0
- package/dist/writers/entropy.js +33 -0
- package/dist/writers/fx.d.ts +3 -0
- package/dist/writers/fx.js +135 -0
- package/dist/writers/github-copilot.d.ts +3 -0
- package/dist/writers/github-copilot.js +71 -0
- package/dist/writers/grok-build.d.ts +3 -0
- package/dist/writers/grok-build.js +43 -0
- package/dist/writers/helpers.d.ts +17 -0
- package/dist/writers/helpers.js +75 -0
- package/dist/writers/omp.d.ts +3 -0
- package/dist/writers/omp.js +7 -0
- package/dist/writers/opencode.d.ts +3 -0
- package/dist/writers/opencode.js +65 -0
- package/dist/writers/pi.d.ts +3 -0
- package/dist/writers/pi.js +45 -0
- package/docs/cline-native-format.md +43 -0
- package/docs/copilot-native-format.md +78 -0
- package/docs/cursor-native-format.md +103 -0
- package/docs/deepagents-native-format.md +98 -0
- package/docs/evidence-api.md +170 -0
- package/docs/fx-native-format.md +87 -0
- package/docs/harness-parity-bootstrap.md +78 -0
- package/docs/harness-support-parity.md +107 -0
- package/docs/inspirations.md +103 -0
- package/docs/ledger-migration.md +141 -0
- package/docs/pi-compaction-context.md +75 -0
- package/docs/pi-provider-context.md +76 -0
- package/docs/research/2026-10-06-huihua-and-oar.md +176 -0
- package/docs/research/ledger/README.md +96 -0
- package/docs/research/ledger/confer/2026-09-13-round-10/README.md +61 -0
- package/docs/research/ledger/confer/2026-09-13-round-2/cass.md +72 -0
- package/docs/research/ledger/confer/2026-09-13-round-3/README.md +70 -0
- package/docs/research/ledger/confer/2026-09-13-round-5/README.md +61 -0
- package/docs/research/ledger/confer/2026-09-13-round-6/README.md +68 -0
- package/docs/research/ledger/confer/2026-09-13-round-7/README.md +64 -0
- package/docs/research/ledger/confer/2026-09-13-round-9/README.md +59 -0
- package/docs/research/ledger/confer/2026-09-14-wake/wake.md +146 -0
- package/docs/research/ledger/docs/inspirations.md +320 -0
- package/docs/research/ledger/docs/pr2-pr6-integration.md +53 -0
- package/docs/research/ledger/docs/research/2026-09-07-confer-inspirations.md +163 -0
- package/docs/research/ledger/docs/research/2026-09-07-letta-continuous-service.md +104 -0
- package/docs/research/ledger/docs/research/2026-09-08-claude-history-retrieval.md +117 -0
- package/docs/research/ledger/docs/research/2026-09-09-agentsview-native-discovery.md +87 -0
- package/docs/research/ledger/docs/research/2026-09-09-daemon-observation.md +231 -0
- package/docs/research/ledger/docs/research/2026-09-09-discovery-pagination.md +114 -0
- package/docs/research/ledger/docs/research/2026-09-09-harness-transcript-contract.md +145 -0
- package/docs/research/ledger/docs/research/2026-09-09-native-history-discovery.md +180 -0
- package/docs/research/ledger/docs/research/2026-09-09-native-history-implementation.md +137 -0
- package/docs/research/ledger/docs/research/2026-09-09-other-inspirations.md +279 -0
- package/docs/research/ledger/docs/research/2026-09-09-trajectory-and-letta-code.md +128 -0
- package/docs/research/ledger/docs/research/2026-09-13-diagnostics-coverage.md +116 -0
- package/docs/research/ledger/docs/research/2026-09-13-fts-rebuild.md +83 -0
- package/docs/research/ledger/docs/research/2026-09-13-read-path-split.md +74 -0
- package/docs/research/ledger/docs/research/2026-09-13-runtime-rollout.md +102 -0
- package/docs/research/ledger/docs/research/2026-09-13-scan-outcomes.md +80 -0
- package/docs/research/ledger/docs/research/2026-09-13-search-quality.md +105 -0
- package/docs/research/ledger/docs/research/2026-09-13-source-coverage.md +64 -0
- package/docs/research/ledger/docs/research/2026-09-13-sync-wall-budget.md +82 -0
- package/docs/research/ledger/docs/research/2026-09-13-typed-read-gate.md +81 -0
- package/docs/research/ledger/docs/research/2026-09-13-writer-lease.md +84 -0
- package/docs/research/ledger/docs/research/2026-09-16-c1-copy-fallback.md +227 -0
- package/docs/research/ledger/docs/research/2026-09-16-c10-kimi.md +414 -0
- package/docs/research/ledger/docs/research/2026-09-16-c2-derive-counter.md +216 -0
- package/docs/research/ledger/docs/research/2026-09-16-c3-echo-filter.md +305 -0
- package/docs/research/ledger/docs/research/2026-09-16-c4-title-index.md +349 -0
- package/docs/research/ledger/docs/research/2026-09-16-c5-trigram.md +268 -0
- package/docs/research/ledger/docs/research/2026-09-16-c6-recency-prior.md +438 -0
- package/docs/research/ledger/docs/research/2026-09-16-c7-tombstones.md +467 -0
- package/docs/research/ledger/docs/research/2026-09-16-c8-remote-truth.md +390 -0
- package/docs/research/ledger/docs/research/2026-09-16-c9-grok.md +380 -0
- package/docs/research/ledger/docs/research/2026-09-16-wake-port-program.md +38 -0
- package/docs/research/ledger/docs/research/2026-09-17-cursor-blobstore.md +157 -0
- package/docs/research/ledger/manifest.json +540 -0
- package/legacy/NOTICE.md +50 -0
- package/legacy/bin/ledger-core.ts +7 -0
- package/legacy/bin/ledger.ts +7 -0
- package/legacy/bin/traj-bridge.ts +7 -0
- package/legacy/scripts/import-claude-history.ts +130 -0
- package/legacy/scripts/native-history-smoke.ts +52 -0
- package/legacy/scripts/observe-daemon.ts +570 -0
- package/legacy/scripts/retitle-sessions.md +19 -0
- package/legacy/scripts/verify-runtime-rollout.ts +151 -0
- package/legacy/src/canonical.ts +108 -0
- package/legacy/src/cli.ts +1293 -0
- package/legacy/src/db.ts +2254 -0
- package/legacy/src/diagnostics.ts +87 -0
- package/legacy/src/discovery.ts +56 -0
- package/legacy/src/embeddings.ts +382 -0
- package/legacy/src/filter.ts +602 -0
- package/legacy/src/gemini_embeddings.ts +103 -0
- package/legacy/src/identity.ts +9 -0
- package/legacy/src/index.ts +10 -0
- package/legacy/src/ingest.ts +791 -0
- package/legacy/src/managed.ts +178 -0
- package/legacy/src/native/common.ts +68 -0
- package/legacy/src/native/cursor.ts +647 -0
- package/legacy/src/native/grok.ts +540 -0
- package/legacy/src/native/kimi.ts +706 -0
- package/legacy/src/native/opencode.ts +470 -0
- package/legacy/src/native/sqlite-ro.ts +281 -0
- package/legacy/src/native.ts +97 -0
- package/legacy/src/normalizer.ts +457 -0
- package/legacy/src/parentage.ts +562 -0
- package/legacy/src/pressure.ts +162 -0
- package/legacy/src/private-storage.ts +74 -0
- package/legacy/src/reader.ts +1255 -0
- package/legacy/src/scanner.ts +591 -0
- package/legacy/src/schema-gate.ts +263 -0
- package/legacy/src/schema.ts +829 -0
- package/legacy/src/semantic-top-k.ts +71 -0
- package/legacy/src/session-hash.ts +49 -0
- package/legacy/src/store-status.ts +218 -0
- package/legacy/src/sync-preparation.ts +45 -0
- package/legacy/src/sync-truth.ts +843 -0
- package/legacy/src/sync.ts +1233 -0
- package/legacy/src/types.ts +436 -0
- package/legacy/src/vocab.txt +30522 -0
- package/legacy/src/watch.ts +673 -0
- package/legacy/src/writer-lease.ts +310 -0
- package/ops/README.md +150 -0
- package/ops/install-cli.sh +63 -0
- package/ops/install-native.sh +35 -0
- package/ops/install-release.sh +49 -0
- package/ops/linux/install-systemd-user.sh +103 -0
- package/ops/macos/install-launchd.sh +72 -0
- package/ops/sync-settings.sh +37 -0
- package/package.json +21 -5
- package/skills/trajectories/SKILL.md +138 -0
- package/skills/trajectories/scripts/trajectories_recent.sh +26 -0
- package/skills/trajectories/scripts/trajectories_search.sh +26 -0
- package/index.js +0 -1
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { existsSync, renameSync, statSync } from "node:fs";
|
|
2
|
+
import { Database } from "bun:sqlite";
|
|
3
|
+
import { ensurePrivateDatabase } from "./private-storage.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Cross-process writer arbitration for one store.
|
|
7
|
+
*
|
|
8
|
+
* The lease is a sibling SQLite database (`<dbPath>-write.lock`) whose write
|
|
9
|
+
* lock *is* the lease: a holder commits its label, then keeps an uncommitted
|
|
10
|
+
* `BEGIN IMMEDIATE` transaction open for the lease lifetime. Process death
|
|
11
|
+
* releases it by descriptor teardown, so there is no TTL, no lease row
|
|
12
|
+
* expiry, and no reaper. The lock file is never unlinked — unlink plus
|
|
13
|
+
* recreate splits the inode and hands two writers the same "exclusive" lock.
|
|
14
|
+
* It holds no user data: deleting it while no writer runs is a safe repair,
|
|
15
|
+
* and an unreadable one is renamed aside automatically.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Bounded wait before a contended writer refuses. */
|
|
19
|
+
export const DEFAULT_WRITER_LEASE_WAIT_MS = 250;
|
|
20
|
+
/** Busy retry spacing; the attempt budget is derived from it. */
|
|
21
|
+
export const WRITER_LEASE_RETRY_MS = 25;
|
|
22
|
+
|
|
23
|
+
const LEASE_TABLE = "lease_state";
|
|
24
|
+
const LEASE_TABLE_SQL = `CREATE TABLE IF NOT EXISTS ${LEASE_TABLE} (
|
|
25
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
26
|
+
holder TEXT NOT NULL,
|
|
27
|
+
pid INTEGER NOT NULL,
|
|
28
|
+
started_at TEXT NOT NULL
|
|
29
|
+
)`;
|
|
30
|
+
|
|
31
|
+
/** Label committed by the process that currently holds the lease. */
|
|
32
|
+
export interface WriterLeaseMetadata {
|
|
33
|
+
holder: string;
|
|
34
|
+
pid: number;
|
|
35
|
+
started_at: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface WriterLease {
|
|
39
|
+
/** Sibling lock database holding the lease. */
|
|
40
|
+
readonly lockPath: string;
|
|
41
|
+
/** Label this holder committed when it took the lease. */
|
|
42
|
+
readonly metadata: WriterLeaseMetadata;
|
|
43
|
+
/** Idempotent release: rollback the held transaction, then close. */
|
|
44
|
+
close(): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface WriterLeaseOptions {
|
|
48
|
+
/** Bounded wait for a contended lease; `0` tries once (default 250 ms). */
|
|
49
|
+
waitMs?: number;
|
|
50
|
+
/** Verb or service class recorded in the lease label. */
|
|
51
|
+
holder: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Lease path derived from the store path; a sibling, never the store itself. */
|
|
55
|
+
export function writerLockPath(dbPath: string): string {
|
|
56
|
+
return `${dbPath}-write.lock`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* In-memory stores are process-local: there is no file to derive a lock from and
|
|
61
|
+
* no other process to arbitrate with, so writers skip the lease. Only SQLite's
|
|
62
|
+
* URI forms count — `:memory:`, `file::memory:…`, and a `file:` URI whose query
|
|
63
|
+
* asks for `mode=memory` — never a filename that merely contains those words.
|
|
64
|
+
*/
|
|
65
|
+
export function isMemoryStorePath(dbPath: string): boolean {
|
|
66
|
+
if (dbPath === ":memory:" || dbPath.startsWith("file::memory:")) return true;
|
|
67
|
+
if (!dbPath.startsWith("file:") || !dbPath.includes("?")) return false;
|
|
68
|
+
return new URLSearchParams(dbPath.slice(dbPath.indexOf("?"))).get("mode") === "memory";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Attempts for a wait budget: one immediate try plus one per retry slot. */
|
|
72
|
+
export function writerLeaseAttempts(waitMs: number, retryMs = WRITER_LEASE_RETRY_MS): number {
|
|
73
|
+
return waitMs > 0 ? Math.ceil(waitMs / Math.max(1, retryMs)) + 1 : 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Human label for a peer's lease, used by every refusal and skip message. A null
|
|
78
|
+
* label means the peek could not read one (no holder ever committed one, or the
|
|
79
|
+
* lock database was unreadable under contention) — say exactly that instead of a
|
|
80
|
+
* vague "unknown".
|
|
81
|
+
*/
|
|
82
|
+
export function describeLeaseHolder(holder: WriterLeaseMetadata | null): string {
|
|
83
|
+
return holder ? `pid ${holder.pid}, holder ${holder.holder}` : "holder unknown (label unavailable)";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Typed refusal: another process holds the store and this writer may not take it. */
|
|
87
|
+
export class WriterLeaseBusyError extends Error {
|
|
88
|
+
readonly code = "WRITER_BUSY";
|
|
89
|
+
readonly lockPath: string;
|
|
90
|
+
/** Current label from the lock database, when it was readable. */
|
|
91
|
+
readonly holder: WriterLeaseMetadata | null;
|
|
92
|
+
|
|
93
|
+
constructor(lockPath: string, holder: WriterLeaseMetadata | null, message?: string) {
|
|
94
|
+
super(
|
|
95
|
+
message ??
|
|
96
|
+
`store is held by another writer (${describeLeaseHolder(holder)}); ` +
|
|
97
|
+
"retry shortly or stop the scheduled ledger service",
|
|
98
|
+
);
|
|
99
|
+
this.name = "WriterLeaseBusyError";
|
|
100
|
+
this.lockPath = lockPath;
|
|
101
|
+
this.holder = holder;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Take the writer lease for `dbPath`, waiting up to `waitMs` for a busy peer.
|
|
107
|
+
* Busy failures retry inside that budget; an unreadable lock database is
|
|
108
|
+
* quarantined once and retried; every other failure propagates as-is.
|
|
109
|
+
*/
|
|
110
|
+
export function acquireWriterLease(dbPath: string, options: WriterLeaseOptions): WriterLease {
|
|
111
|
+
const lockPath = writerLockPath(dbPath);
|
|
112
|
+
const waitMs = Math.max(0, options.waitMs ?? DEFAULT_WRITER_LEASE_WAIT_MS);
|
|
113
|
+
const attempts = writerLeaseAttempts(waitMs);
|
|
114
|
+
const startedAt = Date.now();
|
|
115
|
+
let repaired = false;
|
|
116
|
+
|
|
117
|
+
for (let attempt = 1; ; attempt++) {
|
|
118
|
+
let db: Database | null = null;
|
|
119
|
+
let metadata: WriterLeaseMetadata | null = null;
|
|
120
|
+
let failure: unknown = null;
|
|
121
|
+
try {
|
|
122
|
+
db = openLockDatabase(lockPath);
|
|
123
|
+
if (beginImmediate(db)) metadata = holdLease(db, options.holder);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
failure = error;
|
|
126
|
+
}
|
|
127
|
+
if (metadata && db) return heldWriterLease(db, lockPath, metadata);
|
|
128
|
+
db?.close();
|
|
129
|
+
if (failure && !isBusyError(failure)) {
|
|
130
|
+
// Only a lock database that is not a SQLite database at all is safe to
|
|
131
|
+
// rename aside; every other failure class fails closed (see below).
|
|
132
|
+
if (isNotADatabaseError(failure) && !repaired && quarantineLockDatabase(lockPath)) {
|
|
133
|
+
repaired = true;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
throw failure;
|
|
137
|
+
}
|
|
138
|
+
const remaining = waitMs - (Date.now() - startedAt);
|
|
139
|
+
if (attempt >= attempts || remaining <= 0) break;
|
|
140
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.min(WRITER_LEASE_RETRY_MS, remaining));
|
|
141
|
+
}
|
|
142
|
+
throw new WriterLeaseBusyError(lockPath, readWriterLease(dbPath));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Best-effort peek at the committed lease label. It answers while another
|
|
147
|
+
* process holds the lease (readers do not conflict with the held RESERVED
|
|
148
|
+
* lock) and returns null when no holder committed a readable label.
|
|
149
|
+
*/
|
|
150
|
+
export function readWriterLease(dbPath: string): WriterLeaseMetadata | null {
|
|
151
|
+
const lockPath = writerLockPath(dbPath);
|
|
152
|
+
if (!existsSync(lockPath)) return null;
|
|
153
|
+
let db: Database;
|
|
154
|
+
try {
|
|
155
|
+
db = new Database(lockPath, { readonly: true });
|
|
156
|
+
// The label must be readable while another process holds the lease: without
|
|
157
|
+
// this, a reader that meets a writer's commit fails and reports "holder
|
|
158
|
+
// unknown" at exactly the moment the label matters.
|
|
159
|
+
db.exec("PRAGMA busy_timeout = 250");
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
const row = db.query(`SELECT holder, pid, started_at FROM ${LEASE_TABLE} WHERE id = 1`).get() as
|
|
165
|
+
| { holder: unknown; pid: unknown; started_at: unknown }
|
|
166
|
+
| null;
|
|
167
|
+
if (!row || typeof row.holder !== "string" || !Number.isSafeInteger(row.pid) ||
|
|
168
|
+
typeof row.started_at !== "string") {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
return { holder: row.holder, pid: row.pid as number, started_at: row.started_at };
|
|
172
|
+
} catch {
|
|
173
|
+
// A fresh, empty, or conflicting lock database has no readable label yet.
|
|
174
|
+
return null;
|
|
175
|
+
} finally {
|
|
176
|
+
db.close();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function openLockDatabase(lockPath: string): Database {
|
|
181
|
+
ensurePrivateDatabase(lockPath);
|
|
182
|
+
const db = new Database(lockPath);
|
|
183
|
+
// Disable SQLite's own wait loop: the caller's clock and attempt budget are
|
|
184
|
+
// the only timeouts in play.
|
|
185
|
+
db.exec("PRAGMA busy_timeout = 0");
|
|
186
|
+
return db;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Called while holding the write lock: commit the label, then take the
|
|
191
|
+
* uncommitted transaction that the lease actually is. Returns null when a
|
|
192
|
+
* contender won the commit-to-hold gap, which is a busy attempt, not a lease.
|
|
193
|
+
*/
|
|
194
|
+
function holdLease(db: Database, holder: string): WriterLeaseMetadata | null {
|
|
195
|
+
const metadata: WriterLeaseMetadata = { holder, pid: process.pid, started_at: new Date().toISOString() };
|
|
196
|
+
try {
|
|
197
|
+
db.exec(LEASE_TABLE_SQL);
|
|
198
|
+
// Row id=1 is the single label slot: a stale label (a killed holder, a
|
|
199
|
+
// skipped clear) must be replaced, never collide with this holder.
|
|
200
|
+
db.query(`INSERT OR REPLACE INTO ${LEASE_TABLE} (id, holder, pid, started_at) VALUES (1, ?, ?, ?)`)
|
|
201
|
+
.run(metadata.holder, metadata.pid, metadata.started_at);
|
|
202
|
+
db.exec("COMMIT");
|
|
203
|
+
} catch (error) {
|
|
204
|
+
// The label is bookkeeping, the lock is the lease: keep holding either way.
|
|
205
|
+
try {
|
|
206
|
+
db.exec("ROLLBACK");
|
|
207
|
+
} catch {
|
|
208
|
+
// Closing the connection releases any remaining SQLite lock.
|
|
209
|
+
}
|
|
210
|
+
if (isBusyError(error)) throw error;
|
|
211
|
+
console.error(
|
|
212
|
+
"[ledger] writer lease label unavailable " +
|
|
213
|
+
`(${error instanceof Error ? error.message : String(error)}); holding the store unlabelled`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
if (beginImmediate(db)) return metadata;
|
|
217
|
+
// A contender slipped into the commit-to-hold gap: drop our own label and
|
|
218
|
+
// report a busy attempt instead of a half-held lease.
|
|
219
|
+
clearLeaseLabel(db, metadata.pid);
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function heldWriterLease(db: Database, lockPath: string, metadata: WriterLeaseMetadata): WriterLease {
|
|
224
|
+
let released = false;
|
|
225
|
+
return {
|
|
226
|
+
lockPath,
|
|
227
|
+
metadata,
|
|
228
|
+
close(): void {
|
|
229
|
+
if (released) return;
|
|
230
|
+
released = true;
|
|
231
|
+
try {
|
|
232
|
+
db.exec("ROLLBACK");
|
|
233
|
+
} catch {
|
|
234
|
+
// Closing the connection releases any remaining SQLite lock.
|
|
235
|
+
}
|
|
236
|
+
// Best-effort: drop our own label so a later peek does not name a
|
|
237
|
+
// released holder. A successor's label is never touched.
|
|
238
|
+
clearLeaseLabel(db, metadata.pid);
|
|
239
|
+
db.close();
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Drop the label only when it is still this process's own: an unconditional
|
|
246
|
+
* delete would erase the label a successor just committed, leaving contended
|
|
247
|
+
* refusals unable to name a holder.
|
|
248
|
+
*/
|
|
249
|
+
function clearLeaseLabel(db: Database, pid: number): void {
|
|
250
|
+
try {
|
|
251
|
+
if (!beginImmediate(db)) return;
|
|
252
|
+
db.query(`DELETE FROM ${LEASE_TABLE} WHERE id = 1 AND pid = ?`).run(pid);
|
|
253
|
+
db.exec("COMMIT");
|
|
254
|
+
} catch {
|
|
255
|
+
// Bookkeeping only: the lock, not the label, is the lease.
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Rename an unreadable lock database aside so it cannot wedge every writer
|
|
261
|
+
* forever. The bytes are kept for inspection, the path is never unlinked, and
|
|
262
|
+
* only a regular file is touched.
|
|
263
|
+
*
|
|
264
|
+
* Renaming is safe *because* of how narrow the caller keeps the condition: only
|
|
265
|
+
* SQLITE_NOTADB (the path is not a SQLite database at all) reaches here. A peer
|
|
266
|
+
* that genuinely holds the lease necessarily holds a valid SQLite write lock on
|
|
267
|
+
* a valid database file, so a NOTADB path cannot be a live hold and this rename
|
|
268
|
+
* cannot split one live inode between two holders. Every ambiguous failure —
|
|
269
|
+
* permissions, I/O errors, an unexpected path type, any other open failure —
|
|
270
|
+
* fails closed instead: the caller rethrows it untouched and never renames.
|
|
271
|
+
*/
|
|
272
|
+
function quarantineLockDatabase(lockPath: string): boolean {
|
|
273
|
+
const target = `${lockPath}.corrupt-${Date.now()}`;
|
|
274
|
+
try {
|
|
275
|
+
if (!statSync(lockPath).isFile()) return false;
|
|
276
|
+
renameSync(lockPath, target);
|
|
277
|
+
} catch {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
console.error(
|
|
281
|
+
`[ledger] writer lease lock database is not a SQLite database; ` +
|
|
282
|
+
`quarantined as ${target} and retried`,
|
|
283
|
+
);
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function beginImmediate(db: Database): boolean {
|
|
288
|
+
try {
|
|
289
|
+
db.exec("BEGIN IMMEDIATE");
|
|
290
|
+
return true;
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (isBusyError(error)) return false;
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function isBusyError(error: unknown): boolean {
|
|
298
|
+
const code = (error as { code?: unknown } | null | undefined)?.code;
|
|
299
|
+
if (typeof code === "string" && code.startsWith("SQLITE_BUSY")) return true;
|
|
300
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
301
|
+
return /SQLITE_BUSY|database is locked|database is busy/i.test(message);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** The one failure class that proves no valid holder can be using this file. */
|
|
305
|
+
function isNotADatabaseError(error: unknown): boolean {
|
|
306
|
+
const code = (error as { code?: unknown } | null | undefined)?.code;
|
|
307
|
+
if (code === "SQLITE_NOTADB") return true;
|
|
308
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
309
|
+
return /file is not a database|file is encrypted or is not a database/i.test(message);
|
|
310
|
+
}
|
package/ops/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Trajectories installation and scheduled indexing
|
|
2
|
+
|
|
3
|
+
These operations port Ledger's CLI and OS-supervised bounded lexical sync.
|
|
4
|
+
CLI and release installation leave the existing database and native histories
|
|
5
|
+
unchanged. Scheduled native sync remains separate from indexing a managed
|
|
6
|
+
session/log store, which requires an explicit `index --store` command.
|
|
7
|
+
|
|
8
|
+
## Install the CLI
|
|
9
|
+
|
|
10
|
+
### Native CLI
|
|
11
|
+
|
|
12
|
+
Native releases require no Bun, Node or npm. The repository is currently private,
|
|
13
|
+
so GitHub downloads require an authenticated `gh` client with repository access.
|
|
14
|
+
On an Apple Silicon Mac:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
gh release download 'trajectories@0.1.2' --repo archeism/oh-my-ai \
|
|
18
|
+
--pattern 'trajectories-0.1.2-darwin-arm64.tar.gz*'
|
|
19
|
+
shasum -a 256 --check trajectories-0.1.2-darwin-arm64.tar.gz.sha256
|
|
20
|
+
mkdir -p "$HOME/.local/share/trajectories/native"
|
|
21
|
+
tar -xzf trajectories-0.1.2-darwin-arm64.tar.gz -C "$HOME/.local/share/trajectories/native"
|
|
22
|
+
bash "$HOME/.local/share/trajectories/native/trajectories-0.1.2-darwin-arm64/install.sh"
|
|
23
|
+
trajectories --version
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
On Linux, choose `linux-x64` or `linux-arm64`, and use `sha256sum --check`.
|
|
27
|
+
The installer refuses existing commands unless `--force` explicitly authorizes
|
|
28
|
+
replacement. It creates a symlink to the retained release directory; do not
|
|
29
|
+
delete that directory or separate the executable from `lib/`. It does not change
|
|
30
|
+
the search database or enable an indexing service. Ensure `~/.local/bin` is on PATH.
|
|
31
|
+
Local ONNX inference requires glibc 2.28+ on Linux or macOS 14+ on Apple Silicon. Deep Agents SQLite conversion
|
|
32
|
+
still uses the harness's own Python/LangGraph environment.
|
|
33
|
+
|
|
34
|
+
The service installers below are for source/npm installations and invoke Bun.
|
|
35
|
+
For a native service, its command is the installed `trajectories sync` executable
|
|
36
|
+
directly; the CLI's bounded sync and scheduler protocol are unchanged.
|
|
37
|
+
|
|
38
|
+
### npm or source CLI
|
|
39
|
+
|
|
40
|
+
For the npm distribution, use Bun 1.4.2 or newer:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
bun add --global trajectories@0.1.2
|
|
44
|
+
trajectories --version
|
|
45
|
+
trajectories status --robot
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Put Bun's global bin directory on PATH (`bun pm bin -g`). Packaged `ops/` and
|
|
49
|
+
`skills/` live under `~/.bun/install/global/node_modules/trajectories`
|
|
50
|
+
with the default Bun layout; resolve the installed package location before
|
|
51
|
+
using its OS service installers. The Git-only `install-release.sh` below is
|
|
52
|
+
for source checkouts, not npm installations.
|
|
53
|
+
|
|
54
|
+
Use Bun 1.4.2 or newer. From a monorepo checkout with dependencies installed and the SDK built:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
bun install --frozen-lockfile
|
|
58
|
+
bun run --cwd packages/trajectories build
|
|
59
|
+
bash packages/trajectories/ops/install-cli.sh --bin-dir "$HOME/.local/bin"
|
|
60
|
+
trajectories status --json
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
This creates a worktree-following `trajectories` wrapper. Existing unrelated
|
|
64
|
+
executables are refused unless `--force` is supplied. `TRAJECTORIES_BUN` and
|
|
65
|
+
`TRAJECTORIES_BIN_DIR` override Bun and the target directory; their `LEDGER_*`
|
|
66
|
+
equivalents remain fallback inputs. Installation leaves the `ledger` executable
|
|
67
|
+
and any running Ledger service untouched.
|
|
68
|
+
The wrapper prefers the checkout's `src/cli.ts`; a packed package uses
|
|
69
|
+
`dist/cli.js`. The service installers use the same entrypoint selection.
|
|
70
|
+
Both require `dist/index.js`: the source search/managed-session commands load
|
|
71
|
+
the built SDK. An unbuilt checkout fails before writing a wrapper or service
|
|
72
|
+
definition, with an explicit build command. Installers do not build silently.
|
|
73
|
+
Rebuild after changing SDK source.
|
|
74
|
+
|
|
75
|
+
For reviewed committed bytes, rather than the live worktree:
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
bash packages/trajectories/ops/install-release.sh REVIEWED_COMMIT --bin-dir "$HOME/.local/bin"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The release gate uses Bun 1.4.2 and stores a full reviewed monorepo archive under
|
|
82
|
+
`${TRAJECTORIES_RELEASES_DIR:-${XDG_DATA_HOME:-~/.local/share}/trajectories/releases}/COMMIT`.
|
|
83
|
+
`LEDGER_RELEASES_DIR` remains a fallback. At the audited `7eecb3b` checkpoint the
|
|
84
|
+
tracked archive was approximately 126 MB; dependencies are additional. The root
|
|
85
|
+
frozen lockfile refers to other workspace manifests and the patched Letta
|
|
86
|
+
dependency, so the reviewed archive retains them. The install filters to the
|
|
87
|
+
Trajectories dependency graph with lifecycle scripts disabled, then builds only
|
|
88
|
+
Trajectories. Unrelated root postinstall builds do not run. This is a source
|
|
89
|
+
release path; npm installation uses the separately packed package.
|
|
90
|
+
|
|
91
|
+
Release publication is atomic after dependency installation and build. An
|
|
92
|
+
incomplete, foreign, or symlink release directory is refused. The commit marker
|
|
93
|
+
must match the directory's resolved commit. Reinstallation reuses a completed
|
|
94
|
+
release; do not modify its files. The installer never enables a service.
|
|
95
|
+
|
|
96
|
+
## Scheduled local indexing
|
|
97
|
+
|
|
98
|
+
Keep the original database path during cutover:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
export TRAJECTORIES_DB="$HOME/.local/share/ledger/ledger.sqlite"
|
|
102
|
+
trajectories status --json
|
|
103
|
+
trajectories sync --robot
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The database is `TRAJECTORIES_DB`, then `LEDGER_DB`, then
|
|
107
|
+
`${XDG_DATA_HOME:-~/.local/share}/ledger/ledger.sqlite`. `TRAJECTORIES_DATA_DIR`
|
|
108
|
+
(`LEDGER_DATA_DIR` fallback) changes the fallback directory. Managed sessions
|
|
109
|
+
under `TRAJECTORIES_HOME` are a separate store.
|
|
110
|
+
|
|
111
|
+
Stop the previous Ledger writer before enabling a replacement against that
|
|
112
|
+
database. The installers operate only on the new Trajectories unit/label and
|
|
113
|
+
do not stop the old one for you.
|
|
114
|
+
|
|
115
|
+
The first writable `sync` can upgrade a compatible older search database to
|
|
116
|
+
schema 16. Preserve one consistent SQLite backup before that cutover: read-only
|
|
117
|
+
commands serve schema 15 without upgrading it, but the old Ledger writer cannot
|
|
118
|
+
write the upgraded database. The [Mac cutover receipt](../evidence/mac-cutover-2026-10-07/README.md)
|
|
119
|
+
records a restored schema-15 rollback and the observed writer migration.
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
# Linux: writes private units and reloads systemd; default does not enable them.
|
|
123
|
+
bash packages/trajectories/ops/linux/install-systemd-user.sh --write-only
|
|
124
|
+
systemctl --user cat archeism-trajectories.service archeism-trajectories.timer
|
|
125
|
+
# After inspecting the definition and one bounded manual pass:
|
|
126
|
+
bash packages/trajectories/ops/linux/install-systemd-user.sh --enable-now
|
|
127
|
+
|
|
128
|
+
# macOS: writes private logs/plist and enables the 15-minute RunAtLoad job.
|
|
129
|
+
bash packages/trajectories/ops/macos/install-launchd.sh
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The Linux installer reads `${TRAJECTORIES_ENV_FILE:-${LEDGER_ENV_FILE:-${XDG_CONFIG_HOME:-~/.config}/ledger/daemon.env}}`
|
|
133
|
+
when present. On both systems, every `TRAJECTORIES_*` sync setting takes
|
|
134
|
+
precedence over the equivalent `LEDGER_*` setting. Defaults retain Ledger's
|
|
135
|
+
availability bounds: 25 sessions, 64 MiB source work, 120-second pass, three
|
|
136
|
+
retries, eight attempts/session, 8 MiB/s aggregate reads, 30-second worker
|
|
137
|
+
deadline, two Linux workers (one macOS worker), five reserved fresh sessions,
|
|
138
|
+
one-hour freshness window, 1 MiB chunks, automatic pressure response,
|
|
139
|
+
five-second maintenance and 64 FTS merge pages. Defaults cover all 18 current
|
|
140
|
+
history readers. `ops/sync-settings.sh` lists the exact setting names.
|
|
141
|
+
|
|
142
|
+
The supervised command is lexical `sync` every 15 minutes, with no restart
|
|
143
|
+
loop. Linux also sets 1 GiB memory, 50% CPU and 16-task limits. The retained
|
|
144
|
+
engine intentionally receives `LEDGER_SCHEDULER=1` so scheduled passes yield
|
|
145
|
+
to another writer. Foreground `watch` remains available; it is not the service
|
|
146
|
+
command. Pending work is retained for later passes, rather than treated as a
|
|
147
|
+
complete archive.
|
|
148
|
+
|
|
149
|
+
Fixtures exercise temporary homes, fake OS supervisors and synthetic databases;
|
|
150
|
+
tests never install real units or modify the global CLI.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Install a worktree-following trajectories wrapper. Does not install dependencies
|
|
3
|
+
# or enable services. Usage: ops/install-cli.sh [--bin-dir DIR] [--force]
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SOURCE="${BASH_SOURCE[0]}"
|
|
7
|
+
while [[ -h "$SOURCE" ]]; do
|
|
8
|
+
DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
|
|
9
|
+
SOURCE="$(readlink "$SOURCE")"
|
|
10
|
+
[[ "$SOURCE" == /* ]] || SOURCE="$DIR/$SOURCE"
|
|
11
|
+
done
|
|
12
|
+
PACKAGE_DIR="$(cd -P "$(dirname "$SOURCE")/.." && pwd)"
|
|
13
|
+
CLI_PATH="$PACKAGE_DIR/src/cli.ts"
|
|
14
|
+
[[ -f "$CLI_PATH" ]] || CLI_PATH="$PACKAGE_DIR/dist/cli.js"
|
|
15
|
+
BIN_DIR="${TRAJECTORIES_BIN_DIR:-${LEDGER_BIN_DIR:-$HOME/.local/bin}}"
|
|
16
|
+
FORCE=0
|
|
17
|
+
usage() { echo 'Usage: install-cli.sh [--bin-dir DIR] [--force]'; }
|
|
18
|
+
while [[ $# -gt 0 ]]; do
|
|
19
|
+
case "$1" in
|
|
20
|
+
--bin-dir)
|
|
21
|
+
[[ $# -ge 2 && -n "$2" ]] || { echo '--bin-dir requires a directory.' >&2; exit 1; }
|
|
22
|
+
BIN_DIR="$2"; shift 2 ;;
|
|
23
|
+
--bin-dir=*)
|
|
24
|
+
BIN_DIR="${1#--bin-dir=}"
|
|
25
|
+
[[ -n "$BIN_DIR" ]] || { echo '--bin-dir requires a directory.' >&2; exit 1; }
|
|
26
|
+
shift ;;
|
|
27
|
+
--force) FORCE=1; shift ;;
|
|
28
|
+
--help|-h) usage; exit 0 ;;
|
|
29
|
+
*) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
|
|
30
|
+
esac
|
|
31
|
+
done
|
|
32
|
+
[[ -f "$CLI_PATH" ]] || { echo "Trajectories CLI not found: $CLI_PATH" >&2; exit 1; }
|
|
33
|
+
[[ -f "$PACKAGE_DIR/dist/index.js" ]] || {
|
|
34
|
+
printf 'Build Trajectories before installation: bun run --cwd %q build\n' "$PACKAGE_DIR" >&2
|
|
35
|
+
exit 1
|
|
36
|
+
}
|
|
37
|
+
BUN_BIN="${TRAJECTORIES_BUN:-${LEDGER_BUN:-$(command -v bun || true)}}"
|
|
38
|
+
[[ -n "$BUN_BIN" && -x "$BUN_BIN" ]] || { echo 'Bun executable not found; set TRAJECTORIES_BUN.' >&2; exit 1; }
|
|
39
|
+
mkdir -p "$BIN_DIR"
|
|
40
|
+
TARGET="$BIN_DIR/trajectories"
|
|
41
|
+
if [[ -e "$TARGET" || -L "$TARGET" ]]; then
|
|
42
|
+
if [[ -f "$TARGET" ]] && grep -q 'TRAJECTORIES_CLI_WRAPPER' "$TARGET" && grep -qxF "# package: $PACKAGE_DIR" "$TARGET"; then
|
|
43
|
+
:
|
|
44
|
+
elif [[ "$FORCE" == 1 && ! -d "$TARGET" ]]; then
|
|
45
|
+
echo "Replacing existing $TARGET (--force)." >&2
|
|
46
|
+
else
|
|
47
|
+
echo "Refusing to overwrite $TARGET; use --force or another --bin-dir." >&2
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
fi
|
|
51
|
+
TEMPORARY="$(mktemp "$BIN_DIR/.trajectories.XXXXXX")"
|
|
52
|
+
trap 'rm -f "$TEMPORARY"' EXIT
|
|
53
|
+
{
|
|
54
|
+
echo '#!/usr/bin/env bash'
|
|
55
|
+
echo '# TRAJECTORIES_CLI_WRAPPER v1; delete to uninstall.'
|
|
56
|
+
printf '# package: %s\n' "$PACKAGE_DIR"
|
|
57
|
+
printf 'exec %q run %q "$@"\n' "$BUN_BIN" "$CLI_PATH"
|
|
58
|
+
} > "$TEMPORARY"
|
|
59
|
+
chmod 755 "$TEMPORARY"
|
|
60
|
+
mv -f "$TEMPORARY" "$TARGET"
|
|
61
|
+
trap - EXIT
|
|
62
|
+
echo "Installed trajectories CLI wrapper: $TARGET"
|
|
63
|
+
case ":$PATH:" in *":$BIN_DIR:"*) ;; *) echo "Add $BIN_DIR to PATH to use trajectories." >&2 ;; esac
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Run from an extracted native release kept in a permanent directory.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
RELEASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
5
|
+
BIN_DIR="${TRAJECTORIES_BIN_DIR:-$HOME/.local/bin}"
|
|
6
|
+
FORCE=0
|
|
7
|
+
while [[ $# -gt 0 ]]; do
|
|
8
|
+
case "$1" in
|
|
9
|
+
--bin-dir) [[ $# -ge 2 && -n "$2" ]] || { echo '--bin-dir requires a directory.' >&2; exit 1; }; BIN_DIR="$2"; shift 2 ;;
|
|
10
|
+
--force) FORCE=1; shift ;;
|
|
11
|
+
--help|-h) echo 'Usage: install.sh [--bin-dir DIR] [--force]'; exit 0 ;;
|
|
12
|
+
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
|
13
|
+
esac
|
|
14
|
+
done
|
|
15
|
+
[[ -x "$RELEASE_DIR/trajectories" && -d "$RELEASE_DIR/lib" ]] || {
|
|
16
|
+
echo 'Keep install.sh, the trajectories executable, and lib/ together in the extracted release.' >&2; exit 1;
|
|
17
|
+
}
|
|
18
|
+
"$RELEASE_DIR/trajectories" --version
|
|
19
|
+
mkdir -p "$BIN_DIR"
|
|
20
|
+
TARGET="$BIN_DIR/trajectories"
|
|
21
|
+
if [[ -e "$TARGET" || -L "$TARGET" ]]; then
|
|
22
|
+
if [[ -L "$TARGET" && "$(readlink "$TARGET")" == "$RELEASE_DIR/trajectories" ]]; then
|
|
23
|
+
echo "Already installed: $TARGET"; exit 0
|
|
24
|
+
fi
|
|
25
|
+
[[ "$FORCE" == 1 && ! -d "$TARGET" ]] || {
|
|
26
|
+
echo "Refusing to overwrite $TARGET; use --force for an explicit cutover." >&2; exit 1;
|
|
27
|
+
}
|
|
28
|
+
fi
|
|
29
|
+
STAGE="$(mktemp -d "$BIN_DIR/.trajectories-install.XXXXXX")"
|
|
30
|
+
trap 'rm -rf "$STAGE"' EXIT
|
|
31
|
+
ln -s "$RELEASE_DIR/trajectories" "$STAGE/trajectories"
|
|
32
|
+
mv -f "$STAGE/trajectories" "$TARGET"
|
|
33
|
+
echo "Installed: $TARGET"
|
|
34
|
+
echo "Keep the release directory: $RELEASE_DIR"
|
|
35
|
+
case ":$PATH:" in *":$BIN_DIR:"*) ;; *) echo "Add $BIN_DIR to PATH." ;; esac
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Install a reviewed monorepo revision into a SHA-pinned directory, never the
|
|
3
|
+
# live worktree. Usage: ops/install-release.sh REF [--bin-dir DIR] [--force]
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
umask 077
|
|
6
|
+
if [[ $# == 0 || "$1" == --help || "$1" == -h ]]; then
|
|
7
|
+
echo 'Usage: install-release.sh REF [--bin-dir DIR] [--force]'
|
|
8
|
+
echo 'Requires git, tar and the verified Bun 1.4.2 runtime; does not enable services.'
|
|
9
|
+
exit 0
|
|
10
|
+
fi
|
|
11
|
+
REF="$1"; shift
|
|
12
|
+
PACKAGE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
|
13
|
+
REPO="$(git -C "$PACKAGE_DIR" rev-parse --show-toplevel)"
|
|
14
|
+
BUN_BIN="${TRAJECTORIES_BUN:-${LEDGER_BUN:-$(command -v bun || true)}}"
|
|
15
|
+
[[ -n "$BUN_BIN" && -x "$BUN_BIN" ]] || { echo 'Bun is required; set TRAJECTORIES_BUN.' >&2; exit 1; }
|
|
16
|
+
[[ "$("$BUN_BIN" --version)" == 1.4.2 ]] || { echo 'This release gate is verified with Bun 1.4.2; use that runtime.' >&2; exit 1; }
|
|
17
|
+
SHA="$(git -C "$REPO" rev-parse --verify --end-of-options "${REF}^{commit}")"
|
|
18
|
+
RELEASES="${TRAJECTORIES_RELEASES_DIR:-${LEDGER_RELEASES_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/trajectories/releases}}"
|
|
19
|
+
mkdir -p "$RELEASES"
|
|
20
|
+
[[ ! -L "$RELEASES" ]] || { echo 'Refusing a symlink release directory.' >&2; exit 1; }
|
|
21
|
+
chmod 700 "$RELEASES"
|
|
22
|
+
TARGET="$RELEASES/$SHA"
|
|
23
|
+
STAGE=''
|
|
24
|
+
trap '[[ -z "$STAGE" ]] || rm -rf "$STAGE"' EXIT
|
|
25
|
+
if [[ -e "$TARGET" || -L "$TARGET" ]]; then
|
|
26
|
+
[[ ! -L "$TARGET" && -f "$TARGET/.trajectories-release" && "$(cat "$TARGET/.trajectories-release")" == "$SHA" && -f "$TARGET/packages/trajectories/src/cli.ts" && -f "$TARGET/packages/trajectories/ops/install-cli.sh" ]] || {
|
|
27
|
+
echo "Refusing an incomplete or foreign release directory: $TARGET" >&2; exit 1;
|
|
28
|
+
}
|
|
29
|
+
else
|
|
30
|
+
STAGE="$(mktemp -d "$RELEASES/.install.XXXXXX")"
|
|
31
|
+
# The root lockfile references workspace manifests and dependency patches.
|
|
32
|
+
# Preserve the reviewed monorepo, then install only Trajectories' graph.
|
|
33
|
+
git -C "$REPO" archive --format=tar "$SHA" | tar -xf - -C "$STAGE"
|
|
34
|
+
[[ -f "$STAGE/bun.lock" && -f "$STAGE/package.json" && -f "$STAGE/packages/trajectories/package.json" && -f "$STAGE/packages/trajectories/ops/install-cli.sh" ]] || {
|
|
35
|
+
echo 'Revision lacks the supported Trajectories installer and frozen monorepo lockfile.' >&2; exit 1;
|
|
36
|
+
}
|
|
37
|
+
(
|
|
38
|
+
cd "$STAGE"
|
|
39
|
+
"$BUN_BIN" install --filter trajectories --frozen-lockfile --ignore-scripts
|
|
40
|
+
cd packages/trajectories
|
|
41
|
+
"$BUN_BIN" run build
|
|
42
|
+
)
|
|
43
|
+
printf '%s\n' "$SHA" > "$STAGE/.trajectories-release"
|
|
44
|
+
[[ ! -e "$TARGET" && ! -L "$TARGET" ]] || { echo 'Another installer completed this revision; retry.' >&2; exit 1; }
|
|
45
|
+
mv "$STAGE" "$TARGET"
|
|
46
|
+
STAGE=''
|
|
47
|
+
fi
|
|
48
|
+
TRAJECTORIES_BUN="$BUN_BIN" bash "$TARGET/packages/trajectories/ops/install-cli.sh" "$@"
|
|
49
|
+
printf 'Pinned revision: %s\nRelease directory: %s\n' "$SHA" "$TARGET"
|