@drewpayment/mink 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dashboard/out/404.html +1 -1
  2. package/dashboard/out/action-log.html +1 -1
  3. package/dashboard/out/action-log.txt +1 -1
  4. package/dashboard/out/activity.html +1 -1
  5. package/dashboard/out/activity.txt +1 -1
  6. package/dashboard/out/bugs.html +1 -1
  7. package/dashboard/out/bugs.txt +1 -1
  8. package/dashboard/out/capture.html +1 -1
  9. package/dashboard/out/capture.txt +1 -1
  10. package/dashboard/out/config.html +1 -1
  11. package/dashboard/out/config.txt +1 -1
  12. package/dashboard/out/daemon.html +1 -1
  13. package/dashboard/out/daemon.txt +1 -1
  14. package/dashboard/out/design.html +1 -1
  15. package/dashboard/out/design.txt +1 -1
  16. package/dashboard/out/discord.html +1 -1
  17. package/dashboard/out/discord.txt +1 -1
  18. package/dashboard/out/file-index.html +1 -1
  19. package/dashboard/out/file-index.txt +1 -1
  20. package/dashboard/out/index.html +1 -1
  21. package/dashboard/out/index.txt +1 -1
  22. package/dashboard/out/insights.html +1 -1
  23. package/dashboard/out/insights.txt +1 -1
  24. package/dashboard/out/learning.html +1 -1
  25. package/dashboard/out/learning.txt +1 -1
  26. package/dashboard/out/overview.html +1 -1
  27. package/dashboard/out/overview.txt +1 -1
  28. package/dashboard/out/scheduler.html +1 -1
  29. package/dashboard/out/scheduler.txt +1 -1
  30. package/dashboard/out/sync.html +1 -1
  31. package/dashboard/out/sync.txt +1 -1
  32. package/dashboard/out/tokens.html +1 -1
  33. package/dashboard/out/tokens.txt +1 -1
  34. package/dashboard/out/waste.html +1 -1
  35. package/dashboard/out/waste.txt +1 -1
  36. package/dashboard/out/wiki.html +1 -1
  37. package/dashboard/out/wiki.txt +1 -1
  38. package/dist/cli.js +2105 -1068
  39. package/package.json +1 -1
  40. package/src/commands/bug-search.ts +3 -3
  41. package/src/commands/detect-waste.ts +34 -25
  42. package/src/commands/init.ts +21 -21
  43. package/src/commands/post-read.ts +6 -3
  44. package/src/commands/post-write.ts +6 -3
  45. package/src/commands/pre-read.ts +14 -10
  46. package/src/commands/pre-write.ts +8 -5
  47. package/src/commands/reflect.ts +12 -7
  48. package/src/commands/session-start.ts +34 -3
  49. package/src/commands/session-stop.ts +10 -6
  50. package/src/commands/status.ts +29 -17
  51. package/src/commands/sync-migrate.ts +330 -0
  52. package/src/commands/sync.ts +75 -1
  53. package/src/commands/update.ts +4 -9
  54. package/src/core/conflict-park.ts +84 -0
  55. package/src/core/dashboard-api.ts +12 -31
  56. package/src/core/note-writer.ts +52 -6
  57. package/src/core/paths.ts +66 -10
  58. package/src/core/state-aggregator.ts +304 -0
  59. package/src/core/state-counters.ts +46 -0
  60. package/src/core/sync-merge-drivers.ts +247 -0
  61. package/src/core/sync.ts +150 -68
  62. package/src/core/token-ledger.ts +19 -3
  63. /package/dashboard/out/_next/static/{EC-_8nIOf1GnPrIqZ7Mk3 → r7Xr9mrUpunsz4QtD3jh1}/_buildManifest.js +0 -0
  64. /package/dashboard/out/_next/static/{EC-_8nIOf1GnPrIqZ7Mk3 → r7Xr9mrUpunsz4QtD3jh1}/_ssgManifest.js +0 -0
