@kanadego/dsh-heartbeat 1.7.4 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,13 +3,18 @@ import {
3
3
  loadPool,
4
4
  normalizeCategory,
5
5
  seedsFilePath
6
- } from "./chunk-SVP2NDRF.js";
6
+ } from "./chunk-PQTSXW4F.js";
7
7
  import {
8
+ atomicWriteJsonSync
9
+ } from "./chunk-VIZNIQLK.js";
10
+ import {
11
+ loadEncryptedText,
8
12
  loadJson,
9
13
  readText,
14
+ saveEncryptedText,
10
15
  saveJson,
11
16
  writeText
12
- } from "./chunk-LLD7LUNN.js";
17
+ } from "./chunk-IFTFDHZX.js";
13
18
 
14
19
  // src/core/path-guard.ts
15
20
  import fs from "fs";
@@ -71,80 +76,6 @@ function createPathGuard(workspaceDir) {
71
76
  return guard;
72
77
  }
73
78
 
74
- // src/core/audit-log.ts
75
- import fs3 from "fs";
76
- import path3 from "path";
77
-
78
- // src/core/atomic-fs.ts
79
- import fs2 from "fs";
80
- import path2 from "path";
81
- import { randomUUID, randomFillSync } from "crypto";
82
- function tmpSibling(target, tag = "w") {
83
- return path2.join(
84
- path2.dirname(target),
85
- `.${path2.basename(target)}.${tag}-${randomUUID().slice(0, 8)}.tmp`
86
- );
87
- }
88
- function atomicWriteFileSync(target, data) {
89
- const tmp = tmpSibling(target);
90
- try {
91
- fs2.writeFileSync(tmp, data);
92
- fs2.renameSync(tmp, target);
93
- } finally {
94
- fs2.rmSync(tmp, { force: true });
95
- }
96
- }
97
- function atomicWriteJsonSync(target, value) {
98
- atomicWriteFileSync(target, JSON.stringify(value, null, 2));
99
- }
100
- function shredFileSync(target, passes = 3) {
101
- const stat = fs2.statSync(target);
102
- if (!stat.isFile()) throw new Error(`shred: not a file: ${target}`);
103
- const buf = Buffer.alloc(Math.max(stat.size, 1));
104
- for (let i = 0; i < passes; i++) {
105
- randomFillSync(buf);
106
- fs2.writeFileSync(target, buf);
107
- }
108
- fs2.rmSync(target, { force: true });
109
- }
110
-
111
- // src/core/audit-log.ts
112
- function appendAuditLine(file, event) {
113
- fs3.mkdirSync(path3.dirname(file), { recursive: true });
114
- const line = JSON.stringify({ ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(), ...event });
115
- fs3.appendFileSync(file, line + "\n", "utf8");
116
- }
117
- function readAuditLines(file) {
118
- if (!fs3.existsSync(file)) return [];
119
- const out = [];
120
- const raw = fs3.readFileSync(file, "utf8");
121
- for (const line of raw.split("\n")) {
122
- const trimmed = line.trim();
123
- if (!trimmed) continue;
124
- try {
125
- out.push(JSON.parse(trimmed));
126
- } catch {
127
- out.push({ ts: "", corrupt: true, raw: trimmed.slice(0, 200) });
128
- }
129
- }
130
- return out;
131
- }
132
- function pruneAuditFile(file, maxAgeMs, now = Date.now()) {
133
- if (!fs3.existsSync(file)) return 0;
134
- const lines = readAuditLines(file);
135
- const kept = lines.filter((e) => {
136
- const ev = e;
137
- const ts = Date.parse(ev.ts ?? "");
138
- if (!Number.isFinite(ts)) return true;
139
- return now - ts <= maxAgeMs;
140
- });
141
- const removed = lines.length - kept.length;
142
- if (removed === 0) return 0;
143
- const body = kept.map((e) => JSON.stringify(e)).join("\n");
144
- atomicWriteFileSync(file, body ? body + "\n" : "");
145
- return removed;
146
- }
147
-
148
79
  // src/config/schema.ts
149
80
  var HHMM = /^([01]\d|2[0-3]):[0-5]\d$/;
