@kevin0181/memoc 1.1.3 → 1.1.10

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 (3) hide show
  1. package/README.md +50 -4
  2. package/bin/cli.js +1351 -60
  3. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -172,6 +172,7 @@ function scanProject(dir, depth = 0) {
172
172
  // ═══════════════════════════════════════════════════════════════════
173
173
 
174
174
  function nowISO() { return new Date().toISOString().slice(0, 19); }
175
+ function todayISO() { return new Date().toISOString().slice(0, 10); }
175
176
 
176
177
  function stackStr(stack) { return stack.length ? stack.join(', ') : 'Not detected'; }
177
178
 
@@ -208,16 +209,145 @@ function write(filePath, content) {
208
209
  fs.writeFileSync(filePath, content, 'utf8');
209
210
  }
210
211
 
211
- function tplMemocCmdWrapper(cliPath = runtimeCliPath()) {
212
- return `@echo off\r\nnode "${escapeCmdPath(cliPath)}" %*\r\n`;
212
+ function slugify(value, fallback = 'note') {
213
+ const slug = String(value || '')
214
+ .toLowerCase()
215
+ .replace(/['"]/g, '')
216
+ .replace(/[^a-z0-9]+/g, '-')
217
+ .replace(/^-+|-+$/g, '')
218
+ .slice(0, 80);
219
+ return slug || fallback;
213
220
  }
214
221
 
215
- function tplMemocPs1Wrapper(cliPath = runtimeCliPath()) {
216
- return `& node ${psSingleQuote(cliPath)} @args\nexit $LASTEXITCODE\n`;
222
+ function uniquePath(filePath) {
223
+ if (!fs.existsSync(filePath)) return filePath;
224
+ const ext = path.extname(filePath);
225
+ const base = filePath.slice(0, filePath.length - ext.length);
226
+ let i = 2;
227
+ while (fs.existsSync(`${base}-${i}${ext}`)) i += 1;
228
+ return `${base}-${i}${ext}`;
217
229
  }
218
230
 
219
- function tplMemocShWrapper(cliPath = runtimeCliPath()) {
220
- return `#!/bin/sh\nexec node ${shellSingleQuote(cliPath)} "$@"\n`;
231
+ function markdownTitle(src, fallback) {
232
+ const m = String(src || '').match(/^#\s+(.+)$/m);
233
+ return m ? m[1].trim() : fallback;
234
+ }
235
+
236
+ function execGitConfig(dir, key) {
237
+ try {
238
+ return require('child_process')
239
+ .execFileSync('git', ['config', '--get', key], { cwd: dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
240
+ .trim();
241
+ } catch {
242
+ return '';
243
+ }
244
+ }
245
+
246
+ function actorFile(dir) {
247
+ return path.join(dir, '.memoc', 'local', 'actor');
248
+ }
249
+
250
+ function sanitizeActor(value) {
251
+ return slugify(String(value || '').replace(/@.*$/, ''), 'unknown');
252
+ }
253
+
254
+ function detectActor(dir) {
255
+ if (process.env.MEMOC_ACTOR) return { actor: sanitizeActor(process.env.MEMOC_ACTOR), source: 'MEMOC_ACTOR' };
256
+ const localPath = actorFile(dir);
257
+ try {
258
+ const local = fs.readFileSync(localPath, 'utf8').trim();
259
+ if (local) return { actor: sanitizeActor(local), source: '.memoc/local/actor' };
260
+ } catch {}
261
+ const gitUser = execGitConfig(dir, 'user.name');
262
+ if (gitUser) return { actor: sanitizeActor(gitUser), source: 'git config user.name' };
263
+ const gitEmail = execGitConfig(dir, 'user.email');
264
+ if (gitEmail) return { actor: sanitizeActor(gitEmail), source: 'git config user.email' };
265
+ const osUser = process.env.USER || process.env.USERNAME || process.env.LOGNAME;
266
+ if (osUser) return { actor: sanitizeActor(osUser), source: 'OS user' };
267
+ return { actor: 'unknown', source: 'fallback' };
268
+ }
269
+
270
+ function gitBranch(dir) {
271
+ try {
272
+ return require('child_process')
273
+ .execFileSync('git', ['branch', '--show-current'], { cwd: dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
274
+ .trim() || 'unknown';
275
+ } catch {
276
+ return 'unknown';
277
+ }
278
+ }
279
+
280
+ function gitStatusFiles(dir) {
281
+ try {
282
+ const out = require('child_process')
283
+ .execFileSync('git', ['status', '--short'], { cwd: dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
284
+ .trim();
285
+ if (!out) return [];
286
+ return out.split(/\r?\n/)
287
+ .map(line => line.slice(3).trim())
288
+ .filter(Boolean)
289
+ .filter(file => !file.startsWith('.memoc/worklog/') && !file.startsWith('.memoc/activity.md'))
290
+ .slice(0, 12);
291
+ } catch {
292
+ return [];
293
+ }
294
+ }
295
+
296
+ function tplMemocCmdWrapper() {
297
+ return [
298
+ '@echo off',
299
+ 'set "MEMOC_RUNTIME=%MEMOC_RUNTIME_DIR%"',
300
+ 'if "%MEMOC_RUNTIME%"=="" (',
301
+ ' if not "%LOCALAPPDATA%"=="" (',
302
+ ' set "MEMOC_RUNTIME=%LOCALAPPDATA%\\memoc\\runtime"',
303
+ ' ) else (',
304
+ ' set "MEMOC_RUNTIME=%USERPROFILE%\\AppData\\Local\\memoc\\runtime"',
305
+ ' )',
306
+ ')',
307
+ 'set "MEMOC_CLI=%MEMOC_RUNTIME%\\bin\\cli.js"',
308
+ 'if exist "%MEMOC_CLI%" (',
309
+ ' node "%MEMOC_CLI%" %*',
310
+ ') else (',
311
+ ' npx @kevin0181/memoc@latest %*',
312
+ ')',
313
+ 'exit /b %ERRORLEVEL%',
314
+ '',
315
+ ].join('\r\n');
316
+ }
317
+
318
+ function tplMemocPs1Wrapper() {
319
+ return [
320
+ '$runtime = $env:MEMOC_RUNTIME_DIR',
321
+ 'if (-not $runtime) {',
322
+ ' if ($env:LOCALAPPDATA) { $runtime = Join-Path $env:LOCALAPPDATA "memoc\\runtime" }',
323
+ ' else { $runtime = Join-Path $env:USERPROFILE "AppData\\Local\\memoc\\runtime" }',
324
+ '}',
325
+ '$cli = Join-Path $runtime "bin\\cli.js"',
326
+ 'if (Test-Path $cli) {',
327
+ ' & node $cli @args',
328
+ '} else {',
329
+ ' & npx @kevin0181/memoc@latest @args',
330
+ '}',
331
+ 'exit $LASTEXITCODE',
332
+ '',
333
+ ].join('\n');
334
+ }
335
+
336
+ function tplMemocShWrapper() {
337
+ return [
338
+ '#!/bin/sh',
339
+ 'if [ -n "$MEMOC_RUNTIME_DIR" ]; then',
340
+ ' memoc_runtime="$MEMOC_RUNTIME_DIR"',
341
+ 'else',
342
+ ' memoc_runtime="${HOME:-$PWD}/.local/share/memoc/runtime"',
343
+ 'fi',
344
+ 'memoc_cli="$memoc_runtime/bin/cli.js"',
345
+ 'if [ -f "$memoc_cli" ]; then',
346
+ ' exec node "$memoc_cli" "$@"',
347
+ 'fi',
348
+ 'exec npx @kevin0181/memoc@latest "$@"',
349
+ '',
350
+ ].join('\n');
221
351
  }
222
352
 
223
353
  function defaultUserBinDir() {
@@ -249,11 +379,11 @@ function tplEnvSh() {
249
379
  }
250
380
 
251
381
  function ensurePathHelpers(dir, mark) {
252
- const cliPath = ensureRuntimeInstall(mark);
382
+ ensureRuntimeInstall(mark);
253
383
  const files = [
254
- [path.join(dir, '.memoc', 'bin', 'memoc.cmd'), () => tplMemocCmdWrapper(cliPath), false],
255
- [path.join(dir, '.memoc', 'bin', 'memoc.ps1'), () => tplMemocPs1Wrapper(cliPath), false],
256
- [path.join(dir, '.memoc', 'bin', 'memoc'), () => tplMemocShWrapper(cliPath), true],
384
+ [path.join(dir, '.memoc', 'bin', 'memoc.cmd'), tplMemocCmdWrapper, false],
385
+ [path.join(dir, '.memoc', 'bin', 'memoc.ps1'), tplMemocPs1Wrapper, false],
386
+ [path.join(dir, '.memoc', 'bin', 'memoc'), tplMemocShWrapper, true],
257
387
  [path.join(dir, '.memoc', 'env.ps1'), tplEnvPs1, false],
258
388
  [path.join(dir, '.memoc', 'env.sh'), tplEnvSh, true],
259
389
  ];
@@ -268,15 +398,16 @@ function ensurePathHelpers(dir, mark) {
268
398
 
269
399
  function ensureUserLauncher(mark) {
270
400
  const userBin = defaultUserBinDir();
271
- writeLaunchers(userBin, mark, 'user bin', ensureRuntimeInstall(mark));
401
+ ensureRuntimeInstall(mark);
402
+ writeLaunchers(userBin, mark, 'user bin');
272
403
  return userBin;
273
404
  }
274
405
 
275
- function writeLaunchers(binDir, mark, label, cliPath = ensureRuntimeInstall(mark)) {
406
+ function writeLaunchers(binDir, mark, label) {
276
407
  const files = [
277
- [path.join(binDir, 'memoc.cmd'), () => tplMemocCmdWrapper(cliPath), false],
278
- [path.join(binDir, 'memoc.ps1'), () => tplMemocPs1Wrapper(cliPath), false],
279
- [path.join(binDir, 'memoc'), () => tplMemocShWrapper(cliPath), true],
408
+ [path.join(binDir, 'memoc.cmd'), tplMemocCmdWrapper, false],
409
+ [path.join(binDir, 'memoc.ps1'), tplMemocPs1Wrapper, false],
410
+ [path.join(binDir, 'memoc'), tplMemocShWrapper, true],
280
411
  ];
281
412
 
282
413
  for (const [fp, tpl, executable] of files) {
@@ -373,7 +504,8 @@ function ensureCurrentPathLauncher(mark) {
373
504
  mark('skip', 'active PATH launcher (no writable PATH directory found)');
374
505
  return false;
375
506
  }
376
- writeLaunchers(target, mark, 'active PATH', ensureRuntimeInstall(mark));
507
+ ensureRuntimeInstall(mark);
508
+ writeLaunchers(target, mark, 'active PATH');
377
509
  return true;
378
510
  }
379
511
 
@@ -604,6 +736,9 @@ function managedBlock() {
604
736
  - [ ] Search memory first: \`memoc search "<query>" --limit 5\`, or wrapper fallback above if PATH fails
605
737
  - [ ] Open on demand: \`02\` status, \`04\` resume, \`06\` rules, \`llms.txt\` map
606
738
  - [ ] If memory search is not enough, search project files with \`memoc grep "<query>" --limit 5\` (or wrapper fallback)
739
+ - [ ] If asked to refresh/update memoc project memory, run \`memoc update\` first; this refreshes managed sections, wiki links, and Obsidian tags.
740
+ - [ ] For durable source material use \`memoc ingest <path-or-url>\`; for durable analysis/query results use \`memoc note "<title>"\`; after wiki edits run \`memoc lint-wiki\`.
741
+ - [ ] In shared repos, record meaningful work with \`memoc work "<title>"\`; actor defaults to \`MEMOC_ACTOR\`, local actor, git user, git email, or OS user.
607
742
  - [ ] Keep output small: \`summary\`, \`search --limit\`, \`grep --limit\`, \`--snippets\`
608
743
 
609
744
  ## Before Finishing _(update only applicable files; skip Q&A / throwaway exploration)_
@@ -612,6 +747,9 @@ function managedBlock() {
612
747
  - [ ] Work incomplete or risky? Update \`04-handoff.md\`
613
748
  - [ ] Rule/preference set? Update \`06-project-rules.md\`
614
749
  - [ ] Wiki/systems work? Read \`skills/project-memory-maintainer/SKILL.md\`
750
+ - [ ] User asked to update memoc/project memory? Run \`memoc update\`, then update the smallest relevant agent-owned memory files.
751
+ - [ ] Shared repo work? Prefer \`memoc work "<title>" --from-git\` over appending shared files; run \`memoc activity --write\` only when regenerating indexes.
752
+ - [ ] Keep \`session-summary.md\` as a replace-only snapshot under 800B; move completed work to actor worklogs and resume risks to \`04-handoff.md\`. If it grew, run \`memoc trim-summary\`.
615
753
  ${MGMT_E}`;
616
754
  }
617
755
 
@@ -646,8 +784,9 @@ function coreLlmsInner() {
646
784
  - [Project Brief](.memoc/00-project-brief.md): short identity and direction.
647
785
  - [Workflow](.memoc/01-agent-workflow.md): update trigger matrix.
648
786
  - [Decisions](.memoc/03-decisions.md): durable decisions.
649
- - [Log](.memoc/log.md): append-only history.
650
787
  - [Systems](.memoc/systems/README.md): subsystem docs.
788
+ - [Activity](.memoc/activity.md): generated worklog index.
789
+ - [Raw Sources](.memoc/raw/README.md): immutable source material; do not read by default.
651
790
  - [Wiki](.memoc/wiki/index.md): synthesized knowledge.`;
652
791
  }
653
792
 
@@ -741,6 +880,217 @@ function ensureWikiScaffoldLinks(memDir, mark) {
741
880
  }
742
881
  }
743
882
 
883
+ function ensureObsidianFrontmatter(dir, mark) {
884
+ const files = collectMemocMarkdownFiles(dir);
885
+ let changed = 0;
886
+ for (const fp of files) {
887
+ if (ensureMemocFrontmatter(fp, dir)) changed += 1;
888
+ }
889
+ mark(changed ? 'update' : 'skip', `Obsidian tags (${changed || 'already present'})`);
890
+ }
891
+
892
+ function collectMemocMarkdownFiles(dir) {
893
+ const files = [];
894
+ function walk(root) {
895
+ if (!fs.existsSync(root)) return;
896
+ try {
897
+ const st = fs.statSync(root);
898
+ if (st.isFile()) {
899
+ if (root.endsWith('.md')) files.push(root);
900
+ return;
901
+ }
902
+ if (!st.isDirectory()) return;
903
+ for (const entry of fs.readdirSync(root)) walk(path.join(root, entry));
904
+ } catch {}
905
+ }
906
+ walk(path.join(dir, '.memoc'));
907
+ walk(path.join(dir, 'skills', 'project-memory-maintainer'));
908
+ return files.sort();
909
+ }
910
+
911
+ function ensureMemocFrontmatter(filePath, dir) {
912
+ let src = '';
913
+ try { src = fs.readFileSync(filePath, 'utf8'); } catch { return false; }
914
+ const spec = obsidianFrontmatterSpec(path.relative(dir, filePath));
915
+ const next = mergeYamlFrontmatter(src, spec);
916
+ if (next === src) return false;
917
+ write(filePath, next);
918
+ return true;
919
+ }
920
+
921
+ function obsidianFrontmatterSpec(relPath) {
922
+ const rel = relPath.replace(/\\/g, '/');
923
+ let type = 'core';
924
+ const tags = ['memoc'];
925
+ const now = nowISO();
926
+ const extra = {
927
+ created: now,
928
+ updated: now,
929
+ status: 'active',
930
+ };
931
+
932
+ if (rel.startsWith('.memoc/wiki/')) {
933
+ type = 'wiki';
934
+ extra.confidence = 'medium';
935
+ tags.push('memoc/wiki');
936
+ if (rel.startsWith('.memoc/wiki/sources/')) {
937
+ tags.push('memoc/source');
938
+ extra.status = 'needs-synthesis';
939
+ } else if (rel.startsWith('.memoc/wiki/topics/')) {
940
+ tags.push('memoc/topic');
941
+ } else if (rel.startsWith('.memoc/wiki/global/')) {
942
+ tags.push('memoc/global');
943
+ } else if (rel.endsWith('/sources.md')) {
944
+ tags.push('memoc/source');
945
+ } else if (rel.endsWith('/glossary.md')) {
946
+ tags.push('memoc/glossary');
947
+ } else if (rel.endsWith('/questions.md')) {
948
+ tags.push('memoc/question');
949
+ extra.status = 'needs-review';
950
+ } else if (rel.endsWith('/lint.md')) {
951
+ tags.push('memoc/lint');
952
+ extra.status = 'generated';
953
+ }
954
+ } else if (rel.startsWith('.memoc/systems/')) {
955
+ type = 'system';
956
+ tags.push('memoc/system');
957
+ } else if (rel.startsWith('.memoc/raw/')) {
958
+ type = 'raw';
959
+ tags.push('memoc/raw');
960
+ } else if (rel.startsWith('.memoc/worklog/')) {
961
+ type = 'worklog';
962
+ tags.push('memoc/worklog');
963
+ } else if (rel.startsWith('.memoc/actors/')) {
964
+ type = 'actor';
965
+ tags.push('memoc/actor');
966
+ } else if (rel.startsWith('skills/project-memory-maintainer/')) {
967
+ type = 'skill';
968
+ tags.push('memoc/skill');
969
+ } else if (/(session-summary|current-project-state|handoff|project-rules|decisions|log)\.md$/.test(rel)) {
970
+ type = 'state';
971
+ tags.push('memoc/state');
972
+ } else {
973
+ tags.push('memoc/core');
974
+ }
975
+
976
+ return { type, tags, extra };
977
+ }
978
+
979
+ function mergeYamlFrontmatter(src, spec) {
980
+ const fm = parseYamlFrontmatter(src);
981
+ if (!fm) {
982
+ return `${formatMemocFrontmatter(spec)}\n${String(src || '').replace(/^\uFEFF/, '')}`;
983
+ }
984
+
985
+ let body = fm.body;
986
+ let rest = String(src || '').slice(fm.end).replace(/^\uFEFF/, '');
987
+ const nested = parseYamlFrontmatter(rest);
988
+ if (nested) {
989
+ body = `${nested.body}\n${body}`;
990
+ rest = rest.slice(nested.end).replace(/^\uFEFF/, '');
991
+ }
992
+
993
+ const lines = body.split(/\r?\n/);
994
+ const existingTags = readYamlTags(lines);
995
+ const mergedTags = [...new Set([...existingTags, ...spec.tags])];
996
+ const nextLines = mergeYamlScalar(lines, 'memoc', 'true');
997
+ mergeYamlScalar(nextLines, 'type', spec.type);
998
+ mergeYamlScalar(nextLines, 'scope', 'project-memory');
999
+ mergeYamlScalarIfMissing(nextLines, 'updated', spec.extra.updated);
1000
+ mergeYamlScalarIfMissing(nextLines, 'created', spec.extra.created);
1001
+ mergeYamlScalarIfMissing(nextLines, 'status', spec.extra.status);
1002
+ if (spec.extra.confidence) mergeYamlScalarIfMissing(nextLines, 'confidence', spec.extra.confidence);
1003
+ mergeYamlTags(nextLines, mergedTags);
1004
+
1005
+ const nextFm = ['---', ...nextLines, '---'].join('\n');
1006
+ return `${nextFm}\n${rest.replace(/^\r?\n/, '')}`;
1007
+ }
1008
+
1009
+ function parseYamlFrontmatter(src) {
1010
+ const text = String(src || '').replace(/^\uFEFF/, '');
1011
+ const offset = text.length === src.length ? 0 : 1;
1012
+ if (!text.startsWith('---\n') && !text.startsWith('---\r\n')) return null;
1013
+ const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
1014
+ if (!m) return null;
1015
+ return { body: m[1], end: m[0].length + offset };
1016
+ }
1017
+
1018
+ function formatMemocFrontmatter(spec) {
1019
+ return [
1020
+ '---',
1021
+ 'memoc: true',
1022
+ `type: ${spec.type}`,
1023
+ 'scope: project-memory',
1024
+ `created: ${spec.extra.created}`,
1025
+ `updated: ${spec.extra.updated}`,
1026
+ `status: ${spec.extra.status}`,
1027
+ ...(spec.extra.confidence ? [`confidence: ${spec.extra.confidence}`] : []),
1028
+ 'tags:',
1029
+ ...spec.tags.map(tag => ` - ${tag}`),
1030
+ '---',
1031
+ ].join('\n');
1032
+ }
1033
+
1034
+ function mergeYamlScalar(lines, key, value) {
1035
+ const re = new RegExp(`^${escapeRegExp(key)}\\s*:`);
1036
+ const idx = lines.findIndex(line => re.test(line.trim()));
1037
+ if (idx === -1) lines.push(`${key}: ${value}`);
1038
+ else lines[idx] = `${key}: ${value}`;
1039
+ return lines;
1040
+ }
1041
+
1042
+ function mergeYamlScalarIfMissing(lines, key, value) {
1043
+ const re = new RegExp(`^${escapeRegExp(key)}\\s*:`);
1044
+ if (lines.findIndex(line => re.test(line.trim())) === -1) lines.push(`${key}: ${value}`);
1045
+ return lines;
1046
+ }
1047
+
1048
+ function mergeYamlTags(lines, tags) {
1049
+ const idx = lines.findIndex(line => /^tags\s*:/.test(line.trim()));
1050
+ const tagLines = ['tags:', ...tags.map(tag => ` - ${tag}`)];
1051
+ if (idx === -1) {
1052
+ lines.push(...tagLines);
1053
+ return;
1054
+ }
1055
+
1056
+ let end = idx + 1;
1057
+ while (end < lines.length && (/^\s+-\s+/.test(lines[end]) || lines[end].trim() === '')) end += 1;
1058
+ lines.splice(idx, end - idx, ...tagLines);
1059
+ }
1060
+
1061
+ function readYamlTags(lines) {
1062
+ const tags = [];
1063
+ const inline = lines.find(line => /^tags\s*:\s*\[/.test(line.trim()));
1064
+ if (inline) {
1065
+ const m = inline.match(/\[(.*)\]/);
1066
+ if (m) {
1067
+ for (const item of m[1].split(',')) {
1068
+ const tag = item.trim().replace(/^['"]|['"]$/g, '').replace(/^#/, '');
1069
+ if (tag) tags.push(tag);
1070
+ }
1071
+ }
1072
+ }
1073
+
1074
+ const idx = lines.findIndex(line => /^tags\s*:/.test(line.trim()));
1075
+ if (idx !== -1) {
1076
+ for (let i = idx + 1; i < lines.length; i++) {
1077
+ const m = lines[i].match(/^\s+-\s+(.+?)\s*$/);
1078
+ if (!m) {
1079
+ if (lines[i].trim() === '') continue;
1080
+ break;
1081
+ }
1082
+ const tag = m[1].trim().replace(/^['"]|['"]$/g, '').replace(/^#/, '');
1083
+ if (tag) tags.push(tag);
1084
+ }
1085
+ }
1086
+
1087
+ return [...new Set(tags)];
1088
+ }
1089
+
1090
+ function escapeRegExp(value) {
1091
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1092
+ }
1093
+
744
1094
  // ═══════════════════════════════════════════════════════════════════
745
1095
  // TEMPLATES — entry files
746
1096
  // ═══════════════════════════════════════════════════════════════════
@@ -872,8 +1222,11 @@ ${SNAP_E}
872
1222
  - [Done Checklist](05-done-checklist.md)
873
1223
  - [Project Rules](06-project-rules.md)
874
1224
  - [Session Summary](session-summary.md)
875
- - [Project Log](log.md)
1225
+ - [Activity](activity.md)
1226
+ - [Actors](actors/README.md)
1227
+ - [Worklog](worklog/README.md)
876
1228
  - [Wiki Index](wiki/index.md)
1229
+ - [Raw Sources](raw/README.md)
877
1230
  - [Systems Index](systems/README.md)
878
1231
 
879
1232
  ## System Docs
@@ -911,7 +1264,7 @@ _None yet._
911
1264
 
912
1265
  ## Completed Tasks
913
1266
 
914
- See \`.memoc/log.md\` for full history.
1267
+ See \`.memoc/worklog/\` for full shared activity history.
915
1268
 
916
1269
  ## Commands
917
1270
 
@@ -923,14 +1276,16 @@ _None yet._
923
1276
 
924
1277
  ## Change Log
925
1278
 
926
- See \`.memoc/log.md\`.
1279
+ See \`.memoc/worklog/\` and generated \`.memoc/activity.md\`.
927
1280
  `;
928
1281
  }
929
1282
 
930
1283
  function tplSessionSummary() {
931
1284
  return `# Session Summary
932
1285
  Last: ${nowISO()}
933
- Keep each section 3 bullets. Agent-owned updated by you, not by \`memoc update\`.
1286
+ Replace this file instead of appending to it. Keep total size <800B and each section ≤3 bullets.
1287
+ Completed history belongs in actor worklogs; incomplete/risky resume detail belongs in \`04-handoff.md\`.
1288
+ Agent-owned — updated by you, not by \`memoc update\`.
934
1289
 
935
1290
  ## Status
936
1291
  _What is the current state of the project?_
@@ -966,7 +1321,6 @@ On-demand reference only. The entry-file managed block is authoritative.
966
1321
  | \`.memoc/01-agent-workflow.md\` | When update routing is unclear |
967
1322
  | \`.memoc/05-done-checklist.md\` | Before finishing substantial work |
968
1323
  | \`.memoc/03-decisions.md\` | When a durable decision was made |
969
- | \`.memoc/log.md\` | For append-only history |
970
1324
  | \`.memoc/memoc-usage.md\` | For command details |
971
1325
  | \`.memoc/systems/*.md\` | Before touching a specific subsystem |
972
1326
  | \`.memoc/wiki/*.md\` | For synthesized project knowledge |
@@ -998,12 +1352,17 @@ Shared protocol for any coding agent.
998
1352
 
999
1353
  | Trigger | Update |
1000
1354
  | --- | --- |
1001
- | User creates or changes a requirement | \`02-current-project-state.md\`, \`06-project-rules.md\`, \`log.md\` |
1002
- | Code, config, data, or assets changed | \`02-current-project-state.md\`, relevant \`systems/*.md\`, \`log.md\` |
1355
+ | User asks "update memoc", "refresh project memory", or similar | Run \`memoc update\` first, then update relevant agent-owned memory files |
1356
+ | User creates or changes a requirement | \`02-current-project-state.md\`, \`06-project-rules.md\`, \`memoc work "<title>" --from-git\` |
1357
+ | Code, config, data, or assets changed | \`02-current-project-state.md\`, relevant \`systems/*.md\`, \`memoc work "<title>" --from-git\` |
1003
1358
  | Architecture or system behavior changed | relevant \`systems/*.md\`, \`03-decisions.md\` |
1004
1359
  | A decision should affect future agents | \`03-decisions.md\`, \`02-current-project-state.md\` |
1005
- | Work is substantial enough to resume later | \`04-handoff.md\`, \`02-current-project-state.md\`, \`log.md\` |
1360
+ | Work is substantial enough to resume later | \`04-handoff.md\`, \`02-current-project-state.md\`, \`memoc work "<title>" --from-git\` |
1006
1361
  | Durable knowledge was learned | \`wiki/*.md\`, \`wiki/index.md\` |
1362
+ | Source material should feed the wiki | \`memoc ingest <path-or-url>\`, then synthesize affected \`wiki/topics/*.md\` |
1363
+ | A useful query answer should persist | \`memoc note "<title>"\`, then link related sources/topics |
1364
+ | Shared repo work should be traceable | \`memoc work "<title>"\`; avoid appending long details to shared core files |
1365
+ | \`session-summary.md\` exceeds 800B or starts accumulating history | Run \`memoc trim-summary\`; move completed history to worklog, resume details to \`04-handoff.md\` |
1007
1366
 
1008
1367
  ## Usually No Update Needed
1009
1368
 
@@ -1014,11 +1373,11 @@ Shared protocol for any coding agent.
1014
1373
  ## Documentation Shape
1015
1374
 
1016
1375
  - Entry files: protocol only.
1017
- - \`session-summary.md\`: latest snapshot, max 3 bullets per section.
1376
+ - \`session-summary.md\`: replace-only latest snapshot, <800B, max 3 bullets per section; never use as history.
1018
1377
  - \`02-current-project-state.md\`: current status, tasks, commands, recent notes.
1019
1378
  - \`04-handoff.md\`: resume context, blockers, verified/unverified checks.
1020
1379
  - \`03-decisions.md\`: append durable decisions only.
1021
- - \`log.md\`: full history; keep bulky completed work here.
1380
+ - \`worklog/<actor>/YYYY-MM/*.md\`: actor-scoped append-by-new-file activity records for shared repos.
1022
1381
  - \`systems/*.md\` and \`wiki/*.md\`: on-demand durable knowledge.
1023
1382
  `;
1024
1383
  }
@@ -1090,7 +1449,7 @@ Run through this before saying substantial work is complete.
1090
1449
  - [ ] \`.memoc/02-current-project-state.md\` reflects the new status.
1091
1450
  - [ ] \`.memoc/03-decisions.md\` updated if a durable decision was made.
1092
1451
  - [ ] \`.memoc/04-handoff.md\` updated if work is incomplete or risky.
1093
- - [ ] \`.memoc/log.md\` has a new entry for meaningful work.
1452
+ - [ ] Meaningful shared work has a \`.memoc/worklog/<actor>/YYYY-MM/*.md\` entry.
1094
1453
  - [ ] Relevant \`.memoc/systems/*.md\` or wiki pages updated.
1095
1454
 
1096
1455
  ## Communication
@@ -1115,7 +1474,7 @@ Durable user and project preferences live here. Update when the user gives a rul
1115
1474
  ## Agent Behavior Preferences
1116
1475
 
1117
1476
  - Be factual and operational in memory docs.
1118
- - Keep logs concise; do not paste temporary command output unless it changes future work.
1477
+ - Keep memory notes concise; do not paste temporary command output unless it changes future work.
1119
1478
  - Preserve user changes and avoid reverting unrelated work.
1120
1479
  - State unverified parts honestly in the final answer and handoff.
1121
1480
 
@@ -1128,9 +1487,75 @@ _None yet._
1128
1487
  function tplLog() {
1129
1488
  return `# Project Log
1130
1489
 
1131
- Append-only chronological log for project memory updates.
1490
+ Legacy file. New memoc activity belongs in actor worklogs under \`.memoc/worklog/\`.
1491
+
1492
+ This file is no longer created for new projects. Existing projects may delete it after migrating useful entries to worklogs.
1493
+ `;
1494
+ }
1495
+
1496
+ function tplActivity() {
1497
+ return `# Activity
1498
+
1499
+ Shared activity index for memoc work logs.
1500
+
1501
+ ## How To Use
1502
+
1503
+ - Use \`memoc work "<title>"\` after meaningful work so shared repos get conflict-light per-actor records.
1504
+ - Keep this file short; detailed work belongs in [worklog](worklog/README.md).
1505
+ - Actor is detected from \`MEMOC_ACTOR\`, \`.memoc/local/actor\`, git config, git email, or OS user.
1506
+
1507
+ ## Recent Work
1508
+
1509
+ _None yet._
1510
+
1511
+ ## Related
1512
+
1513
+ - [Actors](actors/README.md)
1514
+ - [Worklog](worklog/README.md)
1515
+ `;
1516
+ }
1517
+
1518
+ function tplActorsReadme() {
1519
+ return `# Actors
1520
+
1521
+ People or agents that update memoc in this shared repo.
1522
+
1523
+ ## Actor Detection
1132
1524
 
1133
- ## [${nowISO()}] init | Initialized memoc memory structure.
1525
+ 1. \`MEMOC_ACTOR\`
1526
+ 2. \`.memoc/local/actor\` set by \`memoc actor set <name>\`
1527
+ 3. \`git config user.name\`
1528
+ 4. \`git config user.email\`
1529
+ 5. OS username
1530
+
1531
+ \`.memoc/local/\` is ignored by git so each machine can keep its own actor setting.
1532
+
1533
+ ## Actors
1534
+
1535
+ _None yet. Use \`memoc actor set <name>\` or \`memoc work "<title>"\`._
1536
+ `;
1537
+ }
1538
+
1539
+ function tplWorklogReadme() {
1540
+ return `# Worklog
1541
+
1542
+ Conflict-light per-actor work records.
1543
+
1544
+ ## Rules
1545
+
1546
+ - Prefer creating new worklog files over appending shared core memory files.
1547
+ - Keep \`session-summary.md\` as the tiny current snapshot.
1548
+ - Put detailed completed work here; put only team-critical unfinished risk in \`04-handoff.md\`.
1549
+
1550
+ ## Layout
1551
+
1552
+ \`\`\`text
1553
+ worklog/<actor>/YYYY-MM/YYYYMMDDTHHMM-title.md
1554
+ \`\`\`
1555
+
1556
+ ## Recent Work
1557
+
1558
+ _None yet._
1134
1559
  `;
1135
1560
  }
1136
1561
 
@@ -1154,6 +1579,15 @@ memoc upgrade
1154
1579
 
1155
1580
  # Explicitly update managed sections based on current project state
1156
1581
  memoc update
1582
+ memoc trim-summary
1583
+
1584
+ # Shared repo actor/work tracking
1585
+ memoc actor
1586
+ memoc actor set neneee
1587
+ memoc work "Auth refresh fix" --from-git
1588
+ memoc activity
1589
+ memoc activity --write
1590
+ memoc doctor
1157
1591
 
1158
1592
  # Tiny status overview
1159
1593
  memoc summary
@@ -1165,6 +1599,11 @@ memoc search "<query>" --snippets --limit 5
1165
1599
  # Search project source/text files when memory is not enough
1166
1600
  memoc grep "<query>" --limit 12
1167
1601
  memoc grep "<query>" --snippets --limit 5
1602
+
1603
+ # Wiki operations
1604
+ memoc ingest <path-or-url>
1605
+ memoc note "Durable topic or query result"
1606
+ memoc lint-wiki
1168
1607
  \`\`\`
1169
1608
 
1170
1609
  If \`memoc\` is not on PATH, use \`.\\.memoc\\bin\\memoc.cmd <command>\` on Windows or \`.memoc/bin/memoc <command>\` in sh for the rest of the session. If the local wrapper is missing, use \`npx @kevin0181/memoc <command>\` or re-run init.
@@ -1180,30 +1619,55 @@ If \`memoc\` is not on PATH, use \`.\\.memoc\\bin\\memoc.cmd <command>\` on Wind
1180
1619
 
1181
1620
  Use \`memoc search\` for known concepts, changed areas, decisions, tasks, or handoff notes. Skip it for brand-new questions where no prior memory can exist.
1182
1621
 
1622
+ Raw files under \`.memoc/raw/\` are intentionally not part of normal memory search. Open them only through a linked source record when provenance is needed.
1623
+
1624
+ ## Shared Repo Activity
1625
+
1626
+ Use \`memoc work "<title>" --from-git\` to create conflict-light activity records under \`.memoc/worklog/<actor>/YYYY-MM/\`. The command prefills actor, branch, timestamp, and changed files from git so agents only need to fill short Summary/Verification notes when useful. Actor is detected in this order: \`MEMOC_ACTOR\`, \`.memoc/local/actor\`, \`git config user.name\`, \`git config user.email\`, OS username. Use \`memoc actor set <name>\` to store a local actor name without committing it.
1627
+
1628
+ \`.memoc/activity.md\`, \`.memoc/worklog/README.md\`, and \`.memoc/actors/README.md\` are regenerated indexes. Run \`memoc activity --write\` to rebuild them from worklog/actor files instead of appending to them during every task.
1629
+
1183
1630
  ## When To Run Memory Updates
1184
1631
 
1185
1632
  Use \`memoc update\` or \`skills/project-memory-maintainer/SKILL.md\` when:
1186
1633
 
1634
+ - The user asks to update memoc, refresh project memory, sync project memory, or "update the project in memoc".
1187
1635
  - Requirements, acceptance criteria, user preferences, or project rules changed.
1188
1636
  - Source code, config, data, content, or package scripts changed.
1189
1637
  - Architecture, data flow, routing, auth, or deployment behavior changed.
1190
1638
  - A decision was made that future agents should not revisit blindly.
1191
1639
  - Work is partial, multi-step, blocked, or likely to be resumed by another agent.
1192
1640
  - New durable knowledge belongs in \`.memoc/wiki/\` or a subsystem doc.
1641
+ - Shared work should be traceable without causing conflicts.
1193
1642
 
1194
1643
  Usually skip for pure Q&A, throwaway exploration, or tiny edits with no future impact.
1195
1644
 
1645
+ When the user asks for a general memoc/project-memory refresh, run \`memoc update\` first. It refreshes managed sections, reconnects default wiki scaffold links, and applies Obsidian frontmatter tags. Then update only the agent-owned files whose content actually changed, such as \`.memoc/session-summary.md\`, \`.memoc/02-current-project-state.md\`, \`.memoc/04-handoff.md\`, \`.memoc/wiki/index.md\`, or actor worklogs.
1646
+
1647
+ \`.memoc/session-summary.md\` is a startup snapshot, not a timeline. Rewrite it in place, do not append old work. If it exceeds 800B, run \`memoc trim-summary\`; it archives the previous summary and rewrites a compact version. Put completed history in actor worklogs, and put unfinished/risky resume detail in \`.memoc/04-handoff.md\`.
1648
+
1196
1649
  ## Updating The Wiki
1197
1650
 
1198
1651
  Create a new Markdown file under \`.memoc/wiki/\` when synthesized knowledge should compound across sessions.
1199
1652
 
1653
+ - \`.memoc/raw/\`: immutable source material copied or referenced by \`memoc ingest\`.
1200
1654
  - \`.memoc/wiki/sources/\`: provenance records.
1201
1655
  - \`.memoc/wiki/topics/\`: synthesized topic pages.
1202
1656
  - \`.memoc/wiki/global/\`: project-wide principles.
1203
1657
 
1204
1658
  After creating or editing wiki pages:
1205
1659
  1. Update \`.memoc/wiki/index.md\`.
1206
- 2. Append \`.memoc/log.md\`.
1660
+ 2. Run \`memoc lint-wiki\`.
1661
+ 3. If the change is meaningful shared work, run \`memoc work "<title>" --from-git\`.
1662
+
1663
+ Useful scaffolds:
1664
+
1665
+ \`\`\`bash
1666
+ memoc ingest path/to/source.md
1667
+ memoc ingest https://example.com/spec
1668
+ memoc note "Auth flow comparison"
1669
+ memoc lint-wiki
1670
+ \`\`\`
1207
1671
 
1208
1672
  ## Updating System Docs
1209
1673
 
@@ -1232,6 +1696,74 @@ Create a new \`.md\` file here when a subsystem becomes important enough that fu
1232
1696
  `;
1233
1697
  }
1234
1698
 
1699
+ function tplRawReadme() {
1700
+ return `# Raw Sources
1701
+
1702
+ Immutable source material for the memoc wiki.
1703
+
1704
+ ## Rules
1705
+
1706
+ - Do not edit raw files after ingest; create a new raw file or source record when material changes.
1707
+ - Do not read raw files at session start. Search or open the linked source/topic page first.
1708
+ - Source records under [wiki/sources](../wiki/sources/README.md) summarize raw material and link to affected topics.
1709
+
1710
+ ## Subdirectories
1711
+
1712
+ - [files](files/README.md) — local files copied during ingest
1713
+ - [urls](urls/README.md) — URL references and fetched/exported material
1714
+ - [conversations](conversations/README.md) — conversation excerpts worth preserving
1715
+ - [docs](docs/README.md) — external docs, specs, and long references
1716
+ `;
1717
+ }
1718
+
1719
+ function tplRawFilesReadme() {
1720
+ return `# Raw Files
1721
+
1722
+ Local files copied by \`memoc ingest <path>\`.
1723
+
1724
+ ## Related
1725
+
1726
+ - [Raw Sources](../README.md)
1727
+ - [Source Records](../../wiki/sources/README.md)
1728
+ `;
1729
+ }
1730
+
1731
+ function tplRawUrlsReadme() {
1732
+ return `# Raw URLs
1733
+
1734
+ URL references recorded by \`memoc ingest <url>\`.
1735
+
1736
+ ## Related
1737
+
1738
+ - [Raw Sources](../README.md)
1739
+ - [Source Records](../../wiki/sources/README.md)
1740
+ `;
1741
+ }
1742
+
1743
+ function tplRawConversationsReadme() {
1744
+ return `# Raw Conversations
1745
+
1746
+ Conversation excerpts that should feed durable wiki synthesis.
1747
+
1748
+ ## Related
1749
+
1750
+ - [Raw Sources](../README.md)
1751
+ - [Source Records](../../wiki/sources/README.md)
1752
+ `;
1753
+ }
1754
+
1755
+ function tplRawDocsReadme() {
1756
+ return `# Raw Docs
1757
+
1758
+ Long-form docs, specs, and references kept separate from synthesized topic pages.
1759
+
1760
+ ## Related
1761
+
1762
+ - [Raw Sources](../README.md)
1763
+ - [Source Records](../../wiki/sources/README.md)
1764
+ `;
1765
+ }
1766
+
1235
1767
  function tplWikiIndex() {
1236
1768
  return `# Wiki Index
1237
1769
 
@@ -1239,6 +1771,7 @@ Persistent LLM-maintained project wiki.
1239
1771
 
1240
1772
  ## Graph Hubs
1241
1773
 
1774
+ - [Raw Sources](../raw/README.md) — immutable source material before synthesis.
1242
1775
  - [Sources](sources.md) — provenance, ingests, and source-to-topic links.
1243
1776
  - [Topics](topics/README.md) — synthesized topic pages.
1244
1777
  - [Global](global/README.md) — project-wide principles and long-lived direction.
@@ -1250,6 +1783,10 @@ Persistent LLM-maintained project wiki.
1250
1783
 
1251
1784
  _None yet. Add every wiki page here with a relative Markdown link and one-line summary._
1252
1785
 
1786
+ ## Saved Queries
1787
+
1788
+ _None yet. Use \`memoc note "<title>"\` for durable analysis or query results that should become a topic._
1789
+
1253
1790
  ## Subdirectories
1254
1791
 
1255
1792
  - [sources/](sources/README.md) — provenance records
@@ -1261,7 +1798,6 @@ _None yet. Add every wiki page here with a relative Markdown link and one-line s
1261
1798
  - [Agent Index](../00-agent-index.md)
1262
1799
  - [Project Brief](../00-project-brief.md)
1263
1800
  - [Current Project State](../02-current-project-state.md)
1264
- - [Project Log](../log.md)
1265
1801
  `;
1266
1802
  }
1267
1803
 
@@ -1274,9 +1810,12 @@ Provenance index for conversations, URLs, docs, issues, and files that feed the
1274
1810
 
1275
1811
  _No sources recorded yet. Link each source record to the topic/global pages it affects._
1276
1812
 
1813
+ Use \`memoc ingest <path-or-url>\` to create source records without loading raw material into startup context.
1814
+
1277
1815
  ## Related
1278
1816
 
1279
1817
  - [Wiki Index](index.md)
1818
+ - [Raw Sources](../raw/README.md)
1280
1819
  - [Source Records Directory](sources/README.md)
1281
1820
  - [Topics](topics/README.md)
1282
1821
  - [Open Questions](questions.md)
@@ -1326,6 +1865,7 @@ Provenance records for conversations, URLs, docs, and issues.
1326
1865
 
1327
1866
  ## How To Link
1328
1867
 
1868
+ - Keep source pages short: summary, raw location, affected pages, open synthesis work.
1329
1869
  - Link each source record back to [Sources](../sources.md).
1330
1870
  - Link outward to every topic, global page, system doc, or question that the source changes.
1331
1871
  - Prefer one source per file when the source is substantial enough to cite later.
@@ -1432,18 +1972,25 @@ Use this local skill after meaningful project work so future agents can continue
1432
1972
 
1433
1973
  ## Maintenance Checklist
1434
1974
 
1975
+ - If the user asked to update/refresh memoc project memory, run \`memoc update\` first so managed sections, wiki scaffold links, and Obsidian tags are current.
1435
1976
  - Keep \`llms.txt\` and \`.memoc/00-agent-index.md\` as concise maps.
1436
1977
  - Keep \`.memoc/00-project-brief.md\` as the shortest project summary.
1978
+ - Rewrite \`.memoc/session-summary.md\` as the latest snapshot only; never append a timeline. If it is over 800B, run \`memoc trim-summary\`.
1437
1979
  - Update \`.memoc/02-current-project-state.md\` with new status, tasks, commands, and change log entries.
1438
1980
  - Update \`.memoc/03-decisions.md\` when a durable decision is made.
1439
1981
  - Update \`.memoc/04-handoff.md\` before ending substantial work.
1440
1982
  - Check \`.memoc/05-done-checklist.md\` before saying substantial work is complete.
1441
1983
  - Update \`.memoc/06-project-rules.md\` when the user gives durable preferences.
1442
- - Append \`.memoc/log.md\` for meaningful changes, decisions, and handoffs.
1984
+ - Create a short actor worklog with \`memoc work "<title>" --from-git\` for meaningful changes, decisions, and handoffs.
1443
1985
  - Create or update \`.memoc/systems/*.md\` when a subsystem needs durable explanation.
1444
1986
  - Create or update \`.memoc/wiki/*.md\` when synthesized knowledge should compound over time.
1987
+ - Use \`memoc ingest <path-or-url>\` for source material and \`memoc note "<title>"\` for durable query results or analysis.
1988
+ - Use \`memoc work "<title>" --from-git\` for meaningful shared-repo work so details are saved in actor-scoped worklog files instead of causing shared-file conflicts.
1445
1989
  - Keep the wiki graph connected: update \`.memoc/wiki/index.md\`, add relative Markdown links between related pages, and include a \`## Related\` section on every new wiki page.
1446
- - Keep completed history in \`.memoc/log.md\`; keep current-state files short.
1990
+ - Run \`memoc lint-wiki\` after wiki/source/topic edits and address broken links before finishing.
1991
+ - Keep completed history in actor worklogs; keep current-state files short.
1992
+ - Move completed session details out of \`session-summary.md\` into \`.memoc/worklog/<actor>/YYYY-MM/\`; move incomplete/risky resume details into \`04-handoff.md\`.
1993
+ - In shared repos, do not use \`log.md\`; prefer new files under \`.memoc/worklog/<actor>/YYYY-MM/\` and regenerate \`.memoc/activity.md\` with \`memoc activity --write\`.
1447
1994
  - Keep tool output small; prefer \`summary\`, file-only search, \`--limit\`, and targeted reads.
1448
1995
 
1449
1996
  ## Wiki Link Rules
@@ -1552,17 +2099,16 @@ function ensureClaudeStopHookFile(dir, mark) {
1552
2099
 
1553
2100
  function ensurePendingGitignore(dir, mark) {
1554
2101
  const gitignorePath = path.join(dir, '.gitignore');
1555
- const PENDING_ENTRY = '.memoc/.pending';
2102
+ const entries = ['.memoc/.pending', '.memoc/local/'];
1556
2103
  const gitignoreContent = fs.existsSync(gitignorePath)
1557
2104
  ? fs.readFileSync(gitignorePath, 'utf8') : '';
1558
- const hasPendingEntry = gitignoreContent
1559
- .split(/\r?\n/)
1560
- .some(line => line.trim() === PENDING_ENTRY);
1561
- if (!hasPendingEntry) {
1562
- fs.appendFileSync(gitignorePath, (gitignoreContent.endsWith('\n') ? '' : '\n') + PENDING_ENTRY + '\n', 'utf8');
1563
- mark('update', '.gitignore (.memoc/.pending added)');
2105
+ const lines = gitignoreContent.split(/\r?\n/).map(line => line.trim());
2106
+ const missing = entries.filter(entry => !lines.includes(entry));
2107
+ if (missing.length) {
2108
+ fs.appendFileSync(gitignorePath, (gitignoreContent.endsWith('\n') ? '' : '\n') + missing.join('\n') + '\n', 'utf8');
2109
+ mark('update', `.gitignore (${missing.join(', ')} added)`);
1564
2110
  } else {
1565
- mark('skip', '.gitignore (.memoc/.pending already present)');
2111
+ mark('skip', '.gitignore (memoc local entries already present)');
1566
2112
  }
1567
2113
  }
1568
2114
 
@@ -1637,9 +2183,16 @@ function run(dir, forceUpdate, action = 'update') {
1637
2183
  [path.join(memDir, '04-handoff.md'), tplHandoff],
1638
2184
  [path.join(memDir, '05-done-checklist.md'), tplDoneChecklist],
1639
2185
  [path.join(memDir, '06-project-rules.md'), tplProjectRules],
1640
- [path.join(memDir, 'log.md'), tplLog],
2186
+ [path.join(memDir, 'activity.md'), tplActivity],
2187
+ [path.join(memDir, 'actors/README.md'), tplActorsReadme],
2188
+ [path.join(memDir, 'worklog/README.md'), tplWorklogReadme],
1641
2189
  [path.join(memDir, 'memoc-usage.md'), tplMemocUsage],
1642
2190
  [path.join(memDir, 'systems/README.md'), tplSystemsReadme],
2191
+ [path.join(memDir, 'raw/README.md'), tplRawReadme],
2192
+ [path.join(memDir, 'raw/files/README.md'), tplRawFilesReadme],
2193
+ [path.join(memDir, 'raw/urls/README.md'), tplRawUrlsReadme],
2194
+ [path.join(memDir, 'raw/conversations/README.md'), tplRawConversationsReadme],
2195
+ [path.join(memDir, 'raw/docs/README.md'), tplRawDocsReadme],
1643
2196
  [path.join(memDir, 'wiki/index.md'), tplWikiIndex],
1644
2197
  [path.join(memDir, 'wiki/sources.md'), tplWikiSources],
1645
2198
  [path.join(memDir, 'wiki/glossary.md'), tplWikiGlossary],
@@ -1661,6 +2214,9 @@ function run(dir, forceUpdate, action = 'update') {
1661
2214
  // .gitignore — add .memoc/.pending if not already present
1662
2215
  ensurePendingGitignore(dir, mark);
1663
2216
 
2217
+ // Obsidian graph filters — tag memoc-owned Markdown without touching unrelated docs
2218
+ ensureObsidianFrontmatter(dir, mark);
2219
+
1664
2220
  // PATH helpers — let agents run memoc even when the npm bin is not on PATH
1665
2221
  ensurePathHelpers(dir, mark);
1666
2222
  ensurePathRegistration(dir, mark);
@@ -1735,9 +2291,16 @@ function run(dir, forceUpdate, action = 'update') {
1735
2291
  [path.join(memDir, '04-handoff.md'), tplHandoff],
1736
2292
  [path.join(memDir, '05-done-checklist.md'), tplDoneChecklist],
1737
2293
  [path.join(memDir, '06-project-rules.md'), tplProjectRules],
1738
- [path.join(memDir, 'log.md'), tplLog],
2294
+ [path.join(memDir, 'activity.md'), tplActivity],
2295
+ [path.join(memDir, 'actors/README.md'), tplActorsReadme],
2296
+ [path.join(memDir, 'worklog/README.md'), tplWorklogReadme],
1739
2297
  [path.join(memDir, 'memoc-usage.md'), tplMemocUsage],
1740
2298
  [path.join(memDir, 'systems/README.md'), tplSystemsReadme],
2299
+ [path.join(memDir, 'raw/README.md'), tplRawReadme],
2300
+ [path.join(memDir, 'raw/files/README.md'), tplRawFilesReadme],
2301
+ [path.join(memDir, 'raw/urls/README.md'), tplRawUrlsReadme],
2302
+ [path.join(memDir, 'raw/conversations/README.md'), tplRawConversationsReadme],
2303
+ [path.join(memDir, 'raw/docs/README.md'), tplRawDocsReadme],
1741
2304
  [path.join(memDir, 'wiki/index.md'), tplWikiIndex],
1742
2305
  [path.join(memDir, 'wiki/sources.md'), tplWikiSources],
1743
2306
  [path.join(memDir, 'wiki/glossary.md'), tplWikiGlossary],
@@ -1755,21 +2318,16 @@ function run(dir, forceUpdate, action = 'update') {
1755
2318
  }
1756
2319
  ensureWikiScaffoldLinks(memDir, mark);
1757
2320
 
2321
+ // Obsidian graph filters — add/merge memoc tags for existing installs too
2322
+ ensureObsidianFrontmatter(dir, mark);
2323
+
1758
2324
  // PATH helpers — let agents run memoc even when the npm bin is not on PATH
1759
2325
  ensureClaudeStopHookFile(dir, mark);
1760
2326
  ensurePendingGitignore(dir, mark);
1761
2327
  ensurePathHelpers(dir, mark);
1762
2328
  ensurePathRegistration(dir, mark);
1763
2329
 
1764
- // Append update record to log.md
1765
- const logPath = path.join(memDir, 'log.md');
1766
- if (fs.existsSync(logPath)) {
1767
- fs.appendFileSync(logPath,
1768
- `\n## [${nowISO()}] ${action} | Re-scanned: ${p.isEmpty ? 'nothing detected' : stackStr(p.stack)}\n`,
1769
- 'utf8'
1770
- );
1771
- mark('append', '.memoc/log.md');
1772
- }
2330
+ mark('skip', '.memoc/log.md (legacy; shared history belongs in worklog)');
1773
2331
  }
1774
2332
 
1775
2333
  hideOnWindows(memDir);
@@ -1808,6 +2366,586 @@ function runAdd(dir) {
1808
2366
  console.log('\n Done.');
1809
2367
  }
1810
2368
 
2369
+ // ═══════════════════════════════════════════════════════════════════
2370
+ // ACTOR / WORKLOG — conflict-light shared repo activity tracking
2371
+ // ═══════════════════════════════════════════════════════════════════
2372
+
2373
+ function runActor(dir) {
2374
+ const sub = (process.argv[3] || '').toLowerCase();
2375
+ if (sub === 'set') {
2376
+ const name = process.argv.slice(4).join(' ').trim();
2377
+ if (!name) {
2378
+ console.error('\n Usage: memoc actor set <name>');
2379
+ process.exit(1);
2380
+ }
2381
+ const actor = sanitizeActor(name);
2382
+ write(actorFile(dir), `${actor}\n`);
2383
+ ensurePendingGitignore(dir, () => {});
2384
+ console.log('\n memoc actor\n');
2385
+ console.log(` Set local actor: ${actor}`);
2386
+ console.log(' Stored in .memoc/local/actor (ignored by git).');
2387
+ console.log();
2388
+ return;
2389
+ }
2390
+
2391
+ const detected = detectActor(dir);
2392
+ console.log('\n memoc actor\n');
2393
+ console.log(` Actor ${detected.actor}`);
2394
+ console.log(` Source ${detected.source}`);
2395
+ console.log('\n Use `memoc actor set <name>` to override locally.\n');
2396
+ }
2397
+
2398
+ function runWork(dir) {
2399
+ const rawArgs = process.argv.slice(3);
2400
+ const opts = { status: 'done', body: '', fromGit: true };
2401
+ const titleParts = [];
2402
+ for (let i = 0; i < rawArgs.length; i++) {
2403
+ const arg = rawArgs[i];
2404
+ if (arg === '--status') {
2405
+ opts.status = sanitizeActor(rawArgs[++i] || 'done');
2406
+ continue;
2407
+ }
2408
+ if (arg.startsWith('--status=')) {
2409
+ opts.status = sanitizeActor(arg.slice('--status='.length) || 'done');
2410
+ continue;
2411
+ }
2412
+ if (arg === '--body') {
2413
+ opts.body = rawArgs.slice(i + 1).join(' ');
2414
+ break;
2415
+ }
2416
+ if (arg === '--from-git') {
2417
+ opts.fromGit = true;
2418
+ continue;
2419
+ }
2420
+ if (arg === '--no-git') {
2421
+ opts.fromGit = false;
2422
+ continue;
2423
+ }
2424
+ titleParts.push(arg);
2425
+ }
2426
+
2427
+ const title = titleParts.join(' ').trim();
2428
+ if (!title) {
2429
+ console.error('\n Usage: memoc work "<title>" [--status done|wip|blocked] [--from-git|--no-git] [--body "summary"]');
2430
+ process.exit(1);
2431
+ }
2432
+
2433
+ ensureMemocBase(dir);
2434
+ const detected = detectActor(dir);
2435
+ ensureActorProfile(dir, detected);
2436
+ const stamp = new Date().toISOString().slice(0, 16).replace(/[-:]/g, '').replace('T', 'T');
2437
+ const month = todayISO().slice(0, 7);
2438
+ const fileName = `${stamp}-${slugify(title, 'work')}.md`;
2439
+ const workPath = uniquePath(path.join(dir, '.memoc', 'worklog', detected.actor, month, fileName));
2440
+ write(workPath, worklogRecord(dir, title, detected, opts));
2441
+ ensureMemocFrontmatter(workPath, dir);
2442
+
2443
+ console.log('\n memoc work\n');
2444
+ console.log(` Actor ${detected.actor} (${detected.source})`);
2445
+ console.log(` Work ${normRel(dir, workPath)}`);
2446
+ console.log(' Next Fill only Summary/Verification if needed; run `memoc activity --write` to regenerate indexes.');
2447
+ console.log();
2448
+ }
2449
+
2450
+ function runActivity(dir) {
2451
+ const writeIndex = process.argv.slice(3).includes('--write');
2452
+ const workRoot = path.join(dir, '.memoc', 'worklog');
2453
+ const files = listMarkdownFiles(workRoot)
2454
+ .filter(fp => path.basename(fp) !== 'README.md')
2455
+ .sort()
2456
+ .reverse();
2457
+ const recent = files.slice(0, 20);
2458
+
2459
+ if (writeIndex) {
2460
+ writeActivityIndexes(dir, recent);
2461
+ ensureObsidianFrontmatter(dir, () => {});
2462
+ }
2463
+
2464
+ console.log('\n memoc activity\n');
2465
+ if (!recent.length) {
2466
+ console.log(' No worklog entries yet. Use `memoc work "<title>"`.');
2467
+ console.log();
2468
+ return;
2469
+ }
2470
+ for (const fp of recent) {
2471
+ const src = safeRead(fp);
2472
+ const title = markdownTitle(src, path.basename(fp, '.md'));
2473
+ const actor = (src.match(/^actor:\s*(.+)$/m) || [])[1] || 'unknown';
2474
+ const status = (src.match(/^status:\s*(.+)$/m) || [])[1] || 'unknown';
2475
+ console.log(` - ${normRel(dir, fp)} ${actor} ${status} ${title}`);
2476
+ }
2477
+ if (writeIndex) console.log('\n Wrote .memoc/activity.md and .memoc/worklog/README.md');
2478
+ console.log();
2479
+ }
2480
+
2481
+ function ensureActorProfile(dir, detected) {
2482
+ const actorPath = path.join(dir, '.memoc', 'actors', `${detected.actor}.md`);
2483
+ if (fs.existsSync(actorPath)) return;
2484
+ write(actorPath, actorProfile(detected));
2485
+ ensureMemocFrontmatter(actorPath, dir);
2486
+ }
2487
+
2488
+ function actorProfile(detected) {
2489
+ return `# ${detected.actor}
2490
+
2491
+ ## Identity
2492
+
2493
+ - Actor: ${detected.actor}
2494
+ - First detected from: ${detected.source}
2495
+ - First seen: ${nowISO()}
2496
+
2497
+ ## Notes
2498
+
2499
+ _Add stable collaboration preferences or ownership notes only when useful._
2500
+
2501
+ ## Related
2502
+
2503
+ - [Actors](README.md)
2504
+ - [Activity](../activity.md)
2505
+ - [Worklog](../worklog/README.md)
2506
+ `;
2507
+ }
2508
+
2509
+ function worklogRecord(dir, title, detected, opts) {
2510
+ const branch = gitBranch(dir);
2511
+ const changedFiles = opts.fromGit ? gitStatusFiles(dir) : [];
2512
+ return `# ${title}
2513
+
2514
+ actor: ${detected.actor}
2515
+ actor_source: ${detected.source}
2516
+ branch: ${branch}
2517
+ status: ${opts.status}
2518
+ created: ${nowISO()}
2519
+
2520
+ ## Summary
2521
+
2522
+ ${opts.body ? `- ${opts.body}` : '_1-3 bullets only. Keep this as a short receipt, not a report._'}
2523
+
2524
+ ## Changed Files
2525
+
2526
+ ${changedFiles.length ? changedFiles.map(file => `- \`${file}\``).join('\n') : '_None detected. Use `memoc work "<title>" --from-git` after editing files to prefill this section._'}
2527
+
2528
+ ## Verification
2529
+
2530
+ _Commands run or checks not run. Keep to 1-3 bullets._
2531
+
2532
+ ## Follow-up
2533
+
2534
+ _None._
2535
+
2536
+ ## Related
2537
+
2538
+ - [Activity](../../../activity.md)
2539
+ - [Worklog](../../README.md)
2540
+ - [Actor](../../../actors/${detected.actor}.md)
2541
+ `;
2542
+ }
2543
+
2544
+ function writeActivityIndexes(dir, recentFiles) {
2545
+ const activityPath = path.join(dir, '.memoc', 'activity.md');
2546
+ const worklogReadme = path.join(dir, '.memoc', 'worklog', 'README.md');
2547
+ const actorsReadme = path.join(dir, '.memoc', 'actors', 'README.md');
2548
+ const rows = recentFiles.map(fp => {
2549
+ const src = safeRead(fp);
2550
+ const title = markdownTitle(src, path.basename(fp, '.md'));
2551
+ const actor = (src.match(/^actor:\s*(.+)$/m) || [])[1] || 'unknown';
2552
+ const status = (src.match(/^status:\s*(.+)$/m) || [])[1] || 'unknown';
2553
+ return { fp, title, actor, status };
2554
+ });
2555
+ const activityItems = rows.length
2556
+ ? rows.map(row => `- [${row.title}](${pathRelativeMarkdown(path.join(dir, '.memoc'), row.fp).replace(/^\.\//, '')}) — ${row.actor} ${row.status}.`).join('\n')
2557
+ : '_None yet._';
2558
+ write(activityPath, `# Activity
2559
+
2560
+ Generated shared activity index for memoc work logs.
2561
+
2562
+ Last generated: ${nowISO()}
2563
+
2564
+ ## Recent Work
2565
+
2566
+ ${activityItems}
2567
+
2568
+ ## Related
2569
+
2570
+ - [Actors](actors/README.md)
2571
+ - [Worklog](worklog/README.md)
2572
+ `);
2573
+
2574
+ const worklogItems = rows.length
2575
+ ? rows.map(row => `- [${row.title}](${pathRelativeMarkdown(path.join(dir, '.memoc', 'worklog'), row.fp).replace(/^\.\//, '')}) — ${row.actor} ${row.status}.`).join('\n')
2576
+ : '_None yet._';
2577
+ write(worklogReadme, `# Worklog
2578
+
2579
+ Generated index of conflict-light per-actor work records.
2580
+
2581
+ Last generated: ${nowISO()}
2582
+
2583
+ ## Layout
2584
+
2585
+ \`\`\`text
2586
+ worklog/<actor>/YYYY-MM/YYYYMMDDTHHMM-title.md
2587
+ \`\`\`
2588
+
2589
+ ## Rules
2590
+
2591
+ - Prefer creating new worklog files over appending shared core memory files.
2592
+ - Keep worklog entries short: 1-3 summary bullets, key files, verification.
2593
+
2594
+ ## Recent Work
2595
+
2596
+ ${worklogItems}
2597
+ `);
2598
+
2599
+ const actorFiles = listMarkdownFiles(path.join(dir, '.memoc', 'actors'))
2600
+ .filter(fp => path.basename(fp) !== 'README.md')
2601
+ .sort();
2602
+ const actorItems = actorFiles.length
2603
+ ? actorFiles.map(fp => `- [${markdownTitle(safeRead(fp), path.basename(fp, '.md'))}](${path.basename(fp)})`).join('\n')
2604
+ : '_None yet. Use `memoc actor set <name>` or `memoc work "<title>"`._';
2605
+ write(actorsReadme, `# Actors
2606
+
2607
+ Generated actor index for this shared repo.
2608
+
2609
+ ## Actor Detection
2610
+
2611
+ 1. \`MEMOC_ACTOR\`
2612
+ 2. \`.memoc/local/actor\` set by \`memoc actor set <name>\`
2613
+ 3. \`git config user.name\`
2614
+ 4. \`git config user.email\`
2615
+ 5. OS username
2616
+
2617
+ \`.memoc/local/\` is ignored by git so each machine can keep its own actor setting.
2618
+
2619
+ ## Actors
2620
+
2621
+ ${actorItems}
2622
+ `);
2623
+ }
2624
+
2625
+ // ═══════════════════════════════════════════════════════════════════
2626
+ // WIKI OPERATIONS — lint, ingest, and durable topic notes
2627
+ // ═══════════════════════════════════════════════════════════════════
2628
+
2629
+ function runWikiLint(dir) {
2630
+ ensureObsidianFrontmatter(dir, () => {});
2631
+ const wikiDir = path.join(dir, '.memoc', 'wiki');
2632
+ const files = listMarkdownFiles(wikiDir);
2633
+ const issues = [];
2634
+ const warnings = [];
2635
+ const inbound = new Map(files.map(fp => [normRel(dir, fp), 0]));
2636
+
2637
+ for (const fp of files) {
2638
+ const rel = normRel(dir, fp);
2639
+ const src = safeRead(fp);
2640
+ if (!parseYamlFrontmatter(src)) warnings.push(`${rel}: missing YAML frontmatter`);
2641
+ if (!src.includes('memoc/wiki')) warnings.push(`${rel}: missing memoc/wiki tag`);
2642
+ if (!/^## Related\b/m.test(src) && !rel.endsWith('wiki/index.md')) warnings.push(`${rel}: missing ## Related section`);
2643
+
2644
+ for (const link of markdownLinks(src)) {
2645
+ if (/^[a-z][a-z0-9+.-]*:/i.test(link) || link.startsWith('#')) continue;
2646
+ const target = resolveMarkdownLink(fp, link);
2647
+ if (!target) continue;
2648
+ if (!fs.existsSync(target)) {
2649
+ issues.push(`${rel}: broken link ${link}`);
2650
+ continue;
2651
+ }
2652
+ const targetRel = normRel(dir, target);
2653
+ if (inbound.has(targetRel)) inbound.set(targetRel, inbound.get(targetRel) + 1);
2654
+ }
2655
+ }
2656
+
2657
+ for (const [rel, count] of inbound.entries()) {
2658
+ if (rel.endsWith('wiki/index.md')) continue;
2659
+ if (count === 0) warnings.push(`${rel}: no inbound wiki links`);
2660
+ }
2661
+
2662
+ const lintPath = path.join(wikiDir, 'lint.md');
2663
+ write(lintPath, wikiLintReport(issues, warnings));
2664
+ ensureMemocFrontmatter(lintPath, dir);
2665
+
2666
+ console.log('\n memoc lint-wiki\n');
2667
+ console.log(` Files ${files.length}`);
2668
+ console.log(` Issues ${issues.length}`);
2669
+ console.log(` Warnings ${warnings.length}`);
2670
+ console.log(' Report .memoc/wiki/lint.md');
2671
+ if (issues.length) {
2672
+ console.log('\n Issues:');
2673
+ for (const issue of issues.slice(0, 10)) console.log(` - ${issue}`);
2674
+ }
2675
+ if (!issues.length && !warnings.length) console.log('\n No issues found.');
2676
+ console.log();
2677
+ }
2678
+
2679
+ function wikiLintReport(issues, warnings) {
2680
+ return `# Wiki Lint
2681
+
2682
+ Last checked: ${nowISO()}
2683
+
2684
+ ## Graph Checks
2685
+
2686
+ - Every wiki page is listed from [Wiki Index](index.md) or a directory README.
2687
+ - Every wiki page links back to an index, hub, source, topic, or related page.
2688
+ - Important concepts mentioned in two or more places have their own linked page.
2689
+ - Source records link to the pages they update, and those pages link back to sources when provenance matters.
2690
+
2691
+ ## Issues
2692
+
2693
+ ${issues.length ? issues.map(x => `- ${x}`).join('\n') : '_No issues found._'}
2694
+
2695
+ ## Warnings
2696
+
2697
+ ${warnings.length ? warnings.map(x => `- ${x}`).join('\n') : '_None._'}
2698
+
2699
+ ## Related
2700
+
2701
+ - [Wiki Index](index.md)
2702
+ - [Sources](sources.md)
2703
+ - [Topics](topics/README.md)
2704
+ - [Open Questions](questions.md)
2705
+ `;
2706
+ }
2707
+
2708
+ function runIngest(dir) {
2709
+ const target = process.argv[3];
2710
+ if (!target) {
2711
+ console.error('\n Usage: memoc ingest <path-or-url>');
2712
+ process.exit(1);
2713
+ }
2714
+
2715
+ ensureMemocBase(dir);
2716
+ const isUrl = /^[a-z][a-z0-9+.-]*:\/\//i.test(target);
2717
+ const title = ingestTitle(dir, target, isUrl);
2718
+ const slug = `${todayISO()}-${slugify(title, 'source')}`;
2719
+ let rawRef;
2720
+ let rawDisplay;
2721
+
2722
+ if (isUrl) {
2723
+ const rawPath = uniquePath(path.join(dir, '.memoc', 'raw', 'urls', `${slug}.md`));
2724
+ write(rawPath, rawUrlRecord(title, target));
2725
+ ensureMemocFrontmatter(rawPath, dir);
2726
+ rawRef = pathRelativeMarkdown(path.join(dir, '.memoc', 'wiki', 'sources'), rawPath);
2727
+ rawDisplay = normRel(dir, rawPath);
2728
+ } else {
2729
+ const abs = path.resolve(dir, target);
2730
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
2731
+ console.error(`\n Source file not found: ${target}`);
2732
+ process.exit(1);
2733
+ }
2734
+ const ext = path.extname(abs) || '.txt';
2735
+ const rawPath = uniquePath(path.join(dir, '.memoc', 'raw', 'files', `${slug}${ext}`));
2736
+ fs.mkdirSync(path.dirname(rawPath), { recursive: true });
2737
+ fs.copyFileSync(abs, rawPath);
2738
+ rawRef = pathRelativeMarkdown(path.join(dir, '.memoc', 'wiki', 'sources'), rawPath);
2739
+ rawDisplay = normRel(dir, rawPath);
2740
+ }
2741
+
2742
+ const sourcePath = uniquePath(path.join(dir, '.memoc', 'wiki', 'sources', `${slug}.md`));
2743
+ write(sourcePath, sourceRecord(title, rawRef, target, isUrl));
2744
+ ensureMemocFrontmatter(sourcePath, dir);
2745
+ addWikiListItem(path.join(dir, '.memoc', 'wiki', 'sources.md'), 'Source Records', pathRelativeMarkdown(path.join(dir, '.memoc', 'wiki'), sourcePath), title, 'needs synthesis');
2746
+ addWikiListItem(path.join(dir, '.memoc', 'wiki', 'sources', 'README.md'), 'Source Records', path.basename(sourcePath), title, 'source record');
2747
+ addWikiListItem(path.join(dir, '.memoc', 'wiki', 'index.md'), 'Pages', pathRelativeMarkdown(path.join(dir, '.memoc', 'wiki'), sourcePath), title, 'source record');
2748
+ console.log('\n memoc ingest\n');
2749
+ console.log(` Source record ${normRel(dir, sourcePath)}`);
2750
+ console.log(` Raw reference ${rawDisplay}`);
2751
+ console.log(' Next Synthesize affected topics, then run memoc lint-wiki.');
2752
+ console.log();
2753
+ }
2754
+
2755
+ function runNote(dir) {
2756
+ const rawArgs = process.argv.slice(3);
2757
+ const bodyIndex = rawArgs.indexOf('--body');
2758
+ let body = '';
2759
+ let titleArgs = rawArgs;
2760
+ if (bodyIndex !== -1) {
2761
+ titleArgs = rawArgs.slice(0, bodyIndex);
2762
+ body = rawArgs.slice(bodyIndex + 1).join(' ');
2763
+ }
2764
+ const title = titleArgs.join(' ').trim();
2765
+ if (!title) {
2766
+ console.error('\n Usage: memoc note "<topic title>" [--body "short note"]');
2767
+ process.exit(1);
2768
+ }
2769
+
2770
+ ensureMemocBase(dir);
2771
+ const topicPath = uniquePath(path.join(dir, '.memoc', 'wiki', 'topics', `${slugify(title, 'topic')}.md`));
2772
+ write(topicPath, topicNote(title, body));
2773
+ ensureMemocFrontmatter(topicPath, dir);
2774
+ addWikiListItem(path.join(dir, '.memoc', 'wiki', 'topics', 'README.md'), 'Topic Pages', path.basename(topicPath), title, 'topic note');
2775
+ addWikiListItem(path.join(dir, '.memoc', 'wiki', 'index.md'), 'Saved Queries', pathRelativeMarkdown(path.join(dir, '.memoc', 'wiki'), topicPath), title, 'saved query/topic note');
2776
+ console.log('\n memoc note\n');
2777
+ console.log(` Topic ${normRel(dir, topicPath)}`);
2778
+ console.log(' Next Link related sources/topics, then run memoc lint-wiki.');
2779
+ console.log();
2780
+ }
2781
+
2782
+ function ensureMemocBase(dir) {
2783
+ const p = scanProject(dir);
2784
+ const memDir = path.join(dir, '.memoc');
2785
+ const files = [
2786
+ [path.join(memDir, 'wiki/index.md'), tplWikiIndex],
2787
+ [path.join(memDir, 'wiki/sources.md'), tplWikiSources],
2788
+ [path.join(memDir, 'wiki/sources/README.md'), tplWikiSourcesReadme],
2789
+ [path.join(memDir, 'wiki/topics/README.md'), tplWikiTopicsReadme],
2790
+ [path.join(memDir, 'raw/README.md'), tplRawReadme],
2791
+ [path.join(memDir, 'raw/files/README.md'), tplRawFilesReadme],
2792
+ [path.join(memDir, 'raw/urls/README.md'), tplRawUrlsReadme],
2793
+ [path.join(memDir, 'activity.md'), tplActivity],
2794
+ [path.join(memDir, 'actors/README.md'), tplActorsReadme],
2795
+ [path.join(memDir, 'worklog/README.md'), tplWorklogReadme],
2796
+ [path.join(memDir, '00-agent-index.md'), () => tplAgentIndex(p)],
2797
+ ];
2798
+ for (const [fp, tpl] of files) ensure(fp, tpl());
2799
+ ensureObsidianFrontmatter(dir, () => {});
2800
+ }
2801
+
2802
+ function ingestTitle(dir, target, isUrl) {
2803
+ if (isUrl) {
2804
+ try {
2805
+ const u = new URL(target);
2806
+ return path.basename(u.pathname) || u.hostname;
2807
+ } catch {
2808
+ return target;
2809
+ }
2810
+ }
2811
+ try {
2812
+ const src = fs.readFileSync(path.resolve(dir, target), 'utf8');
2813
+ return markdownTitle(src, path.basename(target, path.extname(target)));
2814
+ } catch {
2815
+ return path.basename(target, path.extname(target));
2816
+ }
2817
+ }
2818
+
2819
+ function rawUrlRecord(title, url) {
2820
+ return `# ${title}
2821
+
2822
+ Original URL: ${url}
2823
+
2824
+ This raw URL record stores provenance only. Summarize it in a source record before using it as durable project knowledge.
2825
+ `;
2826
+ }
2827
+
2828
+ function sourceRecord(title, rawRef, original, isUrl) {
2829
+ return `# ${title}
2830
+
2831
+ ## Source
2832
+
2833
+ - Raw: [${isUrl ? 'URL record' : 'raw file'}](${rawRef})
2834
+ - Original: ${original}
2835
+ - Ingested: ${nowISO()}
2836
+
2837
+ ## Summary
2838
+
2839
+ _Summarize only the durable facts future agents should reuse._
2840
+
2841
+ ## Affects
2842
+
2843
+ - [Sources](../sources.md)
2844
+ - [Topics](../topics/README.md)
2845
+ - [Open Questions](../questions.md)
2846
+
2847
+ ## Synthesis Tasks
2848
+
2849
+ - [ ] Create or update affected topic/global/system pages.
2850
+ - [ ] Link those pages back to this source when provenance matters.
2851
+ - [ ] Run \`memoc lint-wiki\`.
2852
+
2853
+ ## Related
2854
+
2855
+ - [Sources Index](../sources.md)
2856
+ - [Source Records](README.md)
2857
+ - [Wiki Index](../index.md)
2858
+ `;
2859
+ }
2860
+
2861
+ function topicNote(title, body) {
2862
+ return `# ${title}
2863
+
2864
+ ## Summary
2865
+
2866
+ ${body ? `- ${body}` : '_Capture the durable answer, analysis, or query result here._'}
2867
+
2868
+ ## Evidence
2869
+
2870
+ - [Sources](../sources.md)
2871
+
2872
+ ## Open Questions
2873
+
2874
+ _None yet._
2875
+
2876
+ ## Related
2877
+
2878
+ - [Wiki Index](../index.md)
2879
+ - [Topics](README.md)
2880
+ - [Glossary](../glossary.md)
2881
+ `;
2882
+ }
2883
+
2884
+ function listMarkdownFiles(root) {
2885
+ const files = [];
2886
+ function walk(d) {
2887
+ if (!fs.existsSync(d)) return;
2888
+ for (const entry of fs.readdirSync(d)) {
2889
+ const fp = path.join(d, entry);
2890
+ try {
2891
+ const st = fs.statSync(fp);
2892
+ if (st.isDirectory()) walk(fp);
2893
+ else if (entry.endsWith('.md')) files.push(fp);
2894
+ } catch {}
2895
+ }
2896
+ }
2897
+ walk(root);
2898
+ return files.sort();
2899
+ }
2900
+
2901
+ function safeRead(fp) {
2902
+ try { return fs.readFileSync(fp, 'utf8'); } catch { return ''; }
2903
+ }
2904
+
2905
+ function normRel(dir, fp) {
2906
+ return path.relative(dir, fp).replace(/\\/g, '/');
2907
+ }
2908
+
2909
+ function pathRelativeMarkdown(fromDir, toFile) {
2910
+ let rel = path.relative(fromDir, toFile).replace(/\\/g, '/');
2911
+ if (!rel.startsWith('.')) rel = `./${rel}`;
2912
+ return rel;
2913
+ }
2914
+
2915
+ function markdownLinks(src) {
2916
+ const links = [];
2917
+ const re = /!?\[[^\]]*]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
2918
+ let m;
2919
+ while ((m = re.exec(src))) links.push(m[1]);
2920
+ return links;
2921
+ }
2922
+
2923
+ function resolveMarkdownLink(fromFile, link) {
2924
+ const clean = decodeURIComponent(String(link).split('#')[0]);
2925
+ if (!clean) return null;
2926
+ const base = path.resolve(path.dirname(fromFile), clean);
2927
+ if (path.extname(base)) return base;
2928
+ if (fs.existsSync(`${base}.md`)) return `${base}.md`;
2929
+ return base;
2930
+ }
2931
+
2932
+ function addWikiListItem(filePath, heading, link, title, note) {
2933
+ const src = safeRead(filePath);
2934
+ if (!src) return;
2935
+ if (src.includes(`](${link})`) || src.includes(`](${link.replace(/^\.\//, '')})`)) return;
2936
+ const item = `- [${title}](${link.replace(/^\.\//, '')}) — ${note}.`;
2937
+ const re = new RegExp(`(## ${escapeRegExp(heading)}\\n)([\\s\\S]*?)(?=\\n## |$)`, 'm');
2938
+ const m = src.match(re);
2939
+ if (!m) {
2940
+ write(filePath, `${src.trimEnd()}\n\n## ${heading}\n\n${item}\n`);
2941
+ return;
2942
+ }
2943
+ const replacementBody = m[2].includes('_None yet') || m[2].includes('_No sources recorded yet')
2944
+ ? `\n${item}\n`
2945
+ : `${m[2].trimEnd()}\n${item}\n`;
2946
+ write(filePath, src.replace(re, `$1${replacementBody}`));
2947
+ }
2948
+
1811
2949
  // ═══════════════════════════════════════════════════════════════════
1812
2950
  // SEARCH
1813
2951
  // ═══════════════════════════════════════════════════════════════════
@@ -1971,6 +3109,8 @@ function shouldSkipSearchDir(name, scope = 'memory') {
1971
3109
  skipped.add('.memoc');
1972
3110
  skipped.add('skills');
1973
3111
  skipped.add('.claude');
3112
+ } else {
3113
+ skipped.add('raw');
1974
3114
  }
1975
3115
  return skipped.has(name);
1976
3116
  }
@@ -2015,7 +3155,7 @@ function searchPriority(file, scope = 'memory') {
2015
3155
  '.memoc/04-handoff.md',
2016
3156
  '.memoc/06-project-rules.md',
2017
3157
  '.memoc/03-decisions.md',
2018
- '.memoc/log.md',
3158
+ '.memoc/activity.md',
2019
3159
  'AGENTS.md',
2020
3160
  'CLAUDE.md',
2021
3161
  'llms.txt',
@@ -2049,7 +3189,7 @@ function runTokens(dir) {
2049
3189
  ['03-decisions.md', path.join(memDir, '03-decisions.md')],
2050
3190
  ['04-handoff.md', path.join(memDir, '04-handoff.md')],
2051
3191
  ['06-project-rules.md', path.join(memDir, '06-project-rules.md')],
2052
- ['log.md', path.join(memDir, 'log.md')],
3192
+ ['activity.md', path.join(memDir, 'activity.md')],
2053
3193
  ];
2054
3194
 
2055
3195
  console.log('\n memoc tokens\n');
@@ -2082,13 +3222,62 @@ function runTokens(dir) {
2082
3222
  const summaryContent = read(path.join(memDir, 'session-summary.md'));
2083
3223
  const summaryBytes = Buffer.byteLength(summaryContent, 'utf8');
2084
3224
  if (summaryBytes > 800) {
2085
- console.log(`\n ⚠ session-summary.md is ${summaryBytes}B — recommended <800B. Trim it manually.`);
3225
+ console.log(`\n ⚠ session-summary.md is ${summaryBytes}B — recommended <800B. Run \`memoc trim-summary\`, then move completed history to worklog and resume details to 04-handoff.md.`);
3226
+ }
3227
+ console.log();
3228
+ }
3229
+
3230
+ // ═══════════════════════════════════════════════════════════════════
3231
+ // DOCTOR — quick health checks for shared memoc repos
3232
+ // ═══════════════════════════════════════════════════════════════════
3233
+
3234
+ function runDoctor(dir) {
3235
+ const issues = [];
3236
+ const warnings = [];
3237
+ const memDir = path.join(dir, '.memoc');
3238
+ const summaryPath = path.join(memDir, 'session-summary.md');
3239
+ const summary = safeRead(summaryPath);
3240
+ if (!summary) issues.push('Missing .memoc/session-summary.md');
3241
+ else if (Buffer.byteLength(summary, 'utf8') > 800) warnings.push('session-summary.md exceeds 800B; run memoc trim-summary');
3242
+
3243
+ for (const fp of [
3244
+ path.join(dir, '.memoc', 'bin', 'memoc'),
3245
+ path.join(dir, '.memoc', 'bin', 'memoc.cmd'),
3246
+ path.join(dir, '.memoc', 'bin', 'memoc.ps1'),
3247
+ ]) {
3248
+ const src = safeRead(fp);
3249
+ if (src && (/C:\\Users\\|\/Users\/[^/]+\/\.local\/share\/memoc\/runtime/.test(src))) {
3250
+ issues.push(`${normRel(dir, fp)} contains a user-specific runtime path; run memoc upgrade`);
3251
+ }
3252
+ }
3253
+
3254
+ for (const fp of collectMemocMarkdownFiles(dir)) {
3255
+ const src = safeRead(fp);
3256
+ const fenceCount = (src.match(/^---$/gm) || []).length;
3257
+ if (fenceCount > 2) warnings.push(`${normRel(dir, fp)} may have nested frontmatter; run memoc upgrade`);
3258
+ }
3259
+
3260
+ const actor = detectActor(dir);
3261
+ if (actor.actor === 'unknown') warnings.push('Actor could not be detected; run memoc actor set <name>');
3262
+
3263
+ console.log('\n memoc doctor\n');
3264
+ console.log(` Actor ${actor.actor} (${actor.source})`);
3265
+ console.log(` Issues ${issues.length}`);
3266
+ console.log(` Warnings ${warnings.length}`);
3267
+ if (issues.length) {
3268
+ console.log('\n Issues:');
3269
+ for (const issue of issues) console.log(` - ${issue}`);
3270
+ }
3271
+ if (warnings.length) {
3272
+ console.log('\n Warnings:');
3273
+ for (const warning of warnings.slice(0, 20)) console.log(` - ${warning}`);
2086
3274
  }
3275
+ if (!issues.length && !warnings.length) console.log('\n Looks good.');
2087
3276
  console.log();
2088
3277
  }
2089
3278
 
2090
3279
  // ═══════════════════════════════════════════════════════════════════
2091
- // COMPRESS — archive old log.md entries to keep file small
3280
+ // COMPRESS — legacy log.md archiver
2092
3281
  // ═══════════════════════════════════════════════════════════════════
2093
3282
 
2094
3283
  function runCompress(dir) {
@@ -2124,6 +3313,7 @@ function runCompress(dir) {
2124
3313
  write(logPath, header.trimEnd() + '\n' + toKeep.join('') + '\n');
2125
3314
 
2126
3315
  console.log(`\n memoc compress\n`);
3316
+ console.log(' Legacy command: new activity should use .memoc/worklog/ instead of log.md.');
2127
3317
  console.log(` Archived ${toArchive.length} entries → .memoc/log-archive.md`);
2128
3318
  console.log(` Kept ${toKeep.length} recent entries in log.md`);
2129
3319
  const saved = Buffer.byteLength(toArchive.join(''), 'utf8');
@@ -2131,6 +3321,91 @@ function runCompress(dir) {
2131
3321
  console.log('\n Done.\n');
2132
3322
  }
2133
3323
 
3324
+ // ═══════════════════════════════════════════════════════════════════
3325
+ // TRIM SUMMARY — keep startup memory small and move bulky text aside
3326
+ // ═══════════════════════════════════════════════════════════════════
3327
+
3328
+ function runTrimSummary(dir) {
3329
+ const summaryPath = path.join(dir, '.memoc', 'session-summary.md');
3330
+ const archivePath = path.join(dir, '.memoc', 'session-summary-archive.md');
3331
+ if (!fs.existsSync(summaryPath)) {
3332
+ write(summaryPath, tplSessionSummary());
3333
+ console.log('\n memoc trim-summary\n');
3334
+ console.log(' Added .memoc/session-summary.md');
3335
+ console.log('\n Done.\n');
3336
+ return;
3337
+ }
3338
+
3339
+ const src = fs.readFileSync(summaryPath, 'utf8');
3340
+ const beforeBytes = Buffer.byteLength(src, 'utf8');
3341
+ const compact = compactSessionSummary(src);
3342
+ const afterBytes = Buffer.byteLength(compact, 'utf8');
3343
+
3344
+ if (src === compact && beforeBytes <= 800) {
3345
+ console.log('\n memoc trim-summary\n');
3346
+ console.log(` session-summary.md is already compact (${beforeBytes}B).`);
3347
+ console.log('\n Done.\n');
3348
+ return;
3349
+ }
3350
+
3351
+ const archiveHeader = fs.existsSync(archivePath)
3352
+ ? ''
3353
+ : '# Session Summary Archive\n\nOlder oversized startup summaries moved by `memoc trim-summary`.\n';
3354
+ fs.appendFileSync(archivePath, `${archiveHeader}\n## [${nowISO()}] archived summary (${beforeBytes}B)\n\n${src.trimEnd()}\n`, 'utf8');
3355
+ write(summaryPath, compact);
3356
+
3357
+ console.log('\n memoc trim-summary\n');
3358
+ console.log(` Archived .memoc/session-summary-archive.md`);
3359
+ console.log(` Rewrote .memoc/session-summary.md (${beforeBytes}B → ${afterBytes}B)`);
3360
+ console.log(' Reminder Completed history belongs in worklog; resume details belong in 04-handoff.md.');
3361
+ console.log('\n Done.\n');
3362
+ }
3363
+
3364
+ function compactSessionSummary(src) {
3365
+ const sections = ['Status', 'Changed', 'Open Tasks', 'Resume'];
3366
+ const lines = [
3367
+ '# Session Summary',
3368
+ `Last: ${nowISO()}`,
3369
+ 'Replace this file instead of appending to it. Keep total size <800B and each section ≤3 bullets.',
3370
+ 'Completed history belongs in actor worklogs; incomplete/risky resume detail belongs in `04-handoff.md`.',
3371
+ '',
3372
+ ];
3373
+
3374
+ for (const heading of sections) {
3375
+ lines.push(`## ${heading}`);
3376
+ const bullets = compactSummaryBullets(sectionText(src, heading));
3377
+ if (bullets.length) lines.push(...bullets);
3378
+ else lines.push(summaryPlaceholder(heading));
3379
+ lines.push('');
3380
+ }
3381
+
3382
+ return lines.join('\n').trimEnd() + '\n';
3383
+ }
3384
+
3385
+ function sectionText(src, heading) {
3386
+ const re = new RegExp(`(?:^|\\n)## ${escapeRegExp(heading)}\\n([\\s\\S]*?)(?=\\n## |$)`);
3387
+ const m = String(src || '').match(re);
3388
+ return m ? m[1].trim() : '';
3389
+ }
3390
+
3391
+ function compactSummaryBullets(text) {
3392
+ return String(text || '')
3393
+ .split(/\r?\n/)
3394
+ .map(line => line.trim())
3395
+ .filter(line => line && !line.startsWith('#') && !/^_.*_$/.test(line))
3396
+ .map(line => line.replace(/^[-*]\s+/, '').replace(/^\d+[.)]\s+/, '').trim())
3397
+ .filter(Boolean)
3398
+ .slice(0, 3)
3399
+ .map(line => `- ${line.length > 140 ? `${line.slice(0, 137)}...` : line}`);
3400
+ }
3401
+
3402
+ function summaryPlaceholder(heading) {
3403
+ if (heading === 'Status') return '_Current state in 1-3 bullets._';
3404
+ if (heading === 'Changed') return '_Recent durable changes only._';
3405
+ if (heading === 'Open Tasks') return '_Current open tasks only._';
3406
+ return '_Where the next agent should resume._';
3407
+ }
3408
+
2134
3409
  // ═══════════════════════════════════════════════════════════════════
2135
3410
  // SUMMARY
2136
3411
  // ═══════════════════════════════════════════════════════════════════
@@ -2147,7 +3422,7 @@ function runSummary(dir) {
2147
3422
  }
2148
3423
 
2149
3424
  function section(src, heading) {
2150
- const re = new RegExp(`^## ${heading}\\n([\\s\\S]*?)(?=\\n## |$)`, 'm');
3425
+ const re = new RegExp(`(?:^|\\n)## ${heading}\\n([\\s\\S]*?)(?=\\n## |$)`);
2151
3426
  const m = src.match(re);
2152
3427
  return m ? m[1].trim() : '';
2153
3428
  }
@@ -2205,10 +3480,18 @@ if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {
2205
3480
  console.log(' upgrade Refresh memoc runtime/wrappers and managed sections; preserve memory');
2206
3481
  console.log(' summary Print a tiny status/resume overview');
2207
3482
  console.log(' tokens Estimate token cost of current memory files');
2208
- console.log(' compress Archive old log.md entries to keep file small');
3483
+ console.log(' trim-summary Archive and compact oversized session-summary.md');
3484
+ console.log(' compress Legacy: archive old log.md entries');
2209
3485
  console.log(' add <agent> Add entry file for a specific agent (run without args to list)');
3486
+ console.log(' actor [set <name>] Show or set the local memoc actor');
3487
+ console.log(' work "<title>" Create a conflict-light actor worklog entry');
3488
+ console.log(' activity List recent memoc worklog entries');
3489
+ console.log(' doctor Check common memoc health issues');
2210
3490
  console.log(' search "<query>" Search memory/agent docs (use --snippets for line matches)');
2211
3491
  console.log(' grep "<query>" Search project source/text files (use --snippets for line matches)');
3492
+ console.log(' ingest <path|url> Create a raw/source record scaffold for wiki synthesis');
3493
+ console.log(' note "<title>" Save a durable topic/query-result scaffold');
3494
+ console.log(' lint-wiki Check wiki links, tags, backlinks, and Related sections');
2212
3495
  console.log('\nSearch flags:');
2213
3496
  console.log(' --files Show file names and match counts, sorted by relevance + recency (default)');
2214
3497
  console.log(' --snippets Show matching lines');
@@ -2224,10 +3507,18 @@ if (cmd === 'update') { run(cwd, true, 'update'); process.exit(0); }
2224
3507
  if (cmd === 'upgrade') { run(cwd, true, 'upgrade'); process.exit(0); }
2225
3508
  if (cmd === 'summary') { runSummary(cwd); process.exit(0); }
2226
3509
  if (cmd === 'tokens') { runTokens(cwd); process.exit(0); }
3510
+ if (cmd === 'trim-summary') { runTrimSummary(cwd); process.exit(0); }
2227
3511
  if (cmd === 'compress') { runCompress(cwd); process.exit(0); }
2228
3512
  if (cmd === 'add') { runAdd(cwd); process.exit(0); }
3513
+ if (cmd === 'actor') { runActor(cwd); process.exit(0); }
3514
+ if (cmd === 'work') { runWork(cwd); process.exit(0); }
3515
+ if (cmd === 'activity') { runActivity(cwd); process.exit(0); }
3516
+ if (cmd === 'doctor') { runDoctor(cwd); process.exit(0); }
2229
3517
  if (cmd === 'search') { runSearch(cwd, 'memory'); process.exit(0); }
2230
3518
  if (cmd === 'grep') { runSearch(cwd, 'project'); process.exit(0); }
3519
+ if (cmd === 'ingest') { runIngest(cwd); process.exit(0); }
3520
+ if (cmd === 'note') { runNote(cwd); process.exit(0); }
3521
+ if (cmd === 'lint-wiki') { runWikiLint(cwd); process.exit(0); }
2231
3522
 
2232
3523
  console.error(`Unknown command: ${cmd}`);
2233
3524
  console.error('Run "memoc --help" for usage.');