@memfork/cli 0.1.36 → 0.1.38
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/dist/cli.js +14 -2
- package/dist/commands/doctor.js +36 -0
- package/dist/commands/ops.d.ts +21 -0
- package/dist/commands/ops.js +93 -2
- package/dist/commands/ui-server.js +5 -5
- package/dist/config.d.ts +27 -0
- package/dist/config.js +18 -0
- package/package.json +1 -1
- package/plugins/codex/skills/memory-fork/SKILL.md +39 -11
- package/plugins/cursor/rules/memforks.mdc +25 -6
- package/ui/assets/index-CLAZlKE4.css +1 -0
- package/ui/assets/index-CXGCH_Kj.js +67 -0
- package/ui/assets/{index-NqfJeLDL.js.map → index-CXGCH_Kj.js.map} +1 -1
- package/ui/index.html +2 -2
- package/ui/assets/index-4y8Erh-7.css +0 -1
- package/ui/assets/index-NqfJeLDL.js +0 -67
package/dist/cli.js
CHANGED
|
@@ -25,7 +25,7 @@ const { version } = createRequire(import.meta.url)("../package.json");
|
|
|
25
25
|
import { cmdInit } from "./commands/init.js";
|
|
26
26
|
import { cmdDoctor, cmdDoctorEnv } from "./commands/doctor.js";
|
|
27
27
|
import { cmdInstall } from "./commands/install.js";
|
|
28
|
-
import { cmdStatus, cmdLog, cmdRecall, cmdCommit, cmdMerge, cmdProposals, cmdResolverCreate, cmdPrComment, cmdUi, cmdShow, cmdDiff, cmdDelegates, cmdGrant, cmdGrantMemwal, cmdRevoke, cmdBranch, cmdCheckout, } from "./commands/ops.js";
|
|
28
|
+
import { cmdStatus, cmdLog, cmdRecall, cmdCommit, cmdCat, cmdMerge, cmdProposals, cmdResolverCreate, cmdPrComment, cmdUi, cmdShow, cmdDiff, cmdDelegates, cmdGrant, cmdGrantMemwal, cmdRevoke, cmdBranch, cmdCheckout, } from "./commands/ops.js";
|
|
29
29
|
import { cmdJoin } from "./commands/join.js";
|
|
30
30
|
const program = new Command();
|
|
31
31
|
program
|
|
@@ -87,7 +87,19 @@ program
|
|
|
87
87
|
.option("-f, --facts <facts...>", "one or more fact strings")
|
|
88
88
|
.option("--from-response <text>", "extract facts from a full response text")
|
|
89
89
|
.option("--auto-extract", "use LLM to extract durable facts (requires --from-response)")
|
|
90
|
-
.
|
|
90
|
+
.option("--file <path>", "attach a file as a Walrus artifact (repeatable). Requires artifacts.enabled = true in config.", (v, acc) => [...acc, v], [])
|
|
91
|
+
.option("--epochs <n>", "Walrus storage epochs for this commit's artifacts (overrides config default)", parseInt)
|
|
92
|
+
.action(wrap((opts) => cmdCommit({
|
|
93
|
+
...opts,
|
|
94
|
+
files: opts.file?.map((p) => ({ filePath: p, epochs: opts.epochs })),
|
|
95
|
+
})));
|
|
96
|
+
program
|
|
97
|
+
.command("cat <blobId>")
|
|
98
|
+
.description("retrieve an artifact from Walrus by its blob ID")
|
|
99
|
+
.option("-o, --output <path>", "write bytes to this file (default: print to stdout)")
|
|
100
|
+
.option("--sha256 <hex>", "verify integrity against this SHA-256 hex digest")
|
|
101
|
+
.option("--network <name>", "Walrus network: mainnet (default) or testnet")
|
|
102
|
+
.action(wrap((blobId, opts) => cmdCat(blobId, opts)));
|
|
91
103
|
program
|
|
92
104
|
.command("merge <from> <into>")
|
|
93
105
|
.description("merge memory from one branch into another")
|
package/dist/commands/doctor.js
CHANGED
|
@@ -175,6 +175,42 @@ export async function cmdDoctor() {
|
|
|
175
175
|
fix: "Check your network. MemWal read/write will be unavailable.",
|
|
176
176
|
});
|
|
177
177
|
}
|
|
178
|
+
// ── 8. Artifact storage (optional) ──────────────────────────────────────────
|
|
179
|
+
if (cfg.artifacts.enabled) {
|
|
180
|
+
// Artifacts require WAL tokens. Fetch the signer's WAL coin balance.
|
|
181
|
+
// The WAL package ID is identical on mainnet and testnet.
|
|
182
|
+
// Source: https://docs.wal.app/docs/network-reference
|
|
183
|
+
const walType = "0x356a26eb9e012a68958082340d4c4116e7f55615cf27affcff209cf0ae544f59::wal::WAL";
|
|
184
|
+
try {
|
|
185
|
+
const addr = client.keypair.toSuiAddress();
|
|
186
|
+
const walBalance = await client.suiClient.getBalance({ owner: addr, coinType: walType });
|
|
187
|
+
const walAmount = Number(walBalance.totalBalance) / 1e9;
|
|
188
|
+
const walLow = walAmount < 0.5;
|
|
189
|
+
checks.push({
|
|
190
|
+
label: "Artifact storage (WAL balance)",
|
|
191
|
+
status: walLow ? "warn" : "ok",
|
|
192
|
+
detail: `${walAmount.toFixed(4)} WAL (artifacts.enabled = true)`,
|
|
193
|
+
fix: walLow
|
|
194
|
+
? `Fund ${addr.slice(0, 10)}… with WAL on ${cfg.network}. See docs/architecture/artifacts.md.`
|
|
195
|
+
: undefined,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
checks.push({
|
|
200
|
+
label: "Artifact storage (WAL balance)",
|
|
201
|
+
status: "warn",
|
|
202
|
+
detail: "could not fetch WAL balance",
|
|
203
|
+
fix: "Check your Sui RPC connection.",
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
checks.push({
|
|
209
|
+
label: "Artifact storage",
|
|
210
|
+
status: "skip",
|
|
211
|
+
detail: "disabled (set artifacts.enabled = true to enable Walrus artifact persistence)",
|
|
212
|
+
});
|
|
213
|
+
}
|
|
178
214
|
// ── Print all checks ─────────────────────────────────────────────────────────
|
|
179
215
|
console.log("");
|
|
180
216
|
checks.forEach(printCheck);
|
package/dist/commands/ops.d.ts
CHANGED
|
@@ -18,6 +18,27 @@ export declare function cmdCommit(opts: {
|
|
|
18
18
|
facts?: string[];
|
|
19
19
|
fromResponse?: string;
|
|
20
20
|
autoExtract?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Files to persist as Walrus artifacts attached to this commit.
|
|
23
|
+
* Each entry is { filePath: local FS path, mime?: MIME type, epochs?: override }.
|
|
24
|
+
* Requires artifacts.enabled = true in config and a WAL-funded keypair.
|
|
25
|
+
*/
|
|
26
|
+
files?: Array<{
|
|
27
|
+
filePath: string;
|
|
28
|
+
mime?: string;
|
|
29
|
+
epochs?: number;
|
|
30
|
+
}>;
|
|
31
|
+
}): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* `memfork cat <blobId>` — retrieve an artifact blob from Walrus.
|
|
34
|
+
*
|
|
35
|
+
* Writes bytes to `--output <path>` or, when no output is specified, prints
|
|
36
|
+
* the blob as UTF-8 text. Pass `--sha256 <hex>` to verify integrity.
|
|
37
|
+
*/
|
|
38
|
+
export declare function cmdCat(blobId: string, opts?: {
|
|
39
|
+
output?: string;
|
|
40
|
+
sha256?: string;
|
|
41
|
+
network?: string;
|
|
21
42
|
}): Promise<void>;
|
|
22
43
|
export declare function cmdMerge(from: string, into: string, opts: {
|
|
23
44
|
resolver?: string;
|
package/dist/commands/ops.js
CHANGED
|
@@ -129,18 +129,109 @@ export async function cmdCommit(opts) {
|
|
|
129
129
|
console.error(chalk.red("No facts to commit. Pass --facts or --from-response."));
|
|
130
130
|
process.exit(1);
|
|
131
131
|
}
|
|
132
|
-
|
|
133
|
-
const
|
|
132
|
+
// Build artifact descriptors from --file flags.
|
|
133
|
+
const artifacts = [];
|
|
134
|
+
if (opts.files && opts.files.length > 0) {
|
|
135
|
+
if (!cfg.artifacts.enabled) {
|
|
136
|
+
console.error(chalk.red("Artifact storage is disabled.") +
|
|
137
|
+
chalk.dim("\n Set artifacts.enabled = true in .memfork/config.json and fund your signer with WAL.\n" +
|
|
138
|
+
" See docs/architecture/artifacts.md for setup instructions."));
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
for (const f of opts.files) {
|
|
142
|
+
if (!fs.existsSync(f.filePath)) {
|
|
143
|
+
console.error(chalk.red(`File not found: ${f.filePath}`));
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
const bytes = new Uint8Array(fs.readFileSync(f.filePath));
|
|
147
|
+
artifacts.push({
|
|
148
|
+
path: path.basename(f.filePath),
|
|
149
|
+
bytes,
|
|
150
|
+
mime: f.mime,
|
|
151
|
+
epochs: f.epochs,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const { blobId, artifacts: refs } = await client.commit(branch, {
|
|
156
|
+
facts,
|
|
157
|
+
message: opts.message,
|
|
158
|
+
...(artifacts.length > 0 ? { artifacts } : {}),
|
|
159
|
+
});
|
|
160
|
+
const out = { blobId, branch, artifacts: refs };
|
|
134
161
|
if (process.stdout.isTTY) {
|
|
135
162
|
console.log("");
|
|
136
163
|
console.log(chalk.green("✓") + " Committed to " + chalk.bold(branch));
|
|
137
164
|
console.log(chalk.dim(` blob: ${blobId}`));
|
|
165
|
+
if (refs.length > 0) {
|
|
166
|
+
console.log("");
|
|
167
|
+
console.log(chalk.bold(" Artifacts:"));
|
|
168
|
+
for (const ref of refs) {
|
|
169
|
+
console.log(` ${chalk.cyan(ref.path)} ` +
|
|
170
|
+
chalk.dim(`${(ref.size / 1024).toFixed(1)} KiB `) +
|
|
171
|
+
chalk.dim(`blob: ${ref.blobId.slice(0, 16)}… `) +
|
|
172
|
+
chalk.dim(`sha256: ${ref.sha256.slice(0, 16)}…`));
|
|
173
|
+
console.log(chalk.dim(` Retrieve with: `) +
|
|
174
|
+
chalk.white(`memfork cat ${ref.blobId} --output ${ref.path} --sha256 ${ref.sha256}`));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
138
177
|
console.log("");
|
|
139
178
|
}
|
|
140
179
|
else {
|
|
141
180
|
console.log(JSON.stringify(out));
|
|
142
181
|
}
|
|
143
182
|
}
|
|
183
|
+
// ─── cat ──────────────────────────────────────────────────────────────────────
|
|
184
|
+
/**
|
|
185
|
+
* `memfork cat <blobId>` — retrieve an artifact blob from Walrus.
|
|
186
|
+
*
|
|
187
|
+
* Writes bytes to `--output <path>` or, when no output is specified, prints
|
|
188
|
+
* the blob as UTF-8 text. Pass `--sha256 <hex>` to verify integrity.
|
|
189
|
+
*/
|
|
190
|
+
export async function cmdCat(blobId, opts = {}) {
|
|
191
|
+
// Basic blobId sanity: Walrus blob IDs are base64url (~43 chars) or integer strings.
|
|
192
|
+
if (!blobId || blobId.length < 8 || /\s/.test(blobId)) {
|
|
193
|
+
console.error(chalk.red(`Invalid blob ID: "${blobId}". Expected a Walrus blob ID (base64url).`));
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
const { getArtifact, ArtifactStorageError } = await import("@memfork/core");
|
|
197
|
+
const network = (opts.network ?? resolveConfig().network ?? "mainnet");
|
|
198
|
+
process.stdout.write(chalk.dim(`Fetching blob ${blobId.slice(0, 16)}… from Walrus (${network}) `));
|
|
199
|
+
// sha256 is optional: when omitted, getArtifact skips the integrity check.
|
|
200
|
+
let bytes;
|
|
201
|
+
try {
|
|
202
|
+
bytes = await getArtifact({ blobId, sha256: opts.sha256 ?? "" }, network);
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
console.log(chalk.red("failed"));
|
|
206
|
+
if (err instanceof ArtifactStorageError) {
|
|
207
|
+
// Structured errors: print the reason-specific message cleanly.
|
|
208
|
+
console.error("\n" + chalk.red(err.message) + "\n");
|
|
209
|
+
if (err.reason === "not_found") {
|
|
210
|
+
console.error(chalk.dim(" Tip: re-run with --sha256 to verify integrity once found,\n" +
|
|
211
|
+
" or check the original commit with: memfork log"));
|
|
212
|
+
}
|
|
213
|
+
else if (err.reason === "integrity") {
|
|
214
|
+
console.error(chalk.yellow(" Warning: the blob exists but its contents may be corrupted or tampered with."));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
console.error(chalk.red(String(err)));
|
|
219
|
+
}
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
console.log(chalk.green("done") + chalk.dim(` (${bytes.length} bytes)`));
|
|
223
|
+
if (opts.output) {
|
|
224
|
+
fs.writeFileSync(opts.output, bytes);
|
|
225
|
+
console.log(chalk.green("✓") + " Saved to " + chalk.bold(opts.output));
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
// Print as UTF-8.
|
|
229
|
+
process.stdout.write(new TextDecoder().decode(bytes));
|
|
230
|
+
if (!bytes[bytes.length - 1] || bytes[bytes.length - 1] !== 10) {
|
|
231
|
+
process.stdout.write("\n");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
144
235
|
// ─── merge ────────────────────────────────────────────────────────────────────
|
|
145
236
|
export async function cmdMerge(from, into, opts) {
|
|
146
237
|
const cfg = resolveConfig();
|
|
@@ -230,18 +230,18 @@ async function handleApiHistory(res, url) {
|
|
|
230
230
|
}
|
|
231
231
|
if (payload["type"] !== "commit")
|
|
232
232
|
return [];
|
|
233
|
+
const delta = payload["delta"];
|
|
234
|
+
const facts = delta?.["facts"];
|
|
235
|
+
const artifacts = delta?.["artifacts"];
|
|
233
236
|
return [{
|
|
234
237
|
blob_id: entry.blob_id,
|
|
235
238
|
branch: String(payload["branch"] ?? branch),
|
|
236
239
|
ts_ms: Number(payload["ts_ms"] ?? 0),
|
|
237
240
|
parent_blob_ids: payload["parent_blob_ids"] ?? [],
|
|
238
241
|
parent_blob_hashes: payload["parent_blob_hashes"] ?? [],
|
|
239
|
-
message:
|
|
240
|
-
const delta = payload["delta"];
|
|
241
|
-
const facts = delta?.["facts"];
|
|
242
|
-
return facts?.length ? facts[0] : `commit ${entry.blob_id.slice(0, 8)}`;
|
|
243
|
-
})(),
|
|
242
|
+
message: facts?.length ? facts[0] : `commit ${entry.blob_id.slice(0, 8)}`,
|
|
244
243
|
delta: payload["delta"] ?? {},
|
|
244
|
+
...(artifacts?.length ? { artifacts } : {}),
|
|
245
245
|
}];
|
|
246
246
|
});
|
|
247
247
|
commits.sort((a, b) => a.ts_ms - b.ts_ms);
|
package/dist/config.d.ts
CHANGED
|
@@ -49,6 +49,20 @@ export interface ProjectConfig {
|
|
|
49
49
|
packageId?: string;
|
|
50
50
|
/** Gas sponsor URL. When set, all on-chain txs are sponsored (no SUI balance needed). */
|
|
51
51
|
sponsorUrl?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Opt-in artifact storage config. When enabled, commit() can persist files
|
|
54
|
+
* as standalone Walrus blobs. The signer keypair must hold WAL + SUI.
|
|
55
|
+
* Overrideable via MEMFORK_ARTIFACTS_* env vars.
|
|
56
|
+
*/
|
|
57
|
+
artifacts?: {
|
|
58
|
+
enabled?: boolean;
|
|
59
|
+
/** Walrus epochs to purchase. Default: 12. */
|
|
60
|
+
epochs?: number;
|
|
61
|
+
/** Max artifact size in bytes. Default: 10 MiB. */
|
|
62
|
+
maxBytes?: number;
|
|
63
|
+
/** Optional Walrus upload relay URL. */
|
|
64
|
+
uploadRelayUrl?: string;
|
|
65
|
+
};
|
|
52
66
|
}
|
|
53
67
|
export interface TreeCredential {
|
|
54
68
|
/** Ed25519 private key in bech32 suiprivkey1… format. */
|
|
@@ -77,6 +91,13 @@ export interface ResolvedConfig {
|
|
|
77
91
|
rpcUrl?: string;
|
|
78
92
|
packageId?: string;
|
|
79
93
|
sponsorUrl?: string;
|
|
94
|
+
/** Resolved artifact storage config. enabled=false by default. */
|
|
95
|
+
artifacts: {
|
|
96
|
+
enabled: boolean;
|
|
97
|
+
epochs: number;
|
|
98
|
+
maxBytes: number;
|
|
99
|
+
uploadRelayUrl?: string;
|
|
100
|
+
};
|
|
80
101
|
}
|
|
81
102
|
export declare function projectConfigPath(cwd?: string): string;
|
|
82
103
|
export declare function credentialsPath(): string;
|
|
@@ -113,4 +134,10 @@ export declare function toClientConfig(r: ResolvedConfig): {
|
|
|
113
134
|
delegateKey: string;
|
|
114
135
|
serverUrl: string;
|
|
115
136
|
};
|
|
137
|
+
artifacts: {
|
|
138
|
+
enabled: boolean;
|
|
139
|
+
epochs: number;
|
|
140
|
+
maxBytes: number;
|
|
141
|
+
uploadRelayUrl?: string;
|
|
142
|
+
};
|
|
116
143
|
};
|
package/dist/config.js
CHANGED
|
@@ -167,6 +167,17 @@ export function resolveConfig(opts = {}) {
|
|
|
167
167
|
const network = (env['MEMFORK_NETWORK'] ??
|
|
168
168
|
project?.network ??
|
|
169
169
|
'mainnet');
|
|
170
|
+
// ── Artifact storage config ─────────────────────────────────────────────────
|
|
171
|
+
const artifactsEnabled = env['MEMFORK_ARTIFACTS_ENABLED'] === 'true' ||
|
|
172
|
+
(env['MEMFORK_ARTIFACTS_ENABLED'] === undefined &&
|
|
173
|
+
(project?.artifacts?.enabled ?? false));
|
|
174
|
+
const artifactsEpochs = env['MEMFORK_ARTIFACTS_EPOCHS'] != null
|
|
175
|
+
? parseInt(env['MEMFORK_ARTIFACTS_EPOCHS'], 10)
|
|
176
|
+
: (project?.artifacts?.epochs ?? 12);
|
|
177
|
+
const artifactsMaxBytes = env['MEMFORK_ARTIFACTS_MAX_BYTES'] != null
|
|
178
|
+
? parseInt(env['MEMFORK_ARTIFACTS_MAX_BYTES'], 10)
|
|
179
|
+
: (project?.artifacts?.maxBytes ?? 10 * 1024 * 1024);
|
|
180
|
+
const artifactsUploadRelayUrl = env['MEMFORK_ARTIFACTS_UPLOAD_RELAY_URL'] ?? project?.artifacts?.uploadRelayUrl;
|
|
170
181
|
return {
|
|
171
182
|
treeId,
|
|
172
183
|
privateKey,
|
|
@@ -181,6 +192,12 @@ export function resolveConfig(opts = {}) {
|
|
|
181
192
|
packageId: env['MEMFORK_PACKAGE_ID'] ?? project?.packageId,
|
|
182
193
|
sponsorUrl: env['MEMFORK_SPONSOR_URL'] ??
|
|
183
194
|
project?.sponsorUrl,
|
|
195
|
+
artifacts: {
|
|
196
|
+
enabled: artifactsEnabled,
|
|
197
|
+
epochs: artifactsEpochs,
|
|
198
|
+
maxBytes: artifactsMaxBytes,
|
|
199
|
+
uploadRelayUrl: artifactsUploadRelayUrl,
|
|
200
|
+
},
|
|
184
201
|
};
|
|
185
202
|
}
|
|
186
203
|
/**
|
|
@@ -200,5 +217,6 @@ export function toClientConfig(r) {
|
|
|
200
217
|
delegateKey: r.memwalKey,
|
|
201
218
|
serverUrl: r.memwalRelayer,
|
|
202
219
|
},
|
|
220
|
+
artifacts: r.artifacts,
|
|
203
221
|
};
|
|
204
222
|
}
|
package/package.json
CHANGED
|
@@ -2,33 +2,57 @@
|
|
|
2
2
|
name: memory-fork
|
|
3
3
|
description: >-
|
|
4
4
|
Fork agent memory into parallel branches to explore competing hypotheses.
|
|
5
|
-
Use
|
|
6
|
-
|
|
5
|
+
Use whenever a task has two or more genuinely competing approaches worth
|
|
6
|
+
testing in isolation — whether the user names them explicitly ("explore
|
|
7
|
+
both", "compare A vs B") OR asks an open decision question (e.g. "what's the
|
|
8
|
+
fastest way to X?") where you can identify two credible, mutually-exclusive
|
|
9
|
+
solutions.
|
|
7
10
|
---
|
|
8
11
|
|
|
9
12
|
# Memory Fork
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
Fork the MemForks memory tree whenever a task has two or more genuinely
|
|
15
|
+
competing approaches that deserve to be tested in isolation. Never collapse
|
|
16
|
+
competing ideas into a single stream. The decision to fork is **yours** — you
|
|
17
|
+
do not need the user to ask for branching; you need only to recognise that two
|
|
18
|
+
real alternatives exist.
|
|
14
19
|
|
|
15
20
|
## When to trigger
|
|
16
21
|
|
|
17
|
-
Trigger this skill
|
|
22
|
+
Trigger this skill in either case:
|
|
23
|
+
|
|
24
|
+
**Explicit multi-hypothesis prompts** — the user names the alternatives:
|
|
18
25
|
- "explore both paths" / "try both" / "compare X and Y"
|
|
19
26
|
- "what if we did X instead of Y" (two real alternatives)
|
|
20
27
|
- "should we do A or B?" (genuine decision fork, not a rhetorical question)
|
|
21
|
-
|
|
28
|
+
|
|
29
|
+
**Implicit decision questions** — the user asks an open question and *you*
|
|
30
|
+
identify two credible competing approaches:
|
|
31
|
+
- "What's the fastest way to cut our auth latency?" → e.g. Redis caching vs.
|
|
32
|
+
bcrypt cost tuning
|
|
33
|
+
- "How should we make this query cheaper?" → e.g. add an index vs. denormalise
|
|
34
|
+
- Any open "how do we improve / fix / speed up X?" where two distinct,
|
|
35
|
+
mutually-exclusive strategies are worth measuring before committing
|
|
36
|
+
|
|
37
|
+
**Do NOT fork** when there is only one sensible approach, when the question is
|
|
38
|
+
rhetorical or informational, or when the alternatives are trivial variations of
|
|
39
|
+
each other. Forking is for real, competing, separately-testable hypotheses —
|
|
40
|
+
not for every question.
|
|
41
|
+
|
|
42
|
+
When you fork off an implicit question, name the two approaches you inferred so
|
|
43
|
+
the user can see your reasoning (see the announce format below).
|
|
22
44
|
|
|
23
45
|
## Procedure
|
|
24
46
|
|
|
25
47
|
### 1. Announce the fork
|
|
26
48
|
|
|
27
|
-
Print exactly
|
|
49
|
+
Name the two approaches you identified, then announce the fork. Print exactly
|
|
50
|
+
this shape (substitute the real approaches and current branch):
|
|
28
51
|
|
|
29
52
|
```
|
|
30
|
-
[memforks]
|
|
31
|
-
[memforks]
|
|
53
|
+
[memforks] Two viable approaches detected — <approach A> vs. <approach B>.
|
|
54
|
+
[memforks] Branching memory so each can be tested without contamination.
|
|
55
|
+
[memforks] Forking from <current-branch>@HEAD
|
|
32
56
|
```
|
|
33
57
|
|
|
34
58
|
Then list the branches you will create, one per hypothesis:
|
|
@@ -39,7 +63,11 @@ Then list the branches you will create, one per hypothesis:
|
|
|
39
63
|
```
|
|
40
64
|
|
|
41
65
|
Use kebab-case branch names derived from the hypothesis (e.g. `dev/redis-first`,
|
|
42
|
-
`dev/bcrypt-
|
|
66
|
+
`dev/bcrypt-first`, `dev/approach-a`).
|
|
67
|
+
|
|
68
|
+
> Naming the two approaches in the first line is what makes the fork read as
|
|
69
|
+
> *your* decision rather than a command you were handed — especially when the
|
|
70
|
+
> user asked an open question and never named the alternatives.
|
|
43
71
|
|
|
44
72
|
### 2. Create the branches
|
|
45
73
|
|
|
@@ -55,11 +55,30 @@ Do **not** save: current task state, in-progress work, temporary findings.
|
|
|
55
55
|
|
|
56
56
|
## Forking — exploring competing approaches
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
**
|
|
58
|
+
Fork the memory tree whenever a task has two or more genuinely competing
|
|
59
|
+
approaches worth testing in isolation — and the decision to fork is **yours**.
|
|
60
|
+
Trigger in either case:
|
|
61
|
+
|
|
62
|
+
- **Explicit** — the user names the alternatives ("try both", "compare X vs Y",
|
|
63
|
+
"explore both paths", "should we do A or B?").
|
|
64
|
+
- **Implicit** — the user asks an open decision question ("what's the fastest way
|
|
65
|
+
to cut our auth latency?", "how do we make this cheaper?") and *you* identify
|
|
66
|
+
two credible, mutually-exclusive strategies worth measuring separately. You do
|
|
67
|
+
not need the user to ask for branching; you only need to recognise that two
|
|
68
|
+
real alternatives exist.
|
|
69
|
+
|
|
70
|
+
Do **not** fork when there is one sensible approach, the question is rhetorical
|
|
71
|
+
or informational, or the alternatives are trivial variations. Never collapse
|
|
72
|
+
genuinely competing ideas into a single branch.
|
|
73
|
+
|
|
74
|
+
**Step 1 — name the approaches, then announce and create branches.** When you
|
|
75
|
+
fork off an implicit question, name the two approaches you inferred so the user
|
|
76
|
+
sees your reasoning:
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
[memforks] Two viable approaches detected — <approach A> vs. <approach B>.
|
|
80
|
+
[memforks] Branching memory so each can be tested without contamination.
|
|
81
|
+
```
|
|
63
82
|
|
|
64
83
|
```bash
|
|
65
84
|
memfork branch dev/<hypothesis-a>
|
|
@@ -68,7 +87,7 @@ memfork branch dev/<hypothesis-b>
|
|
|
68
87
|
|
|
69
88
|
Both fork from the current Git branch by default; add `--from <branch>` to fork from another.
|
|
70
89
|
|
|
71
|
-
Use short kebab-case names (`dev/redis-first`, `dev/bcrypt-
|
|
90
|
+
Use short kebab-case names (`dev/redis-first`, `dev/bcrypt-first`).
|
|
72
91
|
|
|
73
92
|
**Step 2 — investigate each path and commit evidence:**
|
|
74
93
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600&display=swap";.topbar{display:flex;align-items:center;gap:var(--space-4);height:var(--topbar-height);padding:0 var(--space-5);border-bottom:1px solid var(--border);background:var(--bg-1);flex-shrink:0;position:relative;z-index:10}.topbar-left{display:flex;align-items:center;gap:var(--space-3);flex-shrink:0}.topbar-logo{display:flex;align-items:center;gap:var(--space-2);font-weight:600;font-size:.9rem;color:var(--fg-0);letter-spacing:-.01em}.topbar-tree-id{font-family:var(--font-mono);font-size:.72rem;color:var(--fg-3);background:var(--bg-2);border:1px solid var(--border);padding:.15rem .5rem;border-radius:var(--radius-sm)}.topbar-live-badge{display:flex;align-items:center;gap:5px;font-size:.72rem;font-family:var(--font-mono);padding:.15rem .55rem;border-radius:999px;border:1px solid transparent}.topbar-live-badge.live{background:var(--accent-dim);color:var(--accent);border-color:var(--accent-border)}.topbar-live-badge.offline{background:var(--bg-2);color:var(--fg-3);border-color:var(--border)}.topbar-live-dot{width:6px;height:6px;border-radius:50%;background:currentColor}.topbar-live-badge.live .topbar-live-dot{animation:pulse-dot 1.8s ease-in-out infinite}@keyframes pulse-dot{0%,to{opacity:1}50%{opacity:.35}}.topbar-views{display:flex;align-items:center;gap:2px;background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-md);padding:3px;flex-shrink:0}.topbar-view-btn{padding:.2rem .75rem;font-size:.78rem;font-weight:500;color:var(--fg-2);border-radius:6px;background:transparent;border:none;transition:background var(--duration-fast) var(--ease-out),color var(--duration-fast) var(--ease-out);white-space:nowrap}.topbar-view-btn:hover{color:var(--fg-0);background:var(--bg-3)}.topbar-view-btn.active{background:var(--bg-0);color:var(--fg-0);box-shadow:var(--shadow-sm)}.topbar-spacer{flex:1}.topbar-branch-select{-moz-appearance:none;appearance:none;-webkit-appearance:none;font-family:var(--font-mono);font-size:.74rem;color:var(--fg-1);background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-sm);padding:.25rem 1.6rem .25rem .65rem;cursor:pointer;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5' viewBox='0 0 8 5'%3E%3Cpath d='M0 0l4 5 4-5z' fill='%23667066'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right .6rem center;transition:border-color var(--duration-fast) var(--ease-out),color var(--duration-fast) var(--ease-out);max-width:200px}.topbar-branch-select:hover{border-color:var(--border-strong);color:var(--fg-0)}.topbar-branch-select:focus-visible{outline:1px solid var(--accent);outline-offset:1px}.topbar-right{display:flex;align-items:center;gap:var(--space-2);flex-shrink:0}.topbar-replay-btn{padding:.2rem .75rem;font-size:.75rem;font-family:var(--font-mono);font-weight:500;color:var(--fg-2);background:transparent;border:1px solid var(--border);border-radius:var(--radius-sm);transition:background var(--duration-fast) var(--ease-out),color var(--duration-fast) var(--ease-out),border-color var(--duration-fast) var(--ease-out);white-space:nowrap}.topbar-replay-btn:hover{background:var(--bg-2);color:var(--fg-0);border-color:var(--border-strong)}.topbar-replay-btn.active{background:var(--accent-dim);color:var(--accent);border-color:var(--accent-border)}.topbar-refresh-btn{padding:.2rem .5rem;font-size:.85rem;color:var(--fg-2);background:transparent;border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;transition:background var(--duration-fast) var(--ease-out),color var(--duration-fast) var(--ease-out);line-height:1}.topbar-refresh-btn:hover:not(:disabled){background:var(--bg-2);color:var(--fg-0)}.topbar-refresh-btn:disabled{cursor:default}.inspector{display:flex;flex-direction:column;gap:var(--space-5);padding:var(--space-5) var(--space-5) var(--space-8);overflow-y:auto;height:100%}.inspector-header{display:flex;flex-direction:column;gap:var(--space-2);padding-bottom:var(--space-4);border-bottom:1px solid var(--border)}.inspector-title-row{display:flex;align-items:center;flex-wrap:wrap;gap:var(--space-2)}.inspector-commit-id{font-family:var(--font-mono);font-size:.88rem;font-weight:600;color:var(--accent);background:var(--accent-dim);border:1px solid var(--accent-border);padding:.2rem .55rem;border-radius:var(--radius-sm);letter-spacing:.03em}.inspector-message{font-size:.875rem;color:var(--fg-0);line-height:1.55;font-weight:500}.inspector-section{display:flex;flex-direction:column;gap:var(--space-2)}.inspector-section-label{font-family:var(--font-mono);font-size:.68rem;letter-spacing:.08em;color:var(--fg-3);text-transform:uppercase;margin-bottom:2px}.inspector-kv{display:grid;grid-template-columns:90px 1fr;align-items:baseline;gap:var(--space-3);font-size:.825rem}.inspector-key{color:var(--fg-2);font-size:.78rem}.inspector-val{color:var(--fg-0);font-family:var(--font-mono);font-size:.78rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.inspector-mono-sm{font-size:.7rem;color:var(--fg-1)}.inspector-parents{list-style:none;display:flex;flex-direction:column;gap:var(--space-1)}.inspector-parent-row{list-style:none}.inspector-parent-btn{display:flex;align-items:center;gap:var(--space-2);width:100%;padding:var(--space-2) var(--space-3);background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-md);font-size:.8rem;color:var(--fg-1);transition:background var(--duration-fast) var(--ease-out),border-color var(--duration-fast) var(--ease-out);text-align:left}.inspector-parent-btn:hover{background:var(--bg-3);border-color:var(--accent);color:var(--fg-0)}.inspector-parent-btn code{font-family:var(--font-mono);color:var(--accent);font-size:.78rem}.inspector-parent-branch{flex:1;color:var(--fg-2);font-size:.75rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.inspector-parent-arrow{color:var(--fg-3);font-size:.75rem}.inspector-link{display:flex;align-items:center;gap:var(--space-2);padding:var(--space-2) var(--space-3);background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-md);color:var(--fg-1);font-size:.8rem;transition:background var(--duration-fast) var(--ease-out),border-color var(--duration-fast) var(--ease-out);text-decoration:none}.inspector-link:hover{background:var(--bg-3);border-color:var(--accent);color:var(--fg-0);text-decoration:none}.inspector-link code{font-family:var(--font-mono);font-size:.72rem;color:var(--accent)}.inspector-link-icon{font-size:.8rem;color:var(--fg-3)}.inspector-link-ext{margin-left:auto;color:var(--fg-3);font-size:.7rem}.inspector-link-sm{padding:var(--space-1) var(--space-2);font-size:.72rem}.inspector-code-block{display:block;font-family:var(--font-mono);font-size:.72rem;color:var(--fg-1);background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-md);padding:var(--space-3);word-break:break-all;line-height:1.6}.inspector-proposal-summary{background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-md);padding:var(--space-3) var(--space-4);display:flex;flex-direction:column;gap:var(--space-2)}.inspector-attestations{list-style:none;display:flex;flex-direction:column;gap:var(--space-2)}.inspector-attest-row{background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-md);padding:var(--space-2) var(--space-3);display:flex;flex-direction:column;gap:var(--space-1)}.inspector-attest-top{display:flex;align-items:center;flex-wrap:wrap;gap:var(--space-2)}.inspector-attest-signer{font-family:var(--font-mono);font-size:.72rem;color:var(--fg-1)}.inspector-attest-time{margin-left:auto;font-size:.7rem;color:var(--fg-3);font-family:var(--font-mono)}.inspector-snapshot{margin-top:auto}.inspector-snapshot-body{background:var(--bg-2);border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:var(--radius-md);padding:var(--space-3) var(--space-4);font-size:.825rem;color:var(--fg-0);line-height:1.6}.inspector-snapshot-hint{font-size:.72rem;color:var(--fg-3);font-style:italic}.inspector-empty-hint{font-size:.8rem;color:var(--fg-3);font-style:italic}.inspector-copy-row{display:flex;align-items:center;gap:var(--space-2);width:100%;background:none;border:none;cursor:pointer;padding:var(--space-1) 0;color:var(--fg-1);font-size:.82rem;text-align:left}.inspector-copy-row:hover{color:var(--accent)}.inspector-copy-badge{margin-left:auto;font-size:.7rem;color:var(--fg-3);background:var(--surface-1);border:1px solid var(--border);border-radius:var(--radius-sm);padding:1px 6px;flex-shrink:0}.inspector-copy-row:hover .inspector-copy-badge{color:var(--accent);border-color:var(--accent)}.right-drawer{position:relative;display:flex;flex-direction:column;width:0;overflow:hidden;border-left:1px solid transparent;background:var(--bg-1);transition:width var(--duration-med) var(--ease-out),border-color var(--duration-med) var(--ease-out);flex-shrink:0}.right-drawer.open{width:var(--drawer-width);border-left-color:var(--border)}.drawer-toolbar{display:flex;align-items:center;justify-content:space-between;padding:0 var(--space-4);height:44px;border-bottom:1px solid var(--border);flex-shrink:0}.drawer-title{font-size:.8rem;font-weight:600;color:var(--fg-1);letter-spacing:.02em}.drawer-body{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.drawer-proposals-footer{border-top:1px solid var(--border);padding:var(--space-3) var(--space-4);flex-shrink:0}.drawer-proposals-label{font-family:var(--font-mono);font-size:.68rem;letter-spacing:.08em;text-transform:uppercase;color:var(--fg-3);margin-bottom:var(--space-2)}.drawer-proposals-list{list-style:none;display:flex;flex-direction:column;gap:var(--space-1)}.drawer-proposal-chip{display:flex;align-items:center;gap:var(--space-2);width:100%;padding:var(--space-2) var(--space-3);background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-md);font-size:.76rem;color:var(--fg-1);text-align:left;transition:background var(--duration-fast) var(--ease-out),border-color var(--duration-fast) var(--ease-out)}.drawer-proposal-chip:hover{background:var(--bg-3);border-color:var(--warning)}.drawer-proposal-chip>span:nth-child(2){flex:1;font-family:var(--font-mono);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.drawer-proposal-chip>span:last-child{color:var(--fg-3);font-size:.7rem;flex-shrink:0}.dag-canvas-wrapper,.dag-canvas-scroll{position:relative;flex:1;overflow:auto;background:var(--bg-0);background-image:linear-gradient(rgba(255,255,255,.012) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.012) 1px,transparent 1px);background-size:32px 32px}.dag-canvas-svg,.dag-canvas{display:block;cursor:grab}.dag-canvas-svg:active,.dag-canvas:active{cursor:grabbing}.dag-node{transition:opacity var(--duration-fast) var(--ease-out)}@keyframes dag-pop-in{0%{transform:scale(.3);opacity:0}65%{transform:scale(1.15);opacity:1}to{transform:scale(1);opacity:1}}.dag-node--new{animation:dag-pop-in .45s cubic-bezier(.34,1.56,.64,1) both;transform-box:fill-box;transform-origin:center}.dag-node:focus-visible circle,.dag-node:focus-visible rect{outline:2px solid var(--accent)}.dag-node-hash{opacity:0;transition:opacity var(--duration-fast) var(--ease-out)}.dag-node:hover .dag-node-hash,.dag-node.selected .dag-node-hash{opacity:1}.dag-edge{transition:opacity var(--duration-fast) var(--ease-out),stroke-width var(--duration-fast) var(--ease-out)}.dag-lane-label{transition:opacity var(--duration-fast) var(--ease-out)}.dag-empty{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--space-2);pointer-events:none;color:var(--fg-3);font-size:.875rem}.dag-empty p{text-align:center}.time-scrubber{padding:8px 16px 10px;border-bottom:1px solid var(--border);background:var(--surface-0);animation:ts-slide-in .12s var(--ease-out) both;overflow:hidden}@keyframes ts-slide-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.time-scrubber-track-row{display:flex;align-items:center;gap:8px}.time-scrubber-past-label,.time-scrubber-live-label{font-size:.68rem;color:var(--fg-3);white-space:nowrap;flex-shrink:0}.time-scrubber-live-label.active{color:var(--accent)}.time-scrubber-input{flex:1;-moz-appearance:none;appearance:none;-webkit-appearance:none;height:3px;border-radius:2px;background:linear-gradient(to right,var(--accent) 0%,var(--accent) calc(var(--fill-pct, 50) * 1%),var(--border-strong) calc(var(--fill-pct, 50) * 1%),var(--border-strong) 100%);outline:none;cursor:pointer}.time-scrubber-input::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;border-radius:50%;background:var(--accent);border:2px solid var(--surface-0);box-shadow:0 0 0 1px var(--accent);cursor:grab;transition:transform 80ms var(--ease-out)}.time-scrubber-input:active::-webkit-slider-thumb{cursor:grabbing;transform:scale(1.2)}.time-scrubber-input::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:var(--accent);border:2px solid var(--surface-0);box-shadow:0 0 0 1px var(--accent);cursor:grab}.time-scrubber-label{margin-top:5px;font-size:.72rem;color:var(--fg-2);display:flex;align-items:center;gap:5px;min-height:1.2em;font-variant-numeric:tabular-nums}.time-scrubber-label--live{color:var(--accent)}.time-scrubber-live-dot{width:6px;height:6px;border-radius:50%;background:var(--accent);display:inline-block;flex-shrink:0;animation:scrubber-pulse 2s ease-in-out infinite}.time-scrubber-at-glyph{opacity:.6;flex-shrink:0}@keyframes scrubber-pulse{0%,to{opacity:1}50%{opacity:.4}}.memory-view{flex:1;display:flex;flex-direction:column;overflow:hidden;background:var(--bg-0)}.memory-search-row{display:flex;align-items:center;gap:var(--space-4);padding:var(--space-4) var(--space-5);border-bottom:1px solid var(--border);flex-shrink:0}.memory-tt-btn{position:relative;display:flex;align-items:center;justify-content:center;width:26px;height:26px;flex-shrink:0;border:none;border-radius:6px;background:transparent;color:var(--fg-3);cursor:pointer;transition:background .12s,color .12s}.memory-tt-btn:hover,.memory-tt-btn.open{background:var(--surface-1);color:var(--fg-1)}.memory-tt-btn.active{color:var(--accent)}.memory-tt-badge{position:absolute;top:4px;right:4px;width:5px;height:5px;border-radius:50%;background:var(--accent);border:1px solid var(--bg-0);pointer-events:none}.memory-search-wrap{position:relative;flex:1;max-width:480px}.memory-search-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--fg-3);pointer-events:none}.memory-search{width:100%;background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-md);padding:.45rem 2rem .45rem 2.1rem;font-family:var(--font-sans);font-size:.85rem;color:var(--fg-0);outline:none;transition:border-color var(--duration-fast) var(--ease-out),box-shadow var(--duration-fast) var(--ease-out)}.memory-search::placeholder{color:var(--fg-3)}.memory-search:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-dim)}.memory-search-clear{position:absolute;right:8px;top:50%;transform:translateY(-50%);width:18px;height:18px;font-size:14px;line-height:18px;text-align:center;color:var(--fg-3);border-radius:50%;transition:color var(--duration-fast) var(--ease-out)}.memory-search-clear:hover{color:var(--fg-0)}.memory-count-label{font-family:var(--font-mono);font-size:.72rem;color:var(--fg-3);white-space:nowrap}.memory-empty{flex:1;display:flex;align-items:center;justify-content:center;color:var(--fg-3);font-size:.875rem}.memory-empty p{text-align:center;line-height:1.8}.memory-groups{flex:1;overflow-y:auto;padding:var(--space-4) var(--space-5) var(--space-8);display:flex;flex-direction:column;gap:var(--space-6)}.memory-group{display:flex;flex-direction:column;gap:var(--space-2)}.memory-group-header{display:flex;align-items:center;gap:var(--space-2);padding-bottom:var(--space-2);border-bottom:1px solid var(--border)}.memory-group-name{font-family:var(--font-mono);font-size:.8rem;font-weight:600;color:var(--fg-1);letter-spacing:.02em}.memory-group-count{font-family:var(--font-mono);font-size:.68rem;color:var(--fg-3)}.memory-fact-list{list-style:none;display:flex;flex-direction:column;gap:2px}.memory-fact-row{padding:var(--space-3) var(--space-4);background:var(--bg-1);border:1px solid var(--border);border-radius:var(--radius-md);display:flex;flex-direction:column;gap:var(--space-2);cursor:pointer;transition:border-color var(--duration-fast) var(--ease-out),background var(--duration-fast) var(--ease-out);outline:none}.memory-fact-row:hover{background:var(--bg-2);border-color:var(--accent-border)}.memory-fact-row:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.memory-fact-content{font-size:.85rem;color:var(--fg-0);line-height:1.6}.memory-fact-meta-row{display:flex;align-items:center;gap:var(--space-2);min-width:0}.memory-fact-branch{font-family:var(--font-mono);font-size:.68rem;font-weight:500;color:var(--accent);background:var(--accent-dim);border-radius:var(--radius-sm);padding:1px 6px;flex-shrink:0}.memory-fact-blob{font-family:var(--font-mono);font-size:.68rem;color:var(--fg-3);flex-shrink:0}.memory-fact-time{margin-left:auto;font-size:.7rem;font-family:var(--font-mono);color:var(--fg-3);white-space:nowrap;flex-shrink:0;-webkit-user-select:none;user-select:none}.history-view{flex:1;display:flex;flex-direction:column;overflow:hidden;background:var(--bg-0)}.history-header{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-3) var(--space-5);border-bottom:1px solid var(--border);flex-shrink:0}.history-header-count{font-family:var(--font-mono);font-size:.72rem;color:var(--fg-3)}.history-tt-btn{margin-left:auto;position:relative;display:flex;align-items:center;justify-content:center;width:26px;height:26px;border:none;border-radius:6px;background:transparent;color:var(--fg-3);cursor:pointer;transition:background .12s,color .12s;flex-shrink:0}.history-tt-btn:hover,.history-tt-btn.open{background:var(--surface-1);color:var(--fg-1)}.history-tt-btn.active{color:var(--accent)}.history-tt-badge{position:absolute;top:4px;right:4px;width:5px;height:5px;border-radius:50%;background:var(--accent);border:1px solid var(--bg-0);pointer-events:none}.history-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--space-3);color:var(--fg-3);font-size:.875rem;padding:var(--space-8);text-align:center}.history-empty-sub{font-size:.78rem;color:var(--fg-3);max-width:420px;line-height:1.6}.history-empty-sub code{color:var(--fg-2)}.history-list{flex:1;overflow-y:auto;list-style:none;padding:var(--space-2) var(--space-3)}.history-row{display:grid;grid-template-columns:28px 1fr;gap:var(--space-3);padding:var(--space-2) var(--space-3);border:1px solid transparent;border-left:3px solid transparent;border-radius:var(--radius-md);transition:background var(--duration-fast) var(--ease-out),border-color var(--duration-fast) var(--ease-out);outline:none}.history-row--commit{cursor:pointer}.history-row--commit:hover{background:var(--bg-2)}.history-row--commit.selected{background:var(--bg-2);border-left-color:var(--border-strong)}.history-row--commit:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.history-row--fork{cursor:default;background:#ffffff04}.history-row--anchor{cursor:pointer;border-left-color:var(--accent-border);background:#1f9d720a}.history-row--anchor:hover{background:#1f9d7214;border-left-color:var(--accent)}.history-row--anchor.selected{background:var(--accent-dim);border-left-color:var(--accent)}.history-row--anchor:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}@keyframes row-pop-in{0%{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:none}}.is-new{animation:row-pop-in .35s var(--ease-out) both}.history-gutter{position:relative;display:flex;flex-direction:column;align-items:center}.history-line{width:2px;flex:1;background:var(--border-strong);min-height:6px}.history-node{width:10px;height:10px;border-radius:50%;border:2px solid currentColor;background:var(--bg-0);flex-shrink:0;z-index:1}.history-node--fork{width:9px;height:9px;border-radius:2px;border:1.5px dashed currentColor;background:var(--bg-0);transform:rotate(45deg);flex-shrink:0;z-index:1}.history-node--anchor.merge{width:12px;height:12px;border-radius:3px;border:2px solid var(--accent);background:var(--accent-dim);transform:rotate(45deg);flex-shrink:0;z-index:1}.history-row--anchor.selected .history-node--anchor{background:var(--accent)}.history-node.branch-green,.history-node--fork.branch-green{color:var(--lane-0)}.history-node.branch-blue,.history-node--fork.branch-blue{color:var(--lane-1)}.history-node.branch-orange,.history-node--fork.branch-orange{color:var(--lane-3)}.history-node.branch-red,.history-node--fork.branch-red{color:var(--danger)}.history-node.branch-purple,.history-node--fork.branch-purple{color:var(--lane-4)}.history-node.branch-muted,.history-node--fork.branch-muted{color:var(--fg-3)}.history-content{display:flex;flex-direction:column;gap:var(--space-1);min-width:0;padding:3px 0}.history-top-row{display:flex;align-items:center;gap:var(--space-2);min-width:0}.history-fork-top{flex-wrap:wrap;row-gap:var(--space-1)}.history-flex-spacer{flex:1}.history-short-id{font-family:var(--font-mono);font-size:.72rem;color:var(--accent);background:var(--bg-1);border:1px solid var(--border);padding:.05rem .4rem;border-radius:var(--radius-sm);flex-shrink:0}.history-row--anchor.selected .history-short-id{background:var(--accent-dim)}.history-message{font-size:.83rem;font-weight:500;color:var(--fg-0);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}.history-row--commit .history-message{color:var(--fg-1);font-weight:400}.history-meta-row{display:flex;align-items:center;gap:var(--space-2);font-size:.72rem;color:var(--fg-2);flex-wrap:wrap}.history-author{font-family:var(--font-mono);font-size:.72rem;color:var(--fg-1)}.history-sep{color:var(--fg-3);-webkit-user-select:none;user-select:none}.history-time{color:var(--fg-2)}.history-parents-hint,.history-resolver-label{font-family:var(--font-mono);color:var(--fg-3);font-size:.68rem}.history-resolver-label{color:var(--fg-2)}.history-tx{font-family:var(--font-mono);font-size:.66rem;color:var(--fg-3)}.history-fork-glyph{font-size:.85rem;color:var(--fg-3);flex-shrink:0;line-height:1;margin-right:1px}.history-fork-label{font-size:.78rem;color:var(--fg-3);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.history-fork-label strong{color:var(--fg-1);font-weight:500}.history-onchain-glyph{color:var(--fg-3);font-size:.75rem;flex-shrink:0;line-height:1}.history-onchain-glyph--anchor{color:var(--accent);font-size:.85rem}.history-message strong{font-weight:600}.merges-view{flex:1;display:flex;flex-direction:column;overflow-y:auto;gap:var(--space-6);padding:var(--space-5);background:var(--bg-0)}.merges-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--space-3);color:var(--fg-3);font-size:.875rem;text-align:center;padding:var(--space-8)}.merges-empty-sub{font-size:.78rem;color:var(--fg-3);max-width:400px;line-height:1.6}.merges-empty-sub code{color:var(--fg-2)}.merges-section{display:flex;flex-direction:column;gap:var(--space-3)}.merges-section-header{display:flex;align-items:center;gap:var(--space-3);padding-bottom:var(--space-2);border-bottom:1px solid var(--border)}.merges-section-title{font-family:var(--font-mono);font-size:.72rem;letter-spacing:.08em;text-transform:uppercase;color:var(--fg-3)}.merges-ceremonies{display:flex;flex-direction:column;gap:var(--space-4)}.ceremony-card{display:flex;flex-direction:column;gap:var(--space-4);border:1px solid var(--border);border-left:3px solid var(--warning);border-radius:var(--radius-lg);background:var(--bg-1);padding:var(--space-4) var(--space-5)}.ceremony-header{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:var(--space-3)}.ceremony-header-left{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap}.ceremony-icon{font-size:1rem;color:var(--warning)}.ceremony-resolver-label{font-family:var(--font-mono);font-size:.72rem;color:var(--fg-3)}.ceremony-route-text{display:flex;align-items:center;gap:var(--space-2);font-size:.85rem;color:var(--fg-1)}.ceremony-route-text strong{color:var(--fg-0);font-weight:600}.ceremony-arrow{color:var(--fg-3);font-size:.8rem}.ceremony-expiry{font-family:var(--font-mono);font-size:.72rem;color:var(--fg-2)}.ceremony-expiry--warn{color:var(--warning)}.ceremony-attests{list-style:none;display:flex;flex-direction:column;gap:var(--space-2)}.ceremony-attests-empty{font-size:.8rem;color:var(--fg-3);font-style:italic;padding:var(--space-2) 0}.ceremony-attest-row{display:flex;align-items:center;gap:var(--space-2);padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);font-size:.8rem;flex-wrap:wrap}.ceremony-attest-row--cast{background:var(--bg-2);border:1px solid var(--border)}.ceremony-attest-row--pending{background:transparent;border:1px dashed var(--border);opacity:.65}@keyframes pulse-dot{0%,to{opacity:.3;transform:scale(.85)}50%{opacity:1;transform:scale(1.1)}}.ceremony-attest-pulse{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--warning);animation:pulse-dot 1.4s ease-in-out infinite;flex-shrink:0}.ceremony-attest-check{color:var(--accent);font-size:.82rem;flex-shrink:0}.ceremony-attest-label{font-family:var(--font-mono);font-size:.76rem;color:var(--fg-1);min-width:60px}.ceremony-attest-model{font-family:var(--font-mono);font-size:.7rem;color:var(--fg-3);background:var(--bg-3);border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 .3rem}.ceremony-attest-vote-text{font-size:.74rem;color:var(--fg-2);max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ceremony-attest-voting{font-size:.75rem;color:var(--fg-3);font-style:italic}.ceremony-attest-time-spacer{flex:1}.ceremony-attest-reltime{font-family:var(--font-mono);font-size:.68rem;color:var(--fg-3);white-space:nowrap}.ceremony-threshold{display:flex;flex-direction:column;gap:var(--space-2)}.ceremony-threshold-bar{height:4px;border-radius:2px;background:var(--bg-3);overflow:hidden}.ceremony-threshold-fill{height:100%;background:var(--accent);border-radius:2px;transition:width .5s var(--ease-out)}.ceremony-threshold-label{font-family:var(--font-mono);font-size:.7rem;color:var(--fg-2)}.ceremony-threshold-note{color:var(--fg-3)}.merges-settled-list{list-style:none;display:flex;flex-direction:column;gap:var(--space-2)}.settled-row{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);padding:var(--space-2) var(--space-3);border:1px solid var(--border);border-radius:var(--radius-md);cursor:pointer;transition:background var(--duration-fast) var(--ease-out),border-color var(--duration-fast) var(--ease-out);flex-wrap:wrap}.settled-row:hover{background:var(--bg-2);border-color:var(--accent-border)}.settled-row:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.settled-row-left{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;min-width:0}.settled-check{color:var(--accent);font-size:.82rem;flex-shrink:0}.settled-route{display:flex;align-items:center;gap:var(--space-2);font-size:.82rem;color:var(--fg-1);min-width:0}.settled-route strong{color:var(--fg-0);font-weight:500}.settled-arrow{color:var(--fg-3);font-size:.78rem}.settled-resolver{font-family:var(--font-mono);font-size:.68rem;color:var(--fg-3)}.settled-row-right{display:flex;align-items:center;gap:var(--space-3);flex-shrink:0}.settled-time{font-family:var(--font-mono);font-size:.72rem;color:var(--fg-3)}.settled-view-hint{font-size:.72rem;color:var(--fg-3);opacity:0;transition:opacity var(--duration-fast) var(--ease-out)}.settled-row:hover .settled-view-hint{opacity:1}.merges-section--graveyard .merges-section-title{color:var(--danger);opacity:.75}.merges-graveyard-note{font-size:.78rem;color:var(--fg-3);line-height:1.6;font-style:italic;max-width:560px}.merges-graveyard-list{list-style:none;display:flex;flex-direction:column;gap:var(--space-3)}.graveyard-row{display:flex;flex-direction:column;gap:var(--space-2);padding:var(--space-3) var(--space-4);background:var(--bg-1);border:1px solid var(--border);border-left:3px solid var(--danger);border-radius:var(--radius-md);opacity:.8;transition:opacity var(--duration-fast) var(--ease-out)}.graveyard-row:hover{opacity:1}.graveyard-row-top{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap}.graveyard-cross{color:var(--danger);font-size:.82rem;flex-shrink:0}.graveyard-time{font-family:var(--font-mono);font-size:.7rem;color:var(--fg-3);margin-left:auto}.graveyard-ask-btn{font-family:var(--font-mono);font-size:.7rem;color:var(--fg-2);background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-sm);padding:.15rem .55rem;cursor:pointer;transition:background var(--duration-fast) var(--ease-out),color var(--duration-fast) var(--ease-out),border-color var(--duration-fast) var(--ease-out)}.graveyard-ask-btn:hover{background:var(--bg-3);border-color:var(--accent-border);color:var(--accent)}.graveyard-rationale{font-size:.78rem;color:var(--fg-2);line-height:1.55;padding-left:var(--space-2);border-left:2px solid var(--border-strong)}:root{--bg-0: #0c0e13;--bg-1: #12151c;--bg-2: #181c25;--bg-3: #20252f;--fg-0: #e4e7ed;--fg-1: #9aa1ae;--fg-2: #69707d;--fg-3: #474d58;--accent: #1f9d72;--accent-hover: #27b886;--accent-dim: rgba(31, 157, 114, .13);--accent-glow: rgba(31, 157, 114, .22);--accent-border: rgba(31, 157, 114, .3);--border: rgba(255, 255, 255, .06);--border-strong: rgba(255, 255, 255, .11);--danger: #d65b5b;--danger-dim: rgba(214, 91, 91, .13);--danger-border: rgba(214, 91, 91, .3);--warning: #d2972f;--warning-dim: rgba(210, 151, 47, .13);--warning-border: rgba(210, 151, 47, .3);--purple: #8a82c9;--purple-dim: rgba(138, 130, 201, .13);--purple-border: rgba(138, 130, 201, .3);--lane-0: #1f9d72;--lane-1: #5683c4;--lane-2: #c06b95;--lane-3: #c1813f;--lane-4: #8a82c9;--lane-5: #3f9d8a;--lane-6: #b39a3c;--lane-7: #a76eb0;--radius-sm: 4px;--radius-md: 7px;--radius-lg: 11px;--shadow-sm: 0 1px 2px rgba(0, 0, 0, .4);--shadow-md: 0 4px 12px rgba(0, 0, 0, .45);--shadow-lg: 0 14px 40px rgba(0, 0, 0, .55);--space-1: .25rem;--space-2: .5rem;--space-3: .75rem;--space-4: 1rem;--space-5: 1.25rem;--space-6: 1.5rem;--space-8: 2rem;--ease-out: cubic-bezier(.2, .8, .2, 1);--duration-fast: .11s;--duration-med: .22s;--duration-slow: .4s;--topbar-height: 52px;--drawer-width: 420px;--font-mono: "JetBrains Mono", "Fira Code", "Cascadia Code", ui-monospace, monospace;--font-sans: "Inter", "Segoe UI", system-ui, sans-serif}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html,body,#root{height:100%;width:100%;overflow:hidden}body{font-family:var(--font-sans);font-size:14px;line-height:1.5;background:var(--bg-0);color:var(--fg-0);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}code,kbd,pre{font-family:var(--font-mono)}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border-strong);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--fg-3)}.chip{display:inline-flex;align-items:center;gap:3px;padding:.1rem .45rem;font-family:var(--font-mono);font-size:.68rem;font-weight:500;border-radius:999px;border:1px solid transparent;white-space:nowrap;line-height:1.6}.chip.green{background:var(--accent-dim);color:var(--accent);border-color:var(--accent-border)}.chip.blue{background:#5683c421;color:var(--lane-1);border-color:#5683c44d}.chip.orange{background:var(--warning-dim);color:var(--warning);border-color:var(--warning-border)}.chip.red{background:var(--danger-dim);color:var(--danger);border-color:var(--danger-border)}.chip.purple{background:var(--purple-dim);color:var(--purple);border-color:var(--purple-border)}.chip.muted{background:var(--bg-2);color:var(--fg-2);border-color:var(--border)}.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:var(--radius-sm);color:var(--fg-2);transition:background var(--duration-fast) var(--ease-out),color var(--duration-fast) var(--ease-out)}.icon-btn:hover{background:var(--bg-3);color:var(--fg-0)}:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.app-root{display:flex;flex-direction:column;height:100%;width:100%;overflow:hidden}.app-body{display:flex;flex:1;overflow:hidden;min-height:0}
|