@@ -0,0 +1,330 @@
1
+ import {
2
+ existsSync,
3
+ readdirSync,
4
+ statSync,
5
+ mkdirSync,
6
+ writeFileSync,
7
+ readFileSync,
8
+ renameSync,
9
+ unlinkSync,
10
+ } from "fs";
11
+ import { join } from "path";
12
+ import { execSync } from "child_process";
13
+ import {
14
+ minkRoot,
15
+ fileIndexCountersPath,
16
+ } from "../core/paths";
17
+ import {
18
+ MINK_SYNC_VERSION,
19
+ readSyncVersion,
20
+ writeSyncVersion,
21
+ ensureGitignore,
22
+ ensureGitAttributes,
23
+ ensureMergeDriversRegistered,
24
+ isSyncInitialized,
25
+ } from "../core/sync";
26
+ import { getOrCreateDeviceId } from "../core/device";
27
+ import { atomicWriteJson, safeReadJson } from "../core/fs-utils";
28
+ import type { FileIndex } from "../types/file-index";
29
+
30
+ const MIGRATE_LOCK = ".sync-migrate.lock";
31
+ const MIGRATE_LOCK_STALE_MS = 300_000; // 5 minutes
32
+ const MIGRATE_BUDGET_MS = 5_000;
33
+
34
+ function gitSafe(args: string, timeoutMs: number = 5_000): string | null {
35
+ try {
36
+ return execSync(`git ${args}`, {
37
+ cwd: minkRoot(),
38
+ timeout: timeoutMs,
39
+ stdio: ["pipe", "pipe", "pipe"],
40
+ })
41
+ .toString()
42
+ .trim();
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ function acquireLock(): boolean {
49
+ const path = join(minkRoot(), MIGRATE_LOCK);
50
+ if (existsSync(path)) {
51
+ try {
52
+ const ageMs = Date.now() - statSync(path).mtimeMs;
53
+ if (ageMs < MIGRATE_LOCK_STALE_MS) return false;
54
+ } catch {
55
+ // If stat fails, treat as stale and reclaim.
56
+ }
57
+ }
58
+ try {
59
+ writeFileSync(path, `${process.pid}\n`);
60
+ return true;
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+
66
+ function releaseLock(): void {
67
+ try {
68
+ unlinkSync(join(minkRoot(), MIGRATE_LOCK));
69
+ } catch {
70
+ // ignore
71
+ }
72
+ }
73
+
74
+ // Move a file from `from` to `to` using `git mv` when possible (preserves
75
+ // history) and a plain rename otherwise. Returns true if the move succeeded
76
+ // or the source did not exist.
77
+ function migrateFile(from: string, to: string): boolean {
78
+ if (!existsSync(from)) return true;
79
+ mkdirSync(join(to, ".."), { recursive: true });
80
+ // Prefer `git mv` so blame/history follow the file. Fall back to a plain
81
+ // rename if git can't handle it (e.g. the file isn't tracked yet).
82
+ if (gitSafe(`mv "${from}" "${to}"`) !== null) return true;
83
+ try {
84
+ renameSync(from, to);
85
+ return true;
86
+ } catch {
87
+ return false;
88
+ }
89
+ }
90
+
91
+ function migrateProject(projDir: string, deviceId: string): void {
92
+ const shardDir = join(projDir, "state", deviceId);
93
+ mkdirSync(shardDir, { recursive: true });
94
+
95
+ // Move per-device-rewritten files into the device shard. `git mv` preserves
96
+ // history; if a sibling shard already exists for this file (re-running the
97
+ // migration after a partial first run), we leave the sibling alone — it's
98
+ // already in the right place.
99
+ for (const file of [
100
+ "token-ledger.json",
101
+ "token-ledger-archive.json",
102
+ "bug-memory.json",
103
+ "action-log.md",
104
+ ]) {
105
+ const legacy = join(projDir, file);
106
+ const shard = join(shardDir, file);
107
+ if (existsSync(shard)) continue;
108
+ migrateFile(legacy, shard);
109
+ }
110
+
111
+ // learning-memory.md: leave canonical in place. Touch an empty sidecar so
112
+ // future incremental writes have a target.
113
+ const sidecar = join(projDir, `learning-memory.${deviceId}.md`);
114
+ if (!existsSync(sidecar)) {
115
+ try {
116
+ writeFileSync(sidecar, "");
117
+ } catch {
118
+ // best-effort
119
+ }
120
+ }
121
+
122
+ // Drop session.json + scheduler-manifest.json from the index — they remain
123
+ // on disk but stop being synced (they're now gitignored under v2).
124
+ for (const f of ["session.json", "scheduler-manifest.json"]) {
125
+ if (existsSync(join(projDir, f))) {
126
+ gitSafe(`rm --cached "${join(projDir, f)}"`);
127
+ }
128
+ }
129
+
130
+ // Split file-index counters out into a per-device counter file.
131
+ const indexPath = join(projDir, "file-index.json");
132
+ if (existsSync(indexPath)) {
133
+ const raw = safeReadJson(indexPath) as FileIndex | null;
134
+ if (
135
+ raw &&
136
+ typeof raw.header === "object" &&
137
+ raw.header !== null &&
138
+ (raw.header.lifetimeHits > 0 || raw.header.lifetimeMisses > 0)
139
+ ) {
140
+ // Carry forward the existing counters so the per-device telemetry
141
+ // continues uninterrupted on this device.
142
+ atomicWriteJson(fileIndexCountersPathFor(projDir), {
143
+ fileIndexHits: raw.header.lifetimeHits,
144
+ fileIndexMisses: raw.header.lifetimeMisses,
145
+ });
146
+ raw.header.lifetimeHits = 0;
147
+ raw.header.lifetimeMisses = 0;
148
+ atomicWriteJson(indexPath, raw);
149
+ }
150
+ }
151
+ }
152
+
153
+ function fileIndexCountersPathFor(projDir: string): string {
154
+ return join(projDir, ".mink-state-counters.json");
155
+ }
156
+
157
+ function listProjects(): string[] {
158
+ const projectsRoot = join(minkRoot(), "projects");
159
+ if (!existsSync(projectsRoot)) return [];
160
+ try {
161
+ return readdirSync(projectsRoot)
162
+ .filter((name) => {
163
+ try {
164
+ return statSync(join(projectsRoot, name)).isDirectory();
165
+ } catch {
166
+ return false;
167
+ }
168
+ })
169
+ .map((name) => join(projectsRoot, name));
170
+ } catch {
171
+ return [];
172
+ }
173
+ }
174
+
175
+ // True if any legacy v1 state shape still lives at the top level of projDir
176
+ // AND no per-device shard has been populated yet. Once a shard directory has
177
+ // any contents, this project counts as migrated even if a stale legacy file
178
+ // is still on disk — that's the case where the user opened a session
179
+ // mid-migration and writes started landing in the shard. The aggregator unions
180
+ // across legacy + shards on read, so the stale file is harmless until cleaned
181
+ // up; what we must avoid is a permanent re-migrate loop on every session-start.
182
+ function projectNeedsMigration(projDir: string): boolean {
183
+ const stateDir = join(projDir, "state");
184
+ if (existsSync(stateDir)) {
185
+ try {
186
+ const shards = readdirSync(stateDir).filter((d) => {
187
+ try {
188
+ return statSync(join(stateDir, d)).isDirectory();
189
+ } catch {
190
+ return false;
191
+ }
192
+ });
193
+ if (shards.length > 0) return false;
194
+ } catch {
195
+ // fall through
196
+ }
197
+ }
198
+ for (const f of [
199
+ "token-ledger.json",
200
+ "token-ledger-archive.json",
201
+ "bug-memory.json",
202
+ "action-log.md",
203
+ ]) {
204
+ if (existsSync(join(projDir, f))) return true;
205
+ }
206
+ return false;
207
+ }
208
+
209
+ function listProjectsNeedingMigration(): string[] {
210
+ return listProjects().filter(projectNeedsMigration);
211
+ }
212
+
213
+ export interface MigrateResult {
214
+ ranMigration: boolean;
215
+ fromVersion: number;
216
+ toVersion: number;
217
+ message?: string;
218
+ }
219
+
220
+ // Idempotent. Safe to invoke from `mink sync migrate` directly or from a
221
+ // session-start auto-trigger when readSyncVersion() < MINK_SYNC_VERSION.
222
+ //
223
+ // We treat the version marker as a hint, not a gate — a previous partial run
224
+ // (interrupted by the budget cap) may have written v2 with projects still
225
+ // pending. We re-run as long as any project on disk still has legacy files at
226
+ // its top level, regardless of marker.
227
+ export function migrateSyncLayout(): MigrateResult {
228
+ const fromVersion = readSyncVersion();
229
+ const pending = listProjectsNeedingMigration();
230
+ if (fromVersion >= MINK_SYNC_VERSION && pending.length === 0) {
231
+ return {
232
+ ranMigration: false,
233
+ fromVersion,
234
+ toVersion: MINK_SYNC_VERSION,
235
+ message: `already at v${MINK_SYNC_VERSION}`,
236
+ };
237
+ }
238
+
239
+ const start = Date.now();
240
+
241
+ if (!acquireLock()) {
242
+ return {
243
+ ranMigration: false,
244
+ fromVersion,
245
+ toVersion: MINK_SYNC_VERSION,
246
+ message: "another migration is in progress",
247
+ };
248
+ }
249
+
250
+ try {
251
+ // Refresh .gitignore/.gitattributes/merge drivers regardless of whether
252
+ // sync is initialised — they're cheap and idempotent. The merge-driver
253
+ // registration is a no-op when .git/ doesn't exist.
254
+ ensureGitignore();
255
+ if (isSyncInitialized()) {
256
+ ensureGitAttributes();
257
+ ensureMergeDriversRegistered();
258
+ }
259
+
260
+ const deviceId = getOrCreateDeviceId();
261
+
262
+ // Stash uncommitted changes so the migrating commit doesn't sweep up
263
+ // unrelated edits. Best-effort — if nothing to stash, this is a no-op.
264
+ let stashed = false;
265
+ if (isSyncInitialized()) {
266
+ const status = gitSafe("status --porcelain");
267
+ if (status && status.trim().length > 0) {
268
+ if (gitSafe("stash push -m mink-sync-migrate") !== null) {
269
+ stashed = true;
270
+ }
271
+ }
272
+ }
273
+
274
+ // Process pending projects only — already-migrated projects are skipped
275
+ // for free, and we resume work from any prior partial run.
276
+ let processed = 0;
277
+ let remaining = 0;
278
+ for (const projDir of listProjectsNeedingMigration()) {
279
+ if (Date.now() - start > MIGRATE_BUDGET_MS) {
280
+ remaining++;
281
+ continue;
282
+ }
283
+ try {
284
+ migrateProject(projDir, deviceId);
285
+ processed++;
286
+ } catch {
287
+ // best-effort per project — never block migration on one project
288
+ }
289
+ }
290
+
291
+ // Only stamp the version marker once nothing is left to migrate. If we
292
+ // still have pending projects, leave the marker as-is so the next session
293
+ // knows to keep going.
294
+ if (remaining === 0 && listProjectsNeedingMigration().length === 0) {
295
+ writeSyncVersion(MINK_SYNC_VERSION);
296
+ }
297
+
298
+ if (isSyncInitialized() && processed > 0) {
299
+ // Skip the lock file — it's part of migration coordination, not state.
300
+ gitSafe("add -A");
301
+ gitSafe(`reset HEAD ".sync-migrate.lock"`);
302
+ gitSafe(
303
+ `commit -m "mink: migrate sync layout v${fromVersion} -> v${MINK_SYNC_VERSION} (device ${deviceId.slice(0, 8)}, ${processed} projects)"`
304
+ );
305
+ }
306
+
307
+ if (stashed) {
308
+ gitSafe("stash pop");
309
+ }
310
+
311
+ return {
312
+ ranMigration: true,
313
+ fromVersion,
314
+ toVersion: MINK_SYNC_VERSION,
315
+ };
316
+ } finally {
317
+ releaseLock();
318
+ }
319
+ }
320
+
321
+ export function syncMigrateCommand(): void {
322
+ const result = migrateSyncLayout();
323
+ if (!result.ranMigration) {
324
+ console.log(`[mink] sync migrate: ${result.message ?? "no-op"}`);
325
+ return;
326
+ }
327
+ console.log(
328
+ `[mink] sync migrate: v${result.fromVersion} → v${result.toVersion} complete`
329
+ );
330
+ }
@@ -7,6 +7,11 @@ import {
7
7
  isSyncInitialized,
8
8
  } from "../core/sync";
9
9
  import { setConfigValue } from "../core/global-config";
10
+ import { runMergeDriver } from "../core/sync-merge-drivers";
11
+ import {
12
+ listParkedConflicts,
13
+ dropParkedConflict,
14
+ } from "../core/conflict-park";
10
15
 
11
16
  export async function sync(args: string[]): Promise<void> {
12
17
  const subcommand = args[0];
@@ -39,13 +44,82 @@ export async function sync(args: string[]): Promise<void> {
39
44
  case "disconnect":
40
45
  return handleDisconnect();
41
46
 
47
+ case "merge-driver":
48
+ return handleMergeDriver(args.slice(1));
49
+
50
+ case "reconcile":
51
+ return handleReconcile(args.slice(1));
52
+
53
+ case "migrate": {
54
+ const { syncMigrateCommand } = await import("./sync-migrate");
55
+ syncMigrateCommand();
56
+ return;
57
+ }
58
+
42
59
  default:
43
60
  console.error(`[mink] unknown sync subcommand: ${subcommand}`);
44
- console.error("Usage: mink sync [init|status|push|pull|pause|resume|disconnect]");
61
+ console.error(
62
+ "Usage: mink sync [init|status|push|pull|pause|resume|disconnect|reconcile|migrate|merge-driver]"
63
+ );
45
64
  process.exit(1);
46
65
  }
47
66
  }
48
67
 
68
+ function handleReconcile(args: string[]): void {
69
+ const sub = args[0];
70
+ if (sub === undefined || sub === "list") {
71
+ const refs = listParkedConflicts();
72
+ if (refs.length === 0) {
73
+ console.log("[mink] no parked conflicts");
74
+ return;
75
+ }
76
+ console.log(`[mink] ${refs.length} parked conflict ref(s):`);
77
+ for (const r of refs) console.log(` ${r}`);
78
+ console.log(
79
+ "Inspect with: cd ~/.mink && git log <ref> | git diff main..<ref>"
80
+ );
81
+ console.log("Drop with: mink sync reconcile drop <ref>");
82
+ return;
83
+ }
84
+ if (sub === "drop") {
85
+ const ref = args[1];
86
+ if (!ref) {
87
+ console.error("Usage: mink sync reconcile drop <ref>");
88
+ process.exit(1);
89
+ }
90
+ if (dropParkedConflict(ref)) {
91
+ console.log(`[mink] dropped ${ref}`);
92
+ } else {
93
+ console.error(`[mink] failed to drop ${ref} — only refs/mink/conflicts/* are droppable`);
94
+ process.exit(1);
95
+ }
96
+ return;
97
+ }
98
+ console.error("Usage: mink sync reconcile [list|drop <ref>]");
99
+ process.exit(1);
100
+ }
101
+
102
+ // Invoked by git for paths matched in .gitattributes. Always exits 0 so a
103
+ // merge can never block — the driver itself logs warnings and falls back to
104
+ // "ours" when inputs are unparseable.
105
+ function handleMergeDriver(args: string[]): void {
106
+ const [name, basePath, oursPath, theirsPath, filePath] = args;
107
+ if (!name || !basePath || !oursPath || !theirsPath) {
108
+ console.error(
109
+ "Usage: mink sync merge-driver <name> <base> <ours> <theirs> [path]"
110
+ );
111
+ process.exit(0); // exit 0 so git never sees a failure here
112
+ }
113
+ const code = runMergeDriver(
114
+ name,
115
+ basePath,
116
+ oursPath,
117
+ theirsPath,
118
+ filePath ?? oursPath
119
+ );
120
+ process.exit(code);
121
+ }
122
+
49
123
  function handleManualSync(): void {
50
124
  if (!isSyncInitialized()) {
51
125
  console.error("[mink] sync is not initialized");
@@ -1,10 +1,9 @@
1
- import { resolve, dirname, basename } from "path";
2
- import { existsSync } from "fs";
1
+ import { resolve } from "path";
3
2
  import { listRegisteredProjects } from "../core/project-registry";
4
3
  import { createBackup } from "../core/backup";
5
4
  import { projectMetaPath } from "../core/paths";
6
5
  import { atomicWriteJson, safeReadJson } from "../core/fs-utils";
7
- import { buildHooksConfig, detectRuntime, mergeHooksIntoSettings } from "./init";
6
+ import { buildHooksConfig, mergeHooksIntoSettings, resolveCliPath } from "./init";
8
7
 
9
8
  function parseArgs(args: string[]): {
10
9
  dryRun: boolean;
@@ -78,12 +77,8 @@ export async function update(cwd: string, args: string[]): Promise<void> {
78
77
  return;
79
78
  }
80
79
 
81
- const runtime = detectRuntime();
82
- const cliPath = resolve(
83
- dirname(new URL(import.meta.url).pathname),
84
- "../cli.ts"
85
- );
86
- const newHooks = buildHooksConfig(runtime, cliPath);
80
+ const cliPath = resolveCliPath();
81
+ const newHooks = buildHooksConfig(cliPath);
87
82
 
88
83
  for (const target of targets) {
89
84
  console.log(`[mink] updating: ${target.name} (${target.id})`);
@@ -0,0 +1,84 @@
1
+ import { execSync } from "child_process";
2
+ import { existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { minkRoot } from "./paths";
5
+ import { getOrCreateDeviceId } from "./device";
6
+
7
+ const GIT_TIMEOUT = 5_000;
8
+
9
+ function git(args: string): string {
10
+ return execSync(`git ${args}`, {
11
+ cwd: minkRoot(),
12
+ timeout: GIT_TIMEOUT,
13
+ stdio: ["pipe", "pipe", "pipe"],
14
+ })
15
+ .toString()
16
+ .trim();
17
+ }
18
+
19
+ function gitSafe(args: string): string | null {
20
+ try {
21
+ return git(args);
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ // Park the current local state onto a hidden ref so an unresolvable merge
28
+ // never blocks sync. Sequence:
29
+ // 1. If a merge is in progress, abort it cleanly (`git merge --abort`).
30
+ // 2. Save HEAD as `refs/mink/conflicts/<deviceId>/<iso-utc>`.
31
+ // 3. Hard-reset working tree to upstream (origin/<branch>) so subsequent
32
+ // writes start from a clean, fast-forwardable state.
33
+ // Returns the parked refname (or null if the operation was a no-op or failed —
34
+ // callers must NEVER throw on the result).
35
+ export function parkConflictingState(reason: string): string | null {
36
+ const root = minkRoot();
37
+ const inMerge =
38
+ existsSync(join(root, ".git", "MERGE_HEAD")) ||
39
+ existsSync(join(root, ".git", "rebase-merge")) ||
40
+ existsSync(join(root, ".git", "rebase-apply"));
41
+
42
+ if (inMerge) {
43
+ gitSafe("merge --abort");
44
+ gitSafe("rebase --abort");
45
+ }
46
+
47
+ const branch = gitSafe("rev-parse --abbrev-ref HEAD") ?? "main";
48
+ const upstream = `origin/${branch}`;
49
+
50
+ // Don't park if there's nothing to save (HEAD already matches upstream).
51
+ const headSha = gitSafe("rev-parse HEAD");
52
+ const upstreamSha = gitSafe(`rev-parse ${upstream}`);
53
+ if (headSha && headSha === upstreamSha) {
54
+ return null;
55
+ }
56
+
57
+ const deviceId = getOrCreateDeviceId();
58
+ const iso = new Date().toISOString().replace(/[:.]/g, "-");
59
+ const ref = `refs/mink/conflicts/${deviceId}/${iso}`;
60
+
61
+ if (!headSha) return null;
62
+
63
+ if (gitSafe(`update-ref ${ref} ${headSha}`) === null) {
64
+ return null;
65
+ }
66
+ gitSafe(`reset --hard ${upstream}`);
67
+
68
+ return ref;
69
+ }
70
+
71
+ // List previously-parked conflict refs. Used by `mink sync reconcile list`.
72
+ export function listParkedConflicts(): string[] {
73
+ const out = gitSafe("for-each-ref --format=%(refname) refs/mink/conflicts");
74
+ if (!out) return [];
75
+ return out
76
+ .split("\n")
77
+ .map((s) => s.trim())
78
+ .filter((s) => s.startsWith("refs/mink/conflicts/"));
79
+ }
80
+
81
+ export function dropParkedConflict(ref: string): boolean {
82
+ if (!ref.startsWith("refs/mink/conflicts/")) return false;
83
+ return gitSafe(`update-ref -d ${ref}`) !== null;
84
+ }
@@ -18,6 +18,12 @@ import { loadLedger } from "./token-ledger";
18
18
  import { parseLearningMemory } from "./learning-memory";
19
19
  import { loadBugMemory } from "./bug-memory";
20
20
  import { safeReadLog, parseLogSessions } from "./action-log";
21
+ import {
22
+ aggregateTokenLedger,
23
+ aggregateBugMemory,
24
+ aggregateActionLog,
25
+ aggregateLearningMemory,
26
+ } from "./state-aggregator";
21
27
  import { getDaemonStatus, startDaemon, stopDaemon } from "./daemon";
22
28
  import { loadManifest, removeFromDeadLetter, saveManifest } from "./scheduler";
23
29
  import { getBuiltInTasks, executeTask } from "./task-registry";
@@ -147,8 +153,8 @@ export function loadOverview(cwd: string): OverviewPayload {
147
153
  : undefined,
148
154
  };
149
155
 
150
- // Token ledger summary
151
- const ledger = loadLedger(tokenLedgerPath(cwd));
156
+ // Token ledger summary (aggregated across all device shards + legacy)
157
+ const ledger = aggregateTokenLedger(cwd);
152
158
  const summary = {
153
159
  totalSessions: ledger.lifetime.totalSessions,
154
160
  totalTokens: ledger.lifetime.totalTokens,
@@ -173,7 +179,7 @@ export function loadOverview(cwd: string): OverviewPayload {
173
179
  }
174
180
 
175
181
  export function loadTokenLedgerPanel(cwd: string): TokenLedgerPayload {
176
- const ledger = loadLedger(tokenLedgerPath(cwd));
182
+ const ledger = aggregateTokenLedger(cwd);
177
183
  return {
178
184
  lifetime: ledger.lifetime,
179
185
  sessions: ledger.sessions,
@@ -216,42 +222,17 @@ export function loadSchedulerPanel(cwd: string): SchedulerPayload {
216
222
  }
217
223
 
218
224
  export function loadLearningMemoryPanel(cwd: string): LearningMemory {
219
- const memPath = learningMemoryPath(cwd);
220
- if (!existsSync(memPath)) {
221
- return {
222
- projectName: "unknown",
223
- sections: {
224
- "User Preferences": [],
225
- "Key Learnings": [],
226
- "Do-Not-Repeat": [],
227
- "Decision Log": [],
228
- },
229
- };
230
- }
231
- try {
232
- const content = readFileSync(memPath, "utf-8");
233
- return parseLearningMemory(content);
234
- } catch {
235
- return {
236
- projectName: "unknown",
237
- sections: {
238
- "User Preferences": [],
239
- "Key Learnings": [],
240
- "Do-Not-Repeat": [],
241
- "Decision Log": [],
242
- },
243
- };
244
- }
225
+ return aggregateLearningMemory(cwd);
245
226
  }
246
227
 
247
228
  export function loadActionLogPanel(cwd: string): ActionLogPayload {
248
- const content = safeReadLog(actionLogPath(cwd));
229
+ const content = aggregateActionLog(cwd);
249
230
  const sessions = parseLogSessions(content);
250
231
  return { sessions };
251
232
  }
252
233
 
253
234
  export function loadBugLogPanel(cwd: string): BugLogPayload {
254
- const memory = loadBugMemory(bugMemoryPath(cwd));
235
+ const memory = aggregateBugMemory(cwd);
255
236
  return { entries: memory.entries, nextId: memory.nextId };
256
237
  }
257
238