150
81
  function isPlainObject(v) {
@@ -162,6 +93,7 @@ function assertPolicy(input) {
162
93
  fail("heartbeat.intervalMin must be a number in [1, 1440]");
163
94
  }
164
95
  if (typeof hb.idleMode !== "boolean") fail("heartbeat.idleMode must be boolean");
96
+ if (typeof hb.archiveRotatedHome !== "boolean") fail("heartbeat.archiveRotatedHome must be boolean");
165
97
  const g = p.gate;
166
98
  if (!isPlainObject(g)) fail("gate missing");
167
99
  if (typeof g.maxDailySend !== "number" || g.maxDailySend < 0) fail("gate.maxDailySend must be >= 0");
@@ -190,6 +122,7 @@ function assertPolicy(input) {
190
122
  if (typeof s.ttlDays[k] !== "number") fail(`seeds.ttlDays.${k} missing`);
191
123
  }
192
124
  if (typeof s.coldBenchDays !== "number") fail("seeds.coldBenchDays missing");
125
+ if (typeof s.archiveCap !== "number" || s.archiveCap < 1) fail("seeds.archiveCap must be >= 1");
193
126
  if (typeof s.retireAfterUsed !== "number" || s.retireAfterUsed < 1) fail("seeds.retireAfterUsed must be >= 1");
194
127
  if (!isPlainObject(s.scoreWeights)) fail("seeds.scoreWeights missing");
195
128
  const pr = p.profile;
@@ -207,6 +140,12 @@ function assertPolicy(input) {
207
140
  if (typeof pr.volatileDays !== "number" || pr.volatileDays < 1) fail("profile.volatileDays must be >= 1");
208
141
  if (typeof pr.stableLowActivityDays !== "number" || pr.stableLowActivityDays < 1) fail("profile.stableLowActivityDays must be >= 1");
209
142
  if (typeof pr.psyEnabled !== "boolean") fail("profile.psyEnabled must be boolean");
143
+ const ob = p.observe;
144
+ if (!isPlainObject(ob)) fail("observe missing");
145
+ if (typeof ob.maxChars !== "number" || ob.maxChars < 20 || ob.maxChars > 2e3) fail("observe.maxChars must be in [20, 2000]");
146
+ if (typeof ob.perBeat !== "number" || ob.perBeat < 1 || ob.perBeat > 100) fail("observe.perBeat must be in [1, 100]");
147
+ if (!isPlainObject(p.weekly)) fail("weekly missing");
148
+ if (typeof p.weekly.enabled !== "boolean") fail("weekly.enabled must be boolean");
210
149
  const r = p.retention;
211
150
  if (!isPlainObject(r)) fail("retention missing");
212
151
  if (typeof r.envPulseHours !== "number" || typeof r.decisionLogDays !== "number") fail("retention fields missing");
@@ -223,23 +162,23 @@ function deepMerge(base, override) {
223
162
  }
224
163
 
225
164
  // src/config/load.ts
226
- import fs4 from "fs";
227
- import path4 from "path";
165
+ import fs2 from "fs";
166
+ import path2 from "path";
228
167
  var USER_POLICY_FILE = "policy.json";
229
168
  function loadPolicy(guard, configDir, settingsDir) {
230
- const factoryPath = path4.join(configDir, "policy.json");
169
+ const factoryPath = path2.join(configDir, "policy.json");
231
170
  let factoryRaw;
232
171
  try {
233
- factoryRaw = JSON.parse(fs4.readFileSync(factoryPath, "utf8"));
172
+ factoryRaw = JSON.parse(fs2.readFileSync(factoryPath, "utf8"));
234
173
  } catch (e) {
235
174
  throw new Error(`factory policy unreadable at ${factoryPath}: ${String(e)}`);
236
175
  }
237
176
  assertPolicy(factoryRaw);
238
- const userPath = guard.assert(path4.join(settingsDir, USER_POLICY_FILE));
177
+ const userPath = guard.assert(path2.join(settingsDir, USER_POLICY_FILE));
239
178
  let merged = factoryRaw;
240
- if (fs4.existsSync(userPath)) {
179
+ if (fs2.existsSync(userPath)) {
241
180
  try {
242
- const userRaw = JSON.parse(fs4.readFileSync(userPath, "utf8"));
181
+ const userRaw = JSON.parse(fs2.readFileSync(userPath, "utf8"));
243
182
  merged = deepMerge(factoryRaw, userRaw);
244
183
  } catch (e) {
245
184
  throw new Error(`user policy layer unparseable at ${userPath}: ${String(e)}`);
@@ -249,26 +188,26 @@ function loadPolicy(guard, configDir, settingsDir) {
249
188
  return merged;
250
189
  }
251
190
  function updateUserPolicy(guard, settingsDir, patch) {
252
- const userPath = guard.assert(path4.join(settingsDir, USER_POLICY_FILE));
191
+ const userPath = guard.assert(path2.join(settingsDir, USER_POLICY_FILE));
253
192
  let user = {};
254
193
  try {
255
- user = JSON.parse(fs4.readFileSync(userPath, "utf8"));
194
+ user = JSON.parse(fs2.readFileSync(userPath, "utf8"));
256
195
  if (!user || typeof user !== "object" || Array.isArray(user)) user = {};
257
196
  } catch {
258
197
  user = {};
259
198
  }
260
199
  const merged = deepMerge(user, patch);
261
- fs4.mkdirSync(path4.dirname(userPath), { recursive: true });
262
- fs4.writeFileSync(userPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
200
+ fs2.mkdirSync(path2.dirname(userPath), { recursive: true });
201
+ fs2.writeFileSync(userPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
263
202
  return merged;
264
203
  }
265
204
 
266
205
  // src/ledger/ledger.ts
267
- import path5 from "path";
268
- import { randomUUID as randomUUID2 } from "crypto";
206
+ import path3 from "path";
207
+ import { randomUUID } from "crypto";
269
208
  var DAY_MS = 864e5;
270
209
  function ledgerFilePath(dataDir) {
271
- return path5.join(dataDir, "ledger.md");
210
+ return path3.join(dataDir, "ledger.md");
272
211
  }
273
212
  var LINE_RE = /^- \[(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})\]\[(open|done)\]\[#([0-9a-f]{6})\] (.*)$/;
274
213
  function renderEntry(e) {
@@ -294,7 +233,7 @@ function appendEntry(guard, file, text, now = Date.now()) {
294
233
  const d = new Date(now);
295
234
  const pad = (n) => String(n).padStart(2, "0");
296
235
  const entry = {
297
- id: randomUUID2().slice(0, 6),
236
+ id: randomUUID().slice(0, 6),
298
237
  date: `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`,
299
238
  time: `${pad(d.getHours())}:${pad(d.getMinutes())}`,
300
239
  status: "open",
@@ -311,9 +250,10 @@ function markDone(guard, file, key, now = Date.now()) {
311
250
  if (!target) return null;
312
251
  const d = new Date(now);
313
252
  const pad = (n) => String(n).padStart(2, "0");
253
+ const doneDate = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
314
254
  const out = rawLines.map((line) => {
315
255
  if (line.includes(`#${target.id}] `)) {
316
- return `- [${target.date} ${pad(d.getHours())}:${pad(d.getMinutes())}][done][#${target.id}] ${target.text}`;
256
+ return `- [${doneDate} ${pad(d.getHours())}:${pad(d.getMinutes())}][done][#${target.id}] ${target.text}`;
317
257
  }
318
258
  return line;
319
259
  });
@@ -330,32 +270,111 @@ function pendingOlderThan(guard, file, days, now = Date.now()) {
330
270
  }
331
271
 
332
272
  // src/browse/browse.ts
333
- import fs5 from "fs";
334
- import path6 from "path";
273
+ import fs3 from "fs";
274
+ import path5 from "path";
275
+
276
+ // src/browse/preference.ts
277
+ import path4 from "path";
278
+ var DAY_MS2 = 864e5;
279
+ var W_MIN = 0.5;
280
+ var W_MAX = 2;
281
+ var SMOOTH_ADOPTED = 1;
282
+ var SMOOTH_DELIVERED = 4;
283
+ var DECAY_HALF_LIFE_DAYS = 30;
284
+ function preferenceFilePath(dataDir) {
285
+ return path4.join(dataDir, "preference.json");
286
+ }
287
+ function num(v) {
288
+ const n = Number(v);
289
+ return Number.isFinite(n) && n >= 0 ? n : 0;
290
+ }
291
+ function sanitizeTopicPref(raw) {
292
+ if (!raw || typeof raw !== "object") return null;
293
+ const t = raw;
294
+ if (!Number.isFinite(Number(t.delivered)) || !Number.isFinite(Number(t.adopted))) return null;
295
+ const delivered = Math.floor(num(t.delivered));
296
+ return {
297
+ delivered,
298
+ adopted: Math.min(delivered, Math.floor(num(t.adopted))),
299
+ lastDeliveredAt: num(t.lastDeliveredAt),
300
+ lastAdoptedAt: num(t.lastAdoptedAt)
301
+ };
302
+ }
303
+ function loadPreference(guard, file) {
304
+ const doc = loadJson(guard, file);
305
+ if (!doc || typeof doc !== "object" || !doc.topics || typeof doc.topics !== "object") {
306
+ return { version: 1, topics: {}, updatedAt: 0 };
307
+ }
308
+ const topics = {};
309
+ for (const [key, raw] of Object.entries(doc.topics)) {
310
+ const t = sanitizeTopicPref(raw);
311
+ if (key && t) topics[key] = t;
312
+ }
313
+ const updatedAt = Number(doc.updatedAt);
314
+ return { version: 1, topics, updatedAt: Number.isFinite(updatedAt) ? updatedAt : 0 };
315
+ }
316
+ function savePreference(guard, file, state) {
317
+ saveJson(guard, file, state);
318
+ }
319
+ function recordDelivery(guard, file, opts) {
320
+ const state = loadPreference(guard, file);
321
+ const adopted = new Set(opts.adoptedTopics);
322
+ for (const topic of opts.offeredTopics) {
323
+ if (!topic) continue;
324
+ const t = state.topics[topic] ?? { delivered: 0, adopted: 0, lastDeliveredAt: 0, lastAdoptedAt: 0 };
325
+ t.delivered += 1;
326
+ t.lastDeliveredAt = opts.now;
327
+ if (adopted.has(topic)) {
328
+ t.adopted += 1;
329
+ t.lastAdoptedAt = opts.now;
330
+ }
331
+ state.topics[topic] = t;
332
+ }
333
+ state.updatedAt = opts.now;
334
+ savePreference(guard, file, state);
335
+ return state;
336
+ }
337
+ function decayed(t, now) {
338
+ const dAgeDays = Math.max(0, now - t.lastDeliveredAt) / DAY_MS2;
339
+ const aAgeDays = Math.max(0, now - t.lastAdoptedAt) / DAY_MS2;
340
+ const dHalf = Math.pow(0.5, dAgeDays / DECAY_HALF_LIFE_DAYS);
341
+ const aHalf = Math.pow(0.5, aAgeDays / DECAY_HALF_LIFE_DAYS);
342
+ return { delivered: t.delivered * dHalf, adopted: t.adopted * aHalf };
343
+ }
344
+ function preferenceWeight(state, topic, now) {
345
+ const t = state.topics[topic];
346
+ const rate = t ? (() => {
347
+ const d = decayed(t, now);
348
+ return (d.adopted + SMOOTH_ADOPTED) / (d.delivered + SMOOTH_DELIVERED);
349
+ })() : SMOOTH_ADOPTED / SMOOTH_DELIVERED;
350
+ return Math.min(W_MAX, Math.max(W_MIN, rate / (SMOOTH_ADOPTED / SMOOTH_DELIVERED)));
351
+ }
352
+
353
+ // src/browse/browse.ts
335
354
  var WATCH_THROTTLE_MS = 6 * 36e5;
336
355
  var UA = { "User-Agent": "dsh-heartbeat/2.0 (+local; personal companion)" };
337
356
  function emptyBrowseState() {
338
357
  return { targets: {}, last_check_at: 0, wander: { focusHistory: {}, focusCount: {}, last_wander_at: 0, refillCount: {} } };
339
358
  }
340
359
  function browseStatePath(paths) {
341
- return path6.join(paths.dataDir, "browse.json");
360
+ return path5.join(paths.dataDir, "browse.json");
342
361
  }
343
362
  function readJsonFile(file, fallback) {
344
363
  try {
345
- return JSON.parse(fs5.readFileSync(file, "utf8"));
364
+ return JSON.parse(fs3.readFileSync(file, "utf8"));
346
365
  } catch {
347
366
  return fallback;
348
367
  }
349
368
  }
350
369
  function loadInterests(paths) {
351
- const userPath = path6.join(paths.settingsDir, "interests.json");
352
- if (fs5.existsSync(userPath)) return readJsonFile(userPath, { interests: [], _schedule: {} });
353
- return readJsonFile(path6.join(paths.configDir, "interests.json"), { interests: [], _schedule: {} });
370
+ const userPath = path5.join(paths.settingsDir, "interests.json");
371
+ if (fs3.existsSync(userPath)) return readJsonFile(userPath, { interests: [], _schedule: {} });
372
+ return readJsonFile(path5.join(paths.configDir, "interests.json"), { interests: [], _schedule: {} });
354
373
  }
355
374
  function loadWatchlist(paths) {
356
- const userPath = path6.join(paths.settingsDir, "watchlist.json");
357
- if (fs5.existsSync(userPath)) return readJsonFile(userPath, { targets: [] });
358
- return readJsonFile(path6.join(paths.configDir, "watchlist.json"), { targets: [] });
375
+ const userPath = path5.join(paths.settingsDir, "watchlist.json");
376
+ if (fs3.existsSync(userPath)) return readJsonFile(userPath, { targets: [] });
377
+ return readJsonFile(path5.join(paths.configDir, "watchlist.json"), { targets: [] });
359
378
  }
360
379
  function loadState(guard, paths) {
361
380
  return loadJson(guard, browseStatePath(paths)) ?? emptyBrowseState();
@@ -423,12 +442,24 @@ function onCooldown(state, focus, cooldownDays, now) {
423
442
  const last = state.wander.focusHistory[focus] ?? 0;
424
443
  return last > now - cooldownDays * 864e5;
425
444
  }
426
- function pickFocus(state, interests, now) {
445
+ var STARVATION_DAYS = 14;
446
+ function pickFocus(state, interests, now, pref) {
427
447
  const sc = interests._schedule ?? {};
428
448
  const cooldown = sc.focus_cooldown_days ?? 3;
429
449
  const pool = (interests.interests ?? []).filter((t) => !onCooldown(state, t, cooldown, now));
430
450
  if (pool.length === 0) return null;
431
- pool.sort((a, b) => (state.wander.focusHistory[a] ?? 0) - (state.wander.focusHistory[b] ?? 0));
451
+ if (!pref) {
452
+ pool.sort((a, b) => (state.wander.focusHistory[a] ?? 0) - (state.wander.focusHistory[b] ?? 0));
453
+ return pool[0];
454
+ }
455
+ const age = (t) => now - (state.wander.focusHistory[t] ?? 0);
456
+ const starved = pool.filter((t) => age(t) >= STARVATION_DAYS * 864e5);
457
+ if (starved.length > 0) {
458
+ starved.sort((a, b) => age(b) - age(a));
459
+ return starved[0];
460
+ }
461
+ const effAge = (t) => age(t) * preferenceWeight(pref, t, now);
462
+ pool.sort((a, b) => effAge(b) - effAge(a));
432
463
  return pool[0];
433
464
  }
434
465
  function adviseWander(guard, paths, policy, now = /* @__PURE__ */ new Date()) {
@@ -444,7 +475,7 @@ function adviseWander(guard, paths, policy, now = /* @__PURE__ */ new Date()) {
444
475
  if (now.getTime() - state.wander.last_wander_at < minGap) {
445
476
  return { focus: null, query: null, skipped: "min-interval" };
446
477
  }
447
- const focus = pickFocus(state, interests, now.getTime());
478
+ const focus = pickFocus(state, interests, now.getTime(), loadPreference(guard, preferenceFilePath(paths.dataDir)));
448
479
  if (!focus) return { focus: null, query: null, skipped: "no-focus" };
449
480
  return { focus, query: `${focus} 2026 \u6700\u65B0`, skipped: null };
450
481
  }
@@ -483,7 +514,7 @@ function adviseRefillWander(guard, paths, policy, now = /* @__PURE__ */ new Date
483
514
  return { focus: null, query: null, skipped: `topic-stock-ok(${topicCount})`, topicCount, refillsToday };
484
515
  }
485
516
  const interests = loadInterests(paths);
486
- const focus = pickFocus(state, interests, now.getTime());
517
+ const focus = pickFocus(state, interests, now.getTime(), loadPreference(guard, preferenceFilePath(paths.dataDir)));
487
518
  if (!focus) {
488
519
  return { focus: null, query: null, skipped: "no-focus", topicCount, refillsToday };
489
520
  }
@@ -493,376 +524,152 @@ function browseStatus(guard, paths) {
493
524
  return loadState(guard, paths);
494
525
  }
495
526
 
496
- // src/profile/store.ts
497
- import path8 from "path";
498
- import { randomUUID as randomUUID3 } from "crypto";
499
- import fs7 from "fs";
500
-
501
- // src/profile/types.ts
502
- var PARTITIONS = ["interest", "projects", "comm", "psy"];
503
- var CONFIDENCE_CAP = {
504
- chat: 0.6,
505
- screen: 0.4,
506
- browse: 0.4,
507
- hand: 1,
508
- ledger: 0.6
509
- };
510
- function emptyProfile() {
511
- return {
512
- version: 1,
513
- partitions: { interest: { entries: [] }, projects: { entries: [] }, comm: { entries: [] }, psy: { entries: [] } }
514
- };
527
+ // src/notify/notify.ts
528
+ import { spawnSync } from "child_process";
529
+ import path6 from "path";
530
+ function runNotify(paths, args) {
531
+ const r = spawnSync(
532
+ "powershell.exe",
533
+ ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", path6.join(paths.assetsDir, "notify.ps1"), ...args],
534
+ { timeout: 2e4, encoding: "utf8" }
535
+ );
536
+ return { status: r.status ?? -1, out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
515
537
  }
516
-
517
- // src/profile/schema.ts
518
- import fs6 from "fs";
519
- import path7 from "path";
520
- function loadProfileSchema(paths) {
521
- const userPath = path7.join(paths.settingsDir, "profile-schema.json");
522
- const file = fs6.existsSync(userPath) ? userPath : path7.join(paths.configDir, "profile-schema.json");
523
- try {
524
- const raw = JSON.parse(fs6.readFileSync(file, "utf8"));
525
- if (!raw.partitions) throw new Error("partitions missing");
526
- return raw;
527
- } catch (e) {
528
- throw new Error(`profile-schema unreadable at ${file}: ${String(e)}`);
529
- }
538
+ function ensureRegistered(paths) {
539
+ const check = runNotify(paths, ["-Check"]);
540
+ if (/REGISTERED: yes/.test(check.out)) return true;
541
+ const reg = runNotify(paths, ["-RegisterOnly"]);
542
+ return reg.status === 0;
530
543
  }
531
- function checkAddAgainstSchema(schema, partition, topic, subTopic, nominated) {
532
- const p = schema.partitions[partition];
533
- if (!p) return { ok: false, reason: `partition not in schema: ${partition}`, temporal: "stable" };
534
- const t = p.topics[topic];
535
- if (!t) return { ok: false, reason: `topic not in schema: ${partition}/${topic}`, temporal: "stable" };
536
- const st = t.subtopics[subTopic];
537
- if (!st) return { ok: false, reason: `sub_topic not in schema: ${partition}/${topic}/${subTopic}`, temporal: "stable" };
538
- const allowed = st.allowed && st.allowed.length > 0 ? st.allowed : ["stable"];
539
- const def = st.default && allowed.includes(st.default) ? st.default : allowed[0];
540
- if (!nominated) return { ok: true, temporal: def };
541
- if (!allowed.includes(nominated)) {
542
- return {
543
- ok: false,
544
- reason: `temporal "${nominated}" not allowed for ${partition}/${topic}/${subTopic} (allowed: ${allowed.join("|")})`,
545
- temporal: def
546
- };
547
- }
548
- return { ok: true, temporal: nominated };
544
+ function sendNewMessageHint(paths) {
545
+ const r = runNotify(paths, ["-Title", "Heartbeat", "-Message", "\u6709\u65B0\u6D88\u606F"]);
546
+ return r.status === 0 && /TOAST_SENT/.test(r.out);
549
547
  }
550
-
551
- // src/profile/store.ts
552
- var DAY_MS2 = 864e5;
553
- function profileFilePath(dataDir) {
554
- return path8.join(dataDir, "profile.json");
548
+ function sendWeeklyReadyHint(paths) {
549
+ const r = runNotify(paths, ["-Title", "\u5FC3\u8DF3\u5468\u62A5", "-Message", "\u672C\u671F\u5468\u62A5\u5DF2\u751F\u6210"]);
550
+ return r.status === 0 && /TOAST_SENT/.test(r.out);
555
551
  }
556
- function journalFilePath(dataDir) {
557
- return path8.join(dataDir, "profile_journal.jsonl");
552
+
553
+ // src/weekly/report.ts
554
+ import fs4 from "fs";
555
+ import path7 from "path";
556
+ var WEEKLY_INTERVAL_MS = 7 * 864e5;
557
+ function weeklyDirPath(dataDir) {
558
+ return path7.join(dataDir, "weekly");
558
559
  }
559
- function loadProfile(guard, file) {
560
- const doc = loadJson(guard, file);
561
- if (!doc || !doc.partitions) return emptyProfile();
562
- for (const p of PARTITIONS) {
563
- if (!doc.partitions[p]) doc.partitions[p] = { entries: [] };
564
- }
565
- return doc;
560
+ function stateFilePath(dataDir) {
561
+ return path7.join(weeklyDirPath(dataDir), "state.json");
566
562
  }
567
- function parseIso(v) {
568
- const t = Date.parse(v);
569
- return Number.isFinite(t) ? t : 0;
563
+ function reportFileName(endIso) {
564
+ return `report-${endIso.slice(0, 10)}.json`;
570
565
  }
571
- function refExists(guard, dataDir, ref) {
572
- const base = ref.split("#")[0] ?? "";
573
- if (!base) return false;
574
- const target = path8.join(dataDir, base);
566
+ function lastWeeklyGeneratedAt(guard, dataDir) {
575
567
  try {
576
- return fs7.existsSync(guard.assert(target));
568
+ const raw = JSON.parse(fs4.readFileSync(guard.assert(stateFilePath(dataDir)), "utf8"));
569
+ return Number(raw.lastGeneratedAt) || 0;
577
570
  } catch {
578
- return false;
571
+ return 0;
579
572
  }
580
573
  }
581
- function capForKinds(kinds) {
582
- if (kinds.length === 0) return 0.4;
583
- return Math.min(...kinds.map((k) => CONFIDENCE_CAP[k] ?? 0.4));
574
+ function weeklyDue(guard, dataDir, now = Date.now()) {
575
+ const last = lastWeeklyGeneratedAt(guard, dataDir);
576
+ return now - last >= WEEKLY_INTERVAL_MS;
584
577
  }
585
- function findActive(doc, id) {
586
- for (const p of PARTITIONS) {
587
- const hit = doc.partitions[p].entries.find((e) => e.id === id && e.validTo === null);
588
- if (hit) return hit;
589
- }
590
- return void 0;
578
+ function ensureWeeklyAnchor(guard, dataDir, now = Date.now()) {
579
+ if (lastWeeklyGeneratedAt(guard, dataDir) > 0) return false;
580
+ fs4.mkdirSync(weeklyDirPath(dataDir), { recursive: true });
581
+ atomicWriteJsonSync(stateFilePath(dataDir), { lastGeneratedAt: now });
582
+ return true;
591
583
  }
592
- function applyOpsToDoc(guard, dataDir, doc, ops, schema, policy, now) {
593
- const applied = [];
594
- const rejected = [];
595
- const nowIso = new Date(now).toISOString();
596
- for (const op of ops) {
597
- if (op.op === "NOOP") {
598
- applied.push(op);
599
- continue;
600
- }
601
- if (op.op === "ADD") {
602
- if (op.partition === "psy" && !policy.profile.psyEnabled) {
603
- rejected.push({ op, reason: "psy partition is disabled" });
604
- continue;
605
- }
606
- const check = checkAddAgainstSchema(schema, op.partition, op.topic, op.subTopic, op.temporal);
607
- if (!check.ok) {
608
- rejected.push({ op, reason: check.reason });
609
- continue;
610
- }
611
- if (!op.evidence || op.evidence.length === 0) {
612
- rejected.push({ op, reason: "ADD without evidence (no provenance, axiom 1)" });
613
- continue;
614
- }
615
- const badRef = op.evidence.find((e) => !refExists(guard, dataDir, e.ref));
616
- if (badRef) {
617
- rejected.push({ op, reason: `evidence ref does not resolve: ${badRef.ref}` });
618
- continue;
619
- }
620
- const cap = capForKinds(op.evidence.map((e) => e.kind));
621
- const active = doc.partitions[op.partition].entries.filter((e) => e.validTo === null);
622
- if (active.length >= policy.profile.partitionCap) {
623
- rejected.push({ op, reason: `partition ${op.partition} at cap (${policy.profile.partitionCap}); converge first` });
624
- continue;
625
- }
626
- dbSeq += 1;
627
- const entry = {
628
- id: `p${dbSeq.toString(36)}${randomUUID3().slice(0, 4)}`,
629
- partition: op.partition,
630
- topic: op.topic,
631
- subTopic: op.subTopic,
632
- content: op.content.trim(),
633
- confidence: Math.min(op.confidence ?? cap, cap),
634
- temporal: check.temporal,
635
- validFrom: nowIso,
636
- validTo: null,
637
- supersededBy: null,
638
- evidence: op.evidence,
639
- createdAt: nowIso,
640
- updatedAt: nowIso,
641
- updateCount: 0
642
- };
643
- op.assignedId = entry.id;
644
- doc.partitions[op.partition].entries.push(entry);
645
- applied.push(op);
646
- continue;
647
- }
648
- if (op.op === "UPDATE") {
649
- const entry = findActive(doc, op.id);
650
- if (!entry) {
651
- rejected.push({ op, reason: `unknown or inactive entry: ${op.id}` });
652
- continue;
653
- }
654
- if (op.changes.content !== void 0) entry.content = op.changes.content.trim();
655
- if (op.changes.confidence !== void 0) {
656
- const cap = capForKinds(entry.evidence.map((e) => e.kind));
657
- if (op.changes.confidence > entry.confidence && entry.evidence.length < 2) {
658
- rejected.push({ op, reason: "confidence upgrade requires a second confirming observation" });
659
- continue;
660
- }
661
- entry.confidence = Math.min(op.changes.confidence, cap);
662
- }
663
- entry.updatedAt = nowIso;
664
- entry.updateCount += 1;
665
- applied.push(op);
666
- continue;
667
- }
668
- if (op.op === "INVALIDATE") {
669
- const entry = findActive(doc, op.id);
670
- if (!entry) {
671
- rejected.push({ op, reason: `unknown or inactive entry: ${op.id}` });
672
- continue;
673
- }
674
- if (entry.temporal === "volatile") {
675
- rejected.push({ op, reason: "volatile expiry is code-owned (time-driven), not LLM-nominated" });
676
- continue;
677
- }
678
- const hasNewObservation = (op.evidence ?? []).length > 0 && (op.evidence ?? []).some((e) => parseIso(e.at) > parseIso(entry.evidence[entry.evidence.length - 1]?.at ?? ""));
679
- if (!hasNewObservation) {
680
- rejected.push({ op, reason: "stable INVALIDATE requires a newer contradicting observation" });
681
- continue;
682
- }
683
- entry.validTo = nowIso;
684
- entry.supersededBy = null;
685
- entry.updatedAt = nowIso;
686
- entry.updateCount += 1;
687
- if (op.evidence) entry.evidence.push(...op.evidence);
688
- applied.push(op);
689
- continue;
690
- }
691
- }
692
- return { applied, rejected };
584
+ function saveWeeklyReport(guard, dataDir, report) {
585
+ const dir = weeklyDirPath(dataDir);
586
+ fs4.mkdirSync(dir, { recursive: true });
587
+ const file = path7.join(dir, reportFileName(report.end));
588
+ saveEncryptedText(guard, file, JSON.stringify(report, null, 1));
589
+ atomicWriteJsonSync(stateFilePath(dataDir), { lastGeneratedAt: Date.parse(report.generatedAt) });
590
+ return file;
693
591
  }
694
- var dbSeq = 0;
695
- function runDeterministicAging(doc, policy, now) {
696
- const nowIso = new Date(now).toISOString();
697
- let volatileExpired = 0;
698
- let lowActivityMarked = 0;
699
- for (const p of PARTITIONS) {
700
- for (const e of doc.partitions[p].entries) {
701
- if (e.validTo !== null) continue;
702
- const lastEvidence = Math.max(...e.evidence.map((x) => parseIso(x.at)), parseIso(e.updatedAt));
703
- if (e.temporal === "volatile") {
704
- if (now - lastEvidence > policy.profile.volatileDays * DAY_MS2) {
705
- e.validTo = nowIso;
706
- e.updatedAt = nowIso;
707
- e.updateCount += 1;
708
- volatileExpired += 1;
709
- }
710
- } else if (!e.lowActivity && now - lastEvidence > policy.profile.stableLowActivityDays * DAY_MS2) {
711
- e.lowActivity = true;
712
- lowActivityMarked += 1;
713
- }
592
+ function listWeeklyReports(guard, dataDir) {
593
+ const out = [];
594
+ try {
595
+ for (const file of fs4.readdirSync(weeklyDirPath(dataDir))) {
596
+ if (!/^report-\d{4}-\d{2}-\d{2}\.json$/.test(file)) continue;
597
+ const rep = readWeeklyReport(guard, dataDir, file);
598
+ if (!rep) continue;
599
+ out.push({ file, start: rep.start, end: rep.end, generatedAt: rep.generatedAt, source: rep.source });
714
600
  }
601
+ } catch {
715
602
  }
716
- return { volatileExpired, lowActivityMarked };
603
+ return out.sort((a, b) => a.end < b.end ? 1 : -1);
717
604
  }
718
- function persistWithJournal(guard, dataDir, doc, record) {
719
- saveJson(guard, profileFilePath(dataDir), doc);
720
- const line = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...record });
721
- const journal = journalFilePath(dataDir);
605
+ function readWeeklyReport(guard, dataDir, file) {
606
+ if (!/^report-\d{4}-\d{2}-\d{2}\.json$/.test(file)) return null;
722
607
  try {
723
- fs7.appendFileSync(guard.assert(journal), line + "\n", "utf8");
608
+ const raw = loadEncryptedText(guard, path7.join(weeklyDirPath(dataDir), file));
609
+ if (!raw) return null;
610
+ const rep = JSON.parse(raw);
611
+ if (typeof rep.text !== "string") return null;
612
+ return rep;
724
613
  } catch {
725
- fs7.mkdirSync(dataDir, { recursive: true });
726
- fs7.appendFileSync(guard.assert(journal), line + "\n", "utf8");
614
+ return null;
727
615
  }
728
616
  }
729
- function applyOpPermissive(doc, op, ts) {
730
- if (op.op === "ADD") {
731
- dbSeq += 1;
732
- doc.partitions[op.partition].entries.push({
733
- id: op.assignedId ?? `r${dbSeq.toString(36)}${randomUUID3().slice(0, 4)}`,
734
- partition: op.partition,
735
- topic: op.topic,
736
- subTopic: op.subTopic,
737
- content: op.content,
738
- confidence: op.confidence ?? 0.5,
739
- temporal: op.temporal ?? "stable",
740
- validFrom: ts,
741
- validTo: null,
742
- supersededBy: null,
743
- evidence: op.evidence,
744
- createdAt: ts,
745
- updatedAt: ts,
746
- updateCount: 0
747
- });
748
- return;
617
+ function buildWeeklyPrompt(facts) {
618
+ return [
619
+ "\u4F60\u662F\u5FC3\u8DF3\u63D2\u4EF6\u7684\u540E\u53F0\u62A5\u544A\u5668\u3002\u6839\u636E\u4E0B\u9762\u7684\u672C\u5468\u4E8B\u5B9E\u6E05\u5355\uFF0C\u7528\u4E2D\u6587\u5199\u4E00\u4EFD\u5468\u62A5\u6B63\u6587\u3002",
620
+ "\u89C4\u5219\uFF1A",
621
+ "- \u53D9\u8FF0\u8005\u81EA\u79F0\u300C\u5FC3\u8DF3\u300D\u2014\u2014\u8FD9\u662F\u63D2\u4EF6\u540E\u53F0\uFF0C\u4E0D\u662F\u4EFB\u4F55\u4F1A\u8BDD\u91CC\u7684 agent\uFF1B\u7EDD\u4E0D\u80FD\u4EE5\u4F1A\u8BDD agent \u7684\u8EAB\u4EFD\u6216\u4EBA\u683C\u81EA\u79F0\u3002",
622
+ "- \u53EA\u62A5\u544A\u4E8B\u5B9E\u6E05\u5355\u91CC\u6709\u7684\u5185\u5BB9\uFF1B\u67D0\u4E00\u8282\u6CA1\u6709\u6570\u636E\u5C31\u6574\u8282\u8DF3\u8FC7\uFF0C\u4E0D\u8981\u7F16\u9020\u3001\u4E0D\u8981\u51D1\u6570\u3002",
623
+ "- \u8BED\u6C14\u5E73\u5B9E\u4E2D\u6027\uFF0C\u4E0D\u7528\u611F\u53F9\u53F7\uFF1B\u7BC7\u5E45\u4E0D\u8D85\u8FC7 250 \u5B57\u3002",
624
+ "- \u5F85\u529E\u79EF\u538B\u90A3\u4E00\u8282\u7528\u5546\u91CF\u7684\u8BED\u6C14\u63D0\u4E00\u53E5\uFF0C\u4E0D\u50AC\u4FC3\u3002",
625
+ "- \u76F4\u63A5\u8F93\u51FA\u5468\u62A5\u6B63\u6587\uFF0C\u4E0D\u8981\u8F93\u51FA\u89E3\u91CA\u3001\u6807\u9898\u6216 JSON\u3002",
626
+ "",
627
+ "\u672C\u5468\u4E8B\u5B9E\uFF08JSON\uFF09\uFF1A",
628
+ JSON.stringify(facts, null, 1)
629
+ ].join("\n");
630
+ }
631
+ function renderTemplateReport(facts) {
632
+ const d = (iso) => iso.slice(0, 10);
633
+ const lines = [`\u672C\u5468\uFF08${d(facts.windowStart)} ~ ${d(facts.windowEnd)}\uFF09`];
634
+ lines.push(`\u8868\u8FBE ${facts.spoken} \u6B21\uFF0C\u9759\u9ED8 ${facts.silent} \u6B21\u3002`);
635
+ if (facts.silentTopReasons.length > 0) {
636
+ lines.push(`\u9759\u9ED8\u4E3B\u56E0\uFF1A${facts.silentTopReasons.map((r) => `${r.reason}\uFF08${r.count}\uFF09`).join("\u3001")}\u3002`);
749
637
  }
750
- if (op.op === "UPDATE") {
751
- const e = [...PARTITIONS].flatMap((p) => doc.partitions[p].entries).find((x) => x.id === op.id);
752
- if (e) {
753
- if (op.changes.content !== void 0) e.content = op.changes.content;
754
- if (op.changes.confidence !== void 0) e.confidence = op.changes.confidence;
755
- e.updatedAt = ts;
756
- e.updateCount += 1;
757
- }
758
- return;
638
+ if (facts.profileAdds.length > 0) {
639
+ lines.push(`\u65B0\u8BA4\u8BC6 ${facts.profileAdds.length} \u6761\uFF1A${facts.profileAdds.map((a) => a.content).join("\uFF1B")}\u3002`);
759
640
  }
760
- if (op.op === "INVALIDATE") {
761
- const e = [...PARTITIONS].flatMap((p) => doc.partitions[p].entries).find((x) => x.id === op.id);
762
- if (e) {
763
- e.validTo = ts;
764
- e.updatedAt = ts;
765
- e.updateCount += 1;
766
- }
641
+ if (facts.ledger.added.length > 0 || facts.ledger.done.length > 0) {
642
+ const parts = [];
643
+ if (facts.ledger.added.length > 0) parts.push(`\u65B0\u589E\u5F85\u529E\uFF1A${facts.ledger.added.join("\u3001")}`);
644
+ if (facts.ledger.done.length > 0) parts.push(`\u5DF2\u89E3\u51B3\uFF1A${facts.ledger.done.join("\u3001")}`);
645
+ lines.push(`${parts.join("\uFF1B")}\u3002`);
767
646
  }
768
- }
769
- function replayJournal(guard, dataDir) {
770
- const journal = journalFilePath(dataDir);
771
- const raw = readText(guard, journal, "");
772
- const doc = emptyProfile();
773
- let records = 0;
774
- let truncatedTail = 0;
775
- const lines = raw.split("\n");
776
- for (let i = 0; i < lines.length; i++) {
777
- const trimmed = lines[i].trim();
778
- if (!trimmed) continue;
779
- try {
780
- const rec = JSON.parse(trimmed);
781
- for (const op of rec.applied ?? []) applyOpPermissive(doc, op, rec.ts);
782
- records += 1;
783
- } catch {
784
- const isLast = lines.slice(i + 1).every((l) => !l.trim());
785
- if (isLast) {
786
- truncatedTail = lines.length - i;
787
- break;
788
- }
789
- }
647
+ if (facts.ledger.stale.length > 0) {
648
+ lines.push(`\u6302\u4E86\u5F88\u4E45\uFF1A${facts.ledger.stale.map((s) => `${s.text}\uFF08${s.days} \u5929\uFF09`).join("\u3001")}\u2014\u2014\u8981\u5220\u6389\u8FD8\u662F\u7EE7\u7EED\u6302\u7740\uFF1F`);
790
649
  }
791
- return { doc, truncatedTail, records };
792
- }
793
- function verifyProfile(guard, dataDir) {
794
- const replayed = replayJournal(guard, dataDir);
795
- const onDisk = loadProfile(guard, profileFilePath(dataDir));
796
- const strip = (doc) => JSON.stringify(doc.partitions, (k, v) => ["id", "supersededBy", "validFrom", "createdAt", "updatedAt", "retiredAt"].includes(k) ? "<norm>" : v);
797
- const ok = strip(replayed.doc) === strip(onDisk);
798
- if (ok) return { ok: true, truncatedTail: replayed.truncatedTail, records: replayed.records };
799
- const diskIds = new Set([...PARTITIONS].flatMap((p) => onDisk.partitions[p].entries.map((e) => e.content)));
800
- const replayIds = new Set([...PARTITIONS].flatMap((p) => replayed.doc.partitions[p].entries.map((e) => e.content)));
801
- const onlyDisk = [...diskIds].find((c) => !replayIds.has(c));
802
- const onlyReplay = [...replayIds].find((c) => !diskIds.has(c));
803
- return {
804
- ok: false,
805
- firstDivergence: {
806
- id: onlyDisk ?? onlyReplay ?? "(content)",
807
- expected: onlyReplay ? "absent in journal replay" : "present in journal replay",
808
- actual: onlyDisk ? "present on disk" : "absent on disk"
809
- },
810
- truncatedTail: replayed.truncatedTail,
811
- records: replayed.records
812
- };
813
- }
814
- function rebuildProfile(guard, dataDir, opts = {}) {
815
- const replayed = replayJournal(guard, dataDir);
816
- const target = profileFilePath(dataDir);
817
- if (opts.check) {
818
- const onDisk = loadProfile(guard, target);
819
- const same = JSON.stringify(onDisk) === JSON.stringify(replayed.doc);
820
- return {
821
- ok: same,
822
- truncatedTail: replayed.truncatedTail,
823
- records: replayed.records,
824
- wrote: false,
825
- diffSummary: same ? "no diff" : "materialized view differs from journal replay"
826
- };
650
+ if (facts.seeds.added.length > 0) {
651
+ lines.push(`\u7D20\u6750\u6C60\u65B0\u589E ${facts.seeds.added.length} \u6761\uFF0C\u5DF2\u6D88\u8D39 ${facts.seeds.consumed.length} \u6761\u3002`);
827
652
  }
828
- saveJson(guard, target, replayed.doc);
829
- if (replayed.truncatedTail > 0) {
830
- writeText(
831
- guard,
832
- path8.join(dataDir, "logs", "rebuild-report.txt"),
833
- `rebuild truncated ${replayed.truncatedTail} torn line(s) at journal tail; ${replayed.records} records applied
834
- `
835
- );
653
+ if (facts.seeds.waiting.length > 0) {
654
+ lines.push(`\u8FD8\u6CA1\u804A\u8FC7\u7684\u7D20\u6750\uFF1A${facts.seeds.waiting.join("\u3001")}\u3002`);
836
655
  }
837
- return { ok: true, truncatedTail: replayed.truncatedTail, records: replayed.records, wrote: true };
838
- }
839
-
840
- // src/notify/notify.ts
841
- import { spawnSync } from "child_process";
842
- import path9 from "path";
843
- function runNotify(paths, args) {
844
- const r = spawnSync(
845
- "powershell.exe",
846
- ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", path9.join(paths.assetsDir, "notify.ps1"), ...args],
847
- { timeout: 2e4, encoding: "utf8" }
848
- );
849
- return { status: r.status ?? -1, out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
850
- }
851
- function ensureRegistered(paths) {
852
- const check = runNotify(paths, ["-Check"]);
853
- if (/REGISTERED: yes/.test(check.out)) return true;
854
- const reg = runNotify(paths, ["-RegisterOnly"]);
855
- return reg.status === 0;
856
- }
857
- function sendNewMessageHint(paths) {
858
- const r = runNotify(paths, ["-Title", "Heartbeat", "-Message", "\u6709\u65B0\u6D88\u606F"]);
859
- return r.status === 0 && /TOAST_SENT/.test(r.out);
656
+ if (facts.reports.total > 0) {
657
+ const reasonPart = facts.reports.reasons.length > 0 ? `\uFF08${facts.reports.reasons.map((r) => `${r.reason} ${r.count} \u6B21`).join("\u3001")}\uFF09` : "";
658
+ lines.push(`\u6295\u9012\u62A5\u8D26 ${facts.reports.total} \u6B21\uFF1A\u7528\u7D20\u6750 ${facts.reports.material}\u3001\u8BF4\u771F\u5FC3\u8BDD ${facts.reports.heartfelt}\u3001\u6CA1\u8BF4\u8BDD ${facts.reports.silent}${reasonPart}\u3002`);
659
+ }
660
+ if (facts.reports.missing > 0) {
661
+ lines.push(`\u53E6\u6709 ${facts.reports.missing} \u6B21\u6295\u9012\u6CA1\u7B49\u5230\u62A5\u8D26\uFF08\u8FD9\u90E8\u5206\u6CA1\u7B97\u8FDB\u504F\u597D\u7EDF\u8BA1\uFF09\u3002`);
662
+ }
663
+ if (facts.peakHours.length > 0) {
664
+ lines.push(`\u6D3B\u8DC3\u9AD8\u5CF0\uFF1A${facts.peakHours.join("\u3001")}\u3002`);
665
+ }
666
+ return lines.join("\n");
860
667
  }
861
668
 
862
669
  // src/core/preset-install.ts
863
- import fs8 from "fs";
670
+ import fs5 from "fs";
864
671
  import os from "os";
865
- import path10 from "path";
672
+ import path8 from "path";
866
673
  import { fileURLToPath } from "url";
867
674
  var COMPOSITION_FILE = "agent.cordis.yml";
868
675
  var METADATA_FILE = "preset.yml";
@@ -870,14 +677,14 @@ var BUNDLED_PRESET_ID = "heartbeat";
870
677
  function bundledPresetDir(moduleUrl, id = BUNDLED_PRESET_ID) {
871
678
  let dir;
872
679
  try {
873
- dir = path10.dirname(fileURLToPath(moduleUrl));
680
+ dir = path8.dirname(fileURLToPath(moduleUrl));
874
681
  } catch {
875
682
  return void 0;
876
683
  }
877
684
  for (let depth = 0; depth < 5; depth += 1) {
878
- const candidate = path10.join(dir, "assets", "presets", id);
879
- if (fs8.existsSync(path10.join(candidate, COMPOSITION_FILE))) return candidate;
880
- const parent = path10.dirname(dir);
685
+ const candidate = path8.join(dir, "assets", "presets", id);
686
+ if (fs5.existsSync(path8.join(candidate, COMPOSITION_FILE))) return candidate;
687
+ const parent = path8.dirname(dir);
881
688
  if (parent === dir) break;
882
689
  dir = parent;
883
690
  }
@@ -887,12 +694,12 @@ function userPresetRoot(roots) {
887
694
  const found = roots?.find(
888
695
  (root) => root?.trust === "user" && typeof root.path === "string" && root.path.length > 0
889
696
  );
890
- return found?.path === void 0 ? void 0 : path10.resolve(found.path);
697
+ return found?.path === void 0 ? void 0 : path8.resolve(found.path);
891
698
  }
892
699
  function conventionalUserPresetRoot(env = process.env, home = os.homedir()) {
893
700
  const override = env.DSH_HOME?.trim();
894
- const root = override && override.length > 0 ? override : path10.join(home, ".dsh");
895
- return path10.join(root, ".agent-presets");
701
+ const root = override && override.length > 0 ? override : path8.join(home, ".dsh");
702
+ return path8.join(root, ".agent-presets");
896
703
  }
897
704
  function installBundledPreset(options) {
898
705
  const id = options.id && options.id.length > 0 ? options.id : BUNDLED_PRESET_ID;
@@ -924,12 +731,12 @@ function installBundledPreset(options) {
924
731
  detail: "the roster mounts no user preset root (includeUserRoot=false)"
925
732
  };
926
733
  }
927
- const dir = path10.join(root, id);
928
- const composition = path10.join(dir, COMPOSITION_FILE);
734
+ const dir = path8.join(root, id);
735
+ const composition = path8.join(dir, COMPOSITION_FILE);
929
736
  try {
930
- if (fs8.existsSync(composition)) {
737
+ if (fs5.existsSync(composition)) {
931
738
  if (options.force !== true) {
932
- const drifted = !sameBytes(composition, path10.join(bundledDir, COMPOSITION_FILE));
739
+ const drifted = !sameBytes(composition, path8.join(bundledDir, COMPOSITION_FILE));
933
740
  return {
934
741
  action: "exists",
935
742
  id,
@@ -938,7 +745,7 @@ function installBundledPreset(options) {
938
745
  detail: drifted ? "kept as-is (differs from the bundled template)" : "kept as-is"
939
746
  };
940
747
  }
941
- fs8.copyFileSync(path10.join(bundledDir, COMPOSITION_FILE), composition);
748
+ fs5.copyFileSync(path8.join(bundledDir, COMPOSITION_FILE), composition);
942
749
  return {
943
750
  action: "restored",
944
751
  id,
@@ -947,11 +754,11 @@ function installBundledPreset(options) {
947
754
  detail: "composition replaced from the bundled template"
948
755
  };
949
756
  }
950
- const existed = fs8.existsSync(dir);
951
- fs8.mkdirSync(dir, { recursive: true });
952
- fs8.copyFileSync(path10.join(bundledDir, COMPOSITION_FILE), composition);
953
- const metadata = path10.join(dir, METADATA_FILE);
954
- if (!fs8.existsSync(metadata)) fs8.copyFileSync(path10.join(bundledDir, METADATA_FILE), metadata);
757
+ const existed = fs5.existsSync(dir);
758
+ fs5.mkdirSync(dir, { recursive: true });
759
+ fs5.copyFileSync(path8.join(bundledDir, COMPOSITION_FILE), composition);
760
+ const metadata = path8.join(dir, METADATA_FILE);
761
+ if (!fs5.existsSync(metadata)) fs5.copyFileSync(path8.join(bundledDir, METADATA_FILE), metadata);
955
762
  return {
956
763
  action: existed ? "repaired" : "created",
957
764
  id,
@@ -969,21 +776,21 @@ function describeInstall(result) {
969
776
  return `preset ${result.id} ${result.action}${where}${why}`;
970
777
  }
971
778
  function presetStatus(moduleUrl, id = BUNDLED_PRESET_ID, root = conventionalUserPresetRoot()) {
972
- const dir = path10.join(root, id);
779
+ const dir = path8.join(root, id);
973
780
  const bundledDir = bundledPresetDir(moduleUrl, id);
974
- const installed = fs8.existsSync(path10.join(dir, COMPOSITION_FILE));
781
+ const installed = fs5.existsSync(path8.join(dir, COMPOSITION_FILE));
975
782
  return {
976
783
  id,
977
784
  dir,
978
785
  ...bundledDir === void 0 ? {} : { bundledDir },
979
786
  installed,
980
- compositionMatches: installed && bundledDir !== void 0 && sameBytes(path10.join(dir, COMPOSITION_FILE), path10.join(bundledDir, COMPOSITION_FILE)),
981
- metadataMatches: bundledDir !== void 0 && fs8.existsSync(path10.join(dir, METADATA_FILE)) && sameBytes(path10.join(dir, METADATA_FILE), path10.join(bundledDir, METADATA_FILE))
787
+ compositionMatches: installed && bundledDir !== void 0 && sameBytes(path8.join(dir, COMPOSITION_FILE), path8.join(bundledDir, COMPOSITION_FILE)),
788
+ metadataMatches: bundledDir !== void 0 && fs5.existsSync(path8.join(dir, METADATA_FILE)) && sameBytes(path8.join(dir, METADATA_FILE), path8.join(bundledDir, METADATA_FILE))
982
789
  };
983
790
  }
984
791
  function sameBytes(left, right) {
985
792
  try {
986
- return fs8.readFileSync(left).equals(fs8.readFileSync(right));
793
+ return fs5.readFileSync(left).equals(fs5.readFileSync(right));
987
794
  } catch {
988
795
  return false;
989
796
  }
@@ -991,10 +798,6 @@ function sameBytes(left, right) {
991
798
 
992
799
  export {
993
800
  createPathGuard,
994
- atomicWriteJsonSync,
995
- shredFileSync,
996
- appendAuditLine,
997
- pruneAuditFile,
998
801
  deepMerge,
999
802
  loadPolicy,
1000
803
  updateUserPolicy,
@@ -1004,6 +807,8 @@ export {
1004
807
  markDone,
1005
808
  scanPending,
1006
809
  pendingOlderThan,
810
+ preferenceFilePath,
811
+ recordDelivery,
1007
812
  loadInterests,
1008
813
  loadWatchlist,
1009
814
  checkWatchlist,
@@ -1011,16 +816,17 @@ export {
1011
816
  completeWander,
1012
817
  adviseRefillWander,
1013
818
  browseStatus,
1014
- loadProfileSchema,
1015
- profileFilePath,
1016
- loadProfile,
1017
- applyOpsToDoc,
1018
- runDeterministicAging,
1019
- persistWithJournal,
1020
- verifyProfile,
1021
- rebuildProfile,
1022
819
  ensureRegistered,
1023
820
  sendNewMessageHint,
821
+ sendWeeklyReadyHint,
822
+ weeklyDirPath,
823
+ weeklyDue,
824
+ ensureWeeklyAnchor,
825
+ saveWeeklyReport,
826
+ listWeeklyReports,
827
+ readWeeklyReport,
828
+ buildWeeklyPrompt,
829
+ renderTemplateReport,
1024
830
  BUNDLED_PRESET_ID,
1025
831
  userPresetRoot,
1026
832
  conventionalUserPresetRoot,
@@ -1028,4 +834,4 @@ export {
1028
834
  describeInstall,
1029
835
  presetStatus
1030
836
  };
1031
- //# sourceMappingURL=chunk-ON4MSU6E.js.map
837
+ //# sourceMappingURL=chunk-2SYXKKZF.js.map