@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.
- package/README.md +50 -4
- package/bin/cli.js +1351 -60
- 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
|
|
212
|
-
|
|
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
|
|
216
|
-
|
|
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
|
|
220
|
-
|
|
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
|
-
|
|
382
|
+
ensureRuntimeInstall(mark);
|
|
253
383
|
const files = [
|
|
254
|
-
[path.join(dir, '.memoc', 'bin', 'memoc.cmd'),
|
|
255
|
-
[path.join(dir, '.memoc', 'bin', 'memoc.ps1'),
|
|
256
|
-
[path.join(dir, '.memoc', 'bin', 'memoc'),
|
|
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
|
-
|
|
401
|
+
ensureRuntimeInstall(mark);
|
|
402
|
+
writeLaunchers(userBin, mark, 'user bin');
|
|
272
403
|
return userBin;
|
|
273
404
|
}
|
|
274
405
|
|
|
275
|
-
function writeLaunchers(binDir, mark, label
|
|
406
|
+
function writeLaunchers(binDir, mark, label) {
|
|
276
407
|
const files = [
|
|
277
|
-
[path.join(binDir, 'memoc.cmd'),
|
|
278
|
-
[path.join(binDir, 'memoc.ps1'),
|
|
279
|
-
[path.join(binDir, 'memoc'),
|
|
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
|
-
|
|
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
|
-
- [
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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
|
|
1002
|
-
|
|
|
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\`, \`
|
|
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
|
-
- \`
|
|
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/
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
|
2102
|
+
const entries = ['.memoc/.pending', '.memoc/local/'];
|
|
1556
2103
|
const gitignoreContent = fs.existsSync(gitignorePath)
|
|
1557
2104
|
? fs.readFileSync(gitignorePath, 'utf8') : '';
|
|
1558
|
-
const
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
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 (
|
|
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, '
|
|
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, '
|
|
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
|
-
|
|
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/
|
|
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
|
-
['
|
|
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.
|
|
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 —
|
|
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(
|
|
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('
|
|
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.');
|