obsidian-second-brain 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/config/projects.example.json +13 -0
- package/dist/artifact-frontmatter.js +46 -0
- package/dist/changeset.js +18 -0
- package/dist/classify.js +50 -0
- package/dist/cli-claude-backend.js +40 -0
- package/dist/cli.js +337 -0
- package/dist/config.js +208 -0
- package/dist/consolidation-backend.js +86 -0
- package/dist/consolidation.js +321 -0
- package/dist/episode-file.js +61 -0
- package/dist/episode-patch.js +28 -0
- package/dist/episode-prompt.js +43 -0
- package/dist/filesystem.js +86 -0
- package/dist/git.js +61 -0
- package/dist/ingest-writer.js +86 -0
- package/dist/ingest.js +217 -0
- package/dist/init.js +343 -0
- package/dist/install-manifest.js +56 -0
- package/dist/lock.js +73 -0
- package/dist/logger.js +19 -0
- package/dist/managed-section.js +30 -0
- package/dist/manifest.js +64 -0
- package/dist/ollama-backend.js +49 -0
- package/dist/plist.js +23 -0
- package/dist/render-claude-jsonl.js +179 -0
- package/dist/render-jsonl-markdown.js +116 -0
- package/dist/report.js +244 -0
- package/dist/shell.js +84 -0
- package/dist/slug.js +16 -0
- package/dist/sync.js +103 -0
- package/dist/synthesis.js +14 -0
- package/dist/uninstall.js +80 -0
- package/package.json +44 -0
- package/templates/claude-md-section.md +12 -0
- package/templates/launchd-weekly.plist.template +35 -0
- package/templates/launchd.plist.template +28 -0
- package/templates/vault-agents.md +124 -0
- package/templates/vault-claude-md.md +1 -0
- package/templates/vault-gitignore +12 -0
- package/templates/wiki-index.md +7 -0
- package/templates/wiki-log.md +1 -0
- package/templates/wrap-command.md +99 -0
package/dist/sync.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { copyFile, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import { hashRenderedBody } from "./changeset.js";
|
|
4
|
+
import { cleanupEmptyDirectories, collectFiles, ensureDirectory, ensureWritableFilePath, removePathEntry } from "./filesystem.js";
|
|
5
|
+
import { renderClaudeJsonlToMarkdown } from "./render-claude-jsonl.js";
|
|
6
|
+
import { renderJsonlToMarkdown } from "./render-jsonl-markdown.js";
|
|
7
|
+
export async function syncProviderTree(input) {
|
|
8
|
+
await ensureDirectory(input.destinationRoot);
|
|
9
|
+
const sourceFiles = await collectFiles(input.sourceRoot);
|
|
10
|
+
const expectedDestinationFiles = collectExpectedDestinationFiles(sourceFiles);
|
|
11
|
+
const renderedFiles = [];
|
|
12
|
+
let copiedFiles = 0;
|
|
13
|
+
let renderedMarkdownFiles = 0;
|
|
14
|
+
for (const sourceFile of sourceFiles) {
|
|
15
|
+
const destinationRelativePath = mapSourceToDestinationPath(sourceFile.relativePath);
|
|
16
|
+
const destinationPath = join(input.destinationRoot, destinationRelativePath);
|
|
17
|
+
await ensureDirectory(dirname(destinationPath));
|
|
18
|
+
await ensureWritableFilePath(destinationPath);
|
|
19
|
+
if (sourceFile.relativePath.endsWith(".jsonl")) {
|
|
20
|
+
const sourceContent = await readFile(sourceFile.absolutePath, "utf8");
|
|
21
|
+
const rendered = input.providerName === "claude"
|
|
22
|
+
? renderClaudeJsonlToMarkdown({
|
|
23
|
+
generatedAt: input.generatedAt,
|
|
24
|
+
relativePath: sourceFile.relativePath,
|
|
25
|
+
sourceContent
|
|
26
|
+
})
|
|
27
|
+
: renderJsonlToMarkdown({
|
|
28
|
+
generatedAt: input.generatedAt,
|
|
29
|
+
providerName: input.providerName,
|
|
30
|
+
relativePath: sourceFile.relativePath,
|
|
31
|
+
sourceContent
|
|
32
|
+
});
|
|
33
|
+
renderedFiles.push(toRenderedFile({
|
|
34
|
+
destinationPath,
|
|
35
|
+
markdown: rendered.markdown,
|
|
36
|
+
metadata: rendered.metadata,
|
|
37
|
+
providerName: input.providerName,
|
|
38
|
+
relativePath: sourceFile.relativePath,
|
|
39
|
+
sourcePath: sourceFile.absolutePath
|
|
40
|
+
}));
|
|
41
|
+
await writeFile(destinationPath, rendered.markdown);
|
|
42
|
+
renderedMarkdownFiles += 1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
await copyFile(sourceFile.absolutePath, destinationPath);
|
|
46
|
+
copiedFiles += 1;
|
|
47
|
+
}
|
|
48
|
+
const destinationFiles = await collectFiles(input.destinationRoot);
|
|
49
|
+
let deletedFiles = 0;
|
|
50
|
+
for (const destinationFile of destinationFiles) {
|
|
51
|
+
if (expectedDestinationFiles.has(destinationFile.relativePath)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
await removePathEntry(destinationFile.absolutePath);
|
|
55
|
+
deletedFiles += 1;
|
|
56
|
+
}
|
|
57
|
+
await cleanupEmptyDirectories(input.destinationRoot);
|
|
58
|
+
return {
|
|
59
|
+
copiedFiles,
|
|
60
|
+
deletedFiles,
|
|
61
|
+
renderedFiles,
|
|
62
|
+
renderedMarkdownFiles
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function toRenderedFile(input) {
|
|
66
|
+
// The relative path is the only per-file-unique identity: subagent
|
|
67
|
+
// transcripts carry the PARENT sessionId in their events (611 of 686 real
|
|
68
|
+
// files) and workflow journals all share the basename journal.jsonl, so
|
|
69
|
+
// neither field converges as a key. Transcripts are track-only (no
|
|
70
|
+
// synthesis), so a rename merely re-tracks the file at zero cost.
|
|
71
|
+
const sessionId = input.relativePath.replace(/\.jsonl$/u, "");
|
|
72
|
+
const segments = input.relativePath.split(/[\\/]/u);
|
|
73
|
+
const firstSegment = segments.length > 1 ? segments[0] : undefined;
|
|
74
|
+
const project = input.metadata.cwd
|
|
75
|
+
? basename(input.metadata.cwd)
|
|
76
|
+
: firstSegment ?? "unknown";
|
|
77
|
+
return {
|
|
78
|
+
contentHash: hashRenderedBody(input.markdown),
|
|
79
|
+
destinationPath: input.destinationPath,
|
|
80
|
+
project,
|
|
81
|
+
providerName: input.providerName,
|
|
82
|
+
sessionId,
|
|
83
|
+
sessionStartedAt: input.metadata.sessionStartedAt,
|
|
84
|
+
sourcePath: input.sourcePath
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function mapSourceToDestinationPath(relativePath) {
|
|
88
|
+
if (relativePath.endsWith(".jsonl")) {
|
|
89
|
+
return relativePath.replace(/\.jsonl$/u, ".md");
|
|
90
|
+
}
|
|
91
|
+
return relativePath;
|
|
92
|
+
}
|
|
93
|
+
function collectExpectedDestinationFiles(sourceFiles) {
|
|
94
|
+
const expectedDestinationFiles = new Set();
|
|
95
|
+
for (const sourceFile of sourceFiles) {
|
|
96
|
+
const destinationRelativePath = mapSourceToDestinationPath(sourceFile.relativePath);
|
|
97
|
+
if (expectedDestinationFiles.has(destinationRelativePath)) {
|
|
98
|
+
throw new Error(`Destination path collision: ${destinationRelativePath}`);
|
|
99
|
+
}
|
|
100
|
+
expectedDestinationFiles.add(destinationRelativePath);
|
|
101
|
+
}
|
|
102
|
+
return expectedDestinationFiles;
|
|
103
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const noopBackend = {
|
|
2
|
+
name: "noop",
|
|
3
|
+
synthesize: async () => ({ episodes: [] })
|
|
4
|
+
};
|
|
5
|
+
export function resolveBackend(name, backends = {}) {
|
|
6
|
+
if (name === "noop") {
|
|
7
|
+
return noopBackend;
|
|
8
|
+
}
|
|
9
|
+
const backend = backends[name];
|
|
10
|
+
if (backend !== undefined) {
|
|
11
|
+
return backend;
|
|
12
|
+
}
|
|
13
|
+
throw new Error(`Unknown synthesis backend: ${name} (available: noop, cli-claude, ollama)`);
|
|
14
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { writeFileAtomic } from "./filesystem.js";
|
|
4
|
+
import { HOURLY_LABEL, installManifestPath, WEEKLY_LABEL } from "./init.js";
|
|
5
|
+
import { isUnchangedInstalledFile, loadInstallManifest } from "./install-manifest.js";
|
|
6
|
+
import { hasManagedSection, removeManagedSection } from "./managed-section.js";
|
|
7
|
+
import { removeLaunchdJob } from "./plist.js";
|
|
8
|
+
export async function runUninstall(options, dependencies) {
|
|
9
|
+
const manifest = await loadInstallManifest(installManifestPath(dependencies.appDir));
|
|
10
|
+
const removed = [];
|
|
11
|
+
const warnings = [];
|
|
12
|
+
// 1. launchd jobs: bootout always; the plist files go through the same
|
|
13
|
+
// manifest-unchanged rule as every other generated file.
|
|
14
|
+
for (const label of [HOURLY_LABEL, WEEKLY_LABEL]) {
|
|
15
|
+
await removeLaunchdJob({
|
|
16
|
+
exec: dependencies.exec,
|
|
17
|
+
label,
|
|
18
|
+
launchAgentsDir: dependencies.launchAgentsDir,
|
|
19
|
+
removeFile: async (path) => {
|
|
20
|
+
await removeIfUnchanged(path, manifest, removed, warnings);
|
|
21
|
+
},
|
|
22
|
+
uid: dependencies.uid
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
// 2. Managed CLAUDE.md section (by markers — the rest of the file is the
|
|
26
|
+
// user's).
|
|
27
|
+
await removeSectionFromClaudeMd(join(options.claudeDir, "CLAUDE.md"), removed, warnings);
|
|
28
|
+
// 3. Every other manifest-listed generated file (wrap command, config).
|
|
29
|
+
for (const filePath of Object.keys(manifest.files)) {
|
|
30
|
+
if (removed.includes(filePath)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
await removeIfUnchanged(filePath, manifest, removed, warnings);
|
|
34
|
+
}
|
|
35
|
+
// 4. The install manifest itself. Vault content (raw/, wiki/, .git) is
|
|
36
|
+
// deliberately retained — it is the user's data and audit trail.
|
|
37
|
+
await rm(installManifestPath(dependencies.appDir), { force: true });
|
|
38
|
+
for (const warning of warnings) {
|
|
39
|
+
dependencies.log(`warning: ${warning}`);
|
|
40
|
+
}
|
|
41
|
+
return { removed, warnings };
|
|
42
|
+
}
|
|
43
|
+
async function removeIfUnchanged(filePath, manifest, removed, warnings) {
|
|
44
|
+
if (manifest.files[filePath] === undefined) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (await isUnchangedInstalledFile(manifest, filePath)) {
|
|
48
|
+
await rm(filePath, { force: true });
|
|
49
|
+
removed.push(filePath);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
await readFile(filePath, "utf8");
|
|
54
|
+
warnings.push(`kept ${filePath} — modified since install`);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Already gone; nothing to do.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function removeSectionFromClaudeMd(claudeMdPath, removed, warnings) {
|
|
61
|
+
let existing;
|
|
62
|
+
try {
|
|
63
|
+
existing = await readFile(claudeMdPath, "utf8");
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (!hasManagedSection(existing)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const next = removeManagedSection(existing);
|
|
72
|
+
if (next.length === 0) {
|
|
73
|
+
await rm(claudeMdPath, { force: true });
|
|
74
|
+
removed.push(claudeMdPath);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
await writeFileAtomic(claudeMdPath, next);
|
|
78
|
+
removed.push(`${claudeMdPath} (managed section)`);
|
|
79
|
+
warnings.push(`${claudeMdPath} kept — only the managed section was removed`);
|
|
80
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "obsidian-second-brain",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Turn an Obsidian vault into a self-maintaining second brain for AI coding sessions",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"templates",
|
|
9
|
+
"config/projects.example.json"
|
|
10
|
+
],
|
|
11
|
+
"author": "Minh Nhat Hoang <hoangminhnhat08@gmail.com>",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/minhnhat08/second-brain.git"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/minhnhat08/second-brain#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/minhnhat08/second-brain/issues"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"bin": {
|
|
22
|
+
"second-brain": "dist/cli.js"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc -p tsconfig.build.json",
|
|
26
|
+
"prepare": "npm run build",
|
|
27
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
28
|
+
"sync": "tsx src/cli.ts",
|
|
29
|
+
"test": "tsx --test tests/**/*.test.ts",
|
|
30
|
+
"coverage": "tsx --test --experimental-test-coverage tests/**/*.test.ts",
|
|
31
|
+
"e2e": "npm run build && tsx --test tests/e2e/cli.e2e.ts tests/e2e/lifecycle.e2e.ts tests/e2e/claude-smoke.e2e.ts"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"zod": "^4.1.12"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^24.6.0",
|
|
38
|
+
"tsx": "^4.20.6",
|
|
39
|
+
"typescript": "^5.9.3"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!-- BEGIN second-brain (managed by second-brain init — do not edit inside) -->
|
|
2
|
+
At the end of any significant task, offer /wrap. /wrap writes
|
|
3
|
+
{{VAULT}}/raw/artifacts/<project>/YYYY-MM-DD-<task>.<session_id>.{plan,output}.md
|
|
4
|
+
with frontmatter (project, task, type, session_id, date).
|
|
5
|
+
|
|
6
|
+
Second-brain retrieval: when the repository alone cannot answer — prior
|
|
7
|
+
decisions and their rationale, how projects relate, "did a past session
|
|
8
|
+
already solve this" — read {{VAULT}}/wiki/index.md and follow only the
|
|
9
|
+
relevant links (entity pages in wiki/, recent activity in wiki/episodes/
|
|
10
|
+
and wiki/reports/). The wiki records history; the repository is the source
|
|
11
|
+
of truth for current implementation — when they disagree, trust the code.
|
|
12
|
+
<!-- END second-brain -->
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>Label</key>
|
|
6
|
+
<string>com.second-brain.weekly</string>
|
|
7
|
+
<key>ProgramArguments</key>
|
|
8
|
+
<array>
|
|
9
|
+
<string>{{NODE_PATH}}</string>
|
|
10
|
+
<string>{{BIN_PATH}}</string>
|
|
11
|
+
<string>sync</string>
|
|
12
|
+
<string>--all</string>
|
|
13
|
+
<string>--consolidate</string>
|
|
14
|
+
<string>--run</string>
|
|
15
|
+
<string>weekly</string>
|
|
16
|
+
</array>
|
|
17
|
+
<key>WorkingDirectory</key>
|
|
18
|
+
<string>{{APP_DIR}}</string>
|
|
19
|
+
<key>StartCalendarInterval</key>
|
|
20
|
+
<dict>
|
|
21
|
+
<key>Weekday</key>
|
|
22
|
+
<integer>0</integer>
|
|
23
|
+
<key>Hour</key>
|
|
24
|
+
<integer>9</integer>
|
|
25
|
+
<key>Minute</key>
|
|
26
|
+
<integer>0</integer>
|
|
27
|
+
</dict>
|
|
28
|
+
<key>RunAtLoad</key>
|
|
29
|
+
<false/>
|
|
30
|
+
<key>StandardOutPath</key>
|
|
31
|
+
<string>/tmp/second-brain-weekly.stdout.log</string>
|
|
32
|
+
<key>StandardErrorPath</key>
|
|
33
|
+
<string>/tmp/second-brain-weekly.stderr.log</string>
|
|
34
|
+
</dict>
|
|
35
|
+
</plist>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>Label</key>
|
|
6
|
+
<string>com.second-brain.hourly</string>
|
|
7
|
+
<key>ProgramArguments</key>
|
|
8
|
+
<array>
|
|
9
|
+
<string>{{NODE_PATH}}</string>
|
|
10
|
+
<string>{{BIN_PATH}}</string>
|
|
11
|
+
<string>sync</string>
|
|
12
|
+
<string>--all</string>
|
|
13
|
+
<string>--ingest</string>
|
|
14
|
+
<string>--run</string>
|
|
15
|
+
<string>hourly</string>
|
|
16
|
+
</array>
|
|
17
|
+
<key>WorkingDirectory</key>
|
|
18
|
+
<string>{{APP_DIR}}</string>
|
|
19
|
+
<key>StartInterval</key>
|
|
20
|
+
<integer>3600</integer>
|
|
21
|
+
<key>RunAtLoad</key>
|
|
22
|
+
<true/>
|
|
23
|
+
<key>StandardOutPath</key>
|
|
24
|
+
<string>/tmp/second-brain.stdout.log</string>
|
|
25
|
+
<key>StandardErrorPath</key>
|
|
26
|
+
<string>/tmp/second-brain.stderr.log</string>
|
|
27
|
+
</dict>
|
|
28
|
+
</plist>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# LLM Wiki Schema
|
|
2
|
+
|
|
3
|
+
This is a personal knowledge base maintained by an LLM. The human curates sources and directs analysis. The LLM handles all writing, cross-referencing, and maintenance.
|
|
4
|
+
|
|
5
|
+
## Directory Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
raw/ # Immutable source documents
|
|
9
|
+
raw/projects/sessions/ # Mirrored AI session transcripts — machine-managed archive
|
|
10
|
+
raw/artifacts/ # Curated task artifacts written by /wrap (plan + output per task)
|
|
11
|
+
wiki/ # LLM-generated and maintained markdown pages
|
|
12
|
+
wiki/index.md # Content catalog — entity/concept pages listed with summaries
|
|
13
|
+
wiki/log.md # Chronological record of all operations
|
|
14
|
+
wiki/episodes/ # Quarantined episodic notes from the automated hourly ingest
|
|
15
|
+
wiki/.ingest-state.json # Ingest manifest (idempotency watermark) — pipeline-owned, never hand-edit
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Conventions
|
|
19
|
+
|
|
20
|
+
- All wiki pages use `[[wikilinks]]` for cross-references (Obsidian style)
|
|
21
|
+
- Page filenames: kebab-case, descriptive
|
|
22
|
+
- Every wiki page has YAML frontmatter:
|
|
23
|
+
|
|
24
|
+
```yaml
|
|
25
|
+
---
|
|
26
|
+
title: Page Title
|
|
27
|
+
type: entity | concept | source-summary | synthesis | comparison | analysis
|
|
28
|
+
tags: [relevant, tags]
|
|
29
|
+
sources: [source-filename.md]
|
|
30
|
+
created: YYYY-MM-DD
|
|
31
|
+
updated: YYYY-MM-DD
|
|
32
|
+
---
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Page Types
|
|
36
|
+
|
|
37
|
+
- **source-summary**: Summary of a single raw source. One per source file.
|
|
38
|
+
- **entity**: A person, organization, tool, place, or other named thing.
|
|
39
|
+
- **concept**: An idea, theory, methodology, or abstract topic.
|
|
40
|
+
- **synthesis**: A page that draws together multiple sources/concepts into an argument or narrative.
|
|
41
|
+
- **comparison**: Side-by-side analysis of two or more entities/concepts.
|
|
42
|
+
- **analysis**: An answer to a specific question, filed for future reference.
|
|
43
|
+
|
|
44
|
+
## Automated Ingest Rules
|
|
45
|
+
|
|
46
|
+
These rules bind every ingest run — automated (hourly/weekly pipeline) or manual. They exist to prevent silent corruption of trusted pages.
|
|
47
|
+
|
|
48
|
+
### Canonical-name resolution
|
|
49
|
+
|
|
50
|
+
Before creating any entity or concept page, resolve the canonical name first:
|
|
51
|
+
|
|
52
|
+
1. Search `wiki/index.md` and existing page frontmatter for the same real-world thing under alternative spellings, abbreviations, and aliases.
|
|
53
|
+
2. If a page already exists under another name, update that page — never create a duplicate.
|
|
54
|
+
3. New page names: kebab-case, the most specific commonly-used name. Record known aliases in frontmatter (`aliases: [...]`).
|
|
55
|
+
|
|
56
|
+
### Additive edits only
|
|
57
|
+
|
|
58
|
+
- Update pages by adding dated sections or appending to existing sections — never rewrite a page wholesale.
|
|
59
|
+
- Never delete prior content. When new information supersedes old, add the correction, mark the old claim explicitly, and keep both.
|
|
60
|
+
- Episodic notes from the hourly pass land only in `wiki/episodes/` — they never touch trusted entity/concept pages directly. Only the weekly consolidation pass may edit stable pages.
|
|
61
|
+
|
|
62
|
+
### Index scope
|
|
63
|
+
|
|
64
|
+
`wiki/index.md` indexes entity, concept, synthesis, comparison, and analysis pages only. Never index individual session transcripts — they are archive material, reachable from project pages via source links.
|
|
65
|
+
|
|
66
|
+
### Write order (transactional)
|
|
67
|
+
|
|
68
|
+
Every ingest run writes in this exact order:
|
|
69
|
+
|
|
70
|
+
1. Wiki pages (entity/concept/episode pages)
|
|
71
|
+
2. `wiki/index.md`
|
|
72
|
+
3. `wiki/log.md`
|
|
73
|
+
4. `wiki/.ingest-state.json` (manifest — last; advancing it commits the run)
|
|
74
|
+
|
|
75
|
+
A crash before step 4 leaves the run fully replayable. Never hand-edit `.ingest-state.json`.
|
|
76
|
+
|
|
77
|
+
## Workflows
|
|
78
|
+
|
|
79
|
+
### Query
|
|
80
|
+
|
|
81
|
+
When the user asks a question:
|
|
82
|
+
|
|
83
|
+
1. Read `wiki/index.md` to identify relevant pages.
|
|
84
|
+
2. Read the relevant pages.
|
|
85
|
+
3. If needed, read raw sources for additional detail.
|
|
86
|
+
4. Synthesize an answer with `[[wikilinks]]` to referenced pages.
|
|
87
|
+
5. If the answer is substantial and reusable, offer to file it as an `analysis` or `synthesis` page.
|
|
88
|
+
6. If filed, update `wiki/index.md` and append to `wiki/log.md`.
|
|
89
|
+
|
|
90
|
+
### Lint
|
|
91
|
+
|
|
92
|
+
When the user asks to health-check the wiki:
|
|
93
|
+
|
|
94
|
+
1. Check for contradictions between pages.
|
|
95
|
+
2. Find stale claims superseded by newer sources.
|
|
96
|
+
3. Identify orphan pages (no inbound links).
|
|
97
|
+
4. Flag concepts mentioned but lacking their own page.
|
|
98
|
+
5. Suggest missing cross-references.
|
|
99
|
+
6. Report findings and fix issues with user approval.
|
|
100
|
+
7. Append a lint entry to `wiki/log.md`.
|
|
101
|
+
|
|
102
|
+
## Index Format (wiki/index.md)
|
|
103
|
+
|
|
104
|
+
Organized by page type. Each entry: `- [[page-name]] — one-line summary (N sources)`
|
|
105
|
+
|
|
106
|
+
## Log Format (wiki/log.md)
|
|
107
|
+
|
|
108
|
+
Each entry:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
## [YYYY-MM-DD] operation | Subject
|
|
112
|
+
Brief description of what was done.
|
|
113
|
+
Pages created: [[page1]], [[page2]]
|
|
114
|
+
Pages updated: [[page3]]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Guidelines
|
|
118
|
+
|
|
119
|
+
- Never modify files in `raw/`. They are immutable source material.
|
|
120
|
+
- Always maintain bidirectional links — if A links to B, B should link back to A.
|
|
121
|
+
- When new information contradicts existing wiki content, note the contradiction explicitly and cite both sources.
|
|
122
|
+
- Prefer updating existing pages over creating new ones when the topic overlaps.
|
|
123
|
+
- Keep summaries concise. Link to source for full detail.
|
|
124
|
+
- Use Obsidian-compatible markdown (callouts, wikilinks, tags, footnotes).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# OS noise
|
|
2
|
+
.DS_Store
|
|
3
|
+
|
|
4
|
+
# Obsidian volatile state
|
|
5
|
+
.obsidian/workspace*
|
|
6
|
+
|
|
7
|
+
# Obsidian trash
|
|
8
|
+
.trash/
|
|
9
|
+
|
|
10
|
+
# Mirrored session transcripts: machine-managed, regenerable by re-running
|
|
11
|
+
# sync, and rewritten with a fresh generated_at every hourly run.
|
|
12
|
+
raw/projects/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Ingest Log
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Capture the just-finished task into the second-brain vault as curated plan + output artifacts
|
|
3
|
+
argument-hint: [short-task-slug]
|
|
4
|
+
allowed-tools: Read, Write, Bash(git rev-parse:*), Bash(date:*), Bash(ls:*), Bash(test:*), Bash(mkdir:*), Bash(mv:*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# /wrap — capture task artifacts into the vault
|
|
8
|
+
|
|
9
|
+
You just finished a task in this session and still hold its full context. Distill it
|
|
10
|
+
into two small, high-signal artifact files written directly into the Obsidian vault.
|
|
11
|
+
These artifacts are the primary input for the vault's automated wiki ingest — write
|
|
12
|
+
clean, intentional summaries, never a transcript dump.
|
|
13
|
+
|
|
14
|
+
## Fixed install paths
|
|
15
|
+
|
|
16
|
+
- Artifacts root: `{{VAULT}}/raw/artifacts`
|
|
17
|
+
|
|
18
|
+
## Pre-computed context
|
|
19
|
+
|
|
20
|
+
- Project root (git toplevel, falls back to cwd): !`git rev-parse --show-toplevel 2>/dev/null || pwd`
|
|
21
|
+
- Today: !`date +%Y-%m-%d`
|
|
22
|
+
- Session ID: ${CLAUDE_SESSION_ID}
|
|
23
|
+
|
|
24
|
+
## Metadata rules
|
|
25
|
+
|
|
26
|
+
1. `project` = basename of the project root above. If the session is not inside any
|
|
27
|
+
real project directory (home dir, /tmp, filesystem root), use the reserved
|
|
28
|
+
constant `_inbox` — a fixed literal, never derived from repo content. It bypasses
|
|
29
|
+
slugification and is the only segment allowed to start with `_`.
|
|
30
|
+
2. `task` = $ARGUMENTS if non-empty, else a short slug derived from the task just
|
|
31
|
+
completed.
|
|
32
|
+
3. `session_id` = the Session ID above. If it is empty, use the basename (minus
|
|
33
|
+
`.jsonl`) of the most recently modified transcript in
|
|
34
|
+
`~/.claude/projects/<slugified-cwd>/`.
|
|
35
|
+
4. `date` = today (above).
|
|
36
|
+
|
|
37
|
+
## Validation — mandatory before any write
|
|
38
|
+
|
|
39
|
+
Slugify `project` and `task`: lowercase, replace whitespace and `_` with `-`, drop
|
|
40
|
+
every character outside `[a-z0-9-]`, collapse repeated `-`, trim leading/trailing `-`.
|
|
41
|
+
After slugification both MUST match `^[a-z0-9][a-z0-9-]*$`; if either is empty,
|
|
42
|
+
abort and tell the user what failed. Never accept `.`, `..`, `/`, or `\` inside a
|
|
43
|
+
segment — these rules guard against prompt-injected path escapes from repo content.
|
|
44
|
+
Sole exception: the reserved fallback `_inbox` is used verbatim and skips the slug
|
|
45
|
+
pipeline (it is a fixed constant, not agent-derived input).
|
|
46
|
+
|
|
47
|
+
## Path containment — run immediately before each write
|
|
48
|
+
|
|
49
|
+
1. `mkdir -p` the project directory `<artifacts-root>/<project>/` first.
|
|
50
|
+
2. Canonicalize and compare: `root="$(realpath "<artifacts-root>")"` and
|
|
51
|
+
`dir="$(realpath "<artifacts-root>/<project>")"`. Require exactly
|
|
52
|
+
`dir == root + "/" + project`. If `realpath` fails or the prefix differs, abort.
|
|
53
|
+
3. `test -L` every path component from the vault root down to the project directory
|
|
54
|
+
(vault, `raw`, `artifacts`, `<project>`) — refuse if ANY component is a symlink.
|
|
55
|
+
4. Immediately before each Write: the exact `.tmp-` path and the final path must
|
|
56
|
+
both be absent and must not be symlinks (`test -e` / `test -L`). Re-run the
|
|
57
|
+
`test -L` check on the final path right before the `mv`.
|
|
58
|
+
5. These prose checks cannot fully close TOCTOU races; the ingest pipeline
|
|
59
|
+
independently re-validates every path it reads — that is the backstop. On ANY
|
|
60
|
+
anomaly, refuse and report instead of working around it.
|
|
61
|
+
|
|
62
|
+
## Files to write
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
<artifacts-root>/<project>/<date>-<task>.<session_id>.plan.md
|
|
66
|
+
<artifacts-root>/<project>/<date>-<task>.<session_id>.output.md
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Both with this frontmatter (English, no emojis):
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
---
|
|
73
|
+
project: <project>
|
|
74
|
+
task: <one-line human-readable task description>
|
|
75
|
+
type: plan # or: output
|
|
76
|
+
session_id: <session_id>
|
|
77
|
+
date: <date>
|
|
78
|
+
---
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
- `plan.md` — the intent: what was asked, the approach chosen, decisions made and
|
|
82
|
+
why, alternatives rejected, constraints discovered.
|
|
83
|
+
- `output.md` — the result: what actually changed (files, commits, configs), how it
|
|
84
|
+
was verified, gotchas hit, follow-ups left open.
|
|
85
|
+
|
|
86
|
+
Keep each file under ~120 lines. Write for a future reader with zero session
|
|
87
|
+
context. Use `[[wikilinks]]` only for project-specific entities likely to have wiki
|
|
88
|
+
pages; plain `[text](url)` links for well-known technologies.
|
|
89
|
+
|
|
90
|
+
## Write procedure
|
|
91
|
+
|
|
92
|
+
1. `mkdir -p` the project directory under the artifacts root.
|
|
93
|
+
2. Write each file atomically: write to `<dir>/.tmp-<final-name>` with the Write
|
|
94
|
+
tool, then `mv` it to the final name.
|
|
95
|
+
3. Never overwrite an existing artifact: if a final path already exists, append a
|
|
96
|
+
`-2` (then `-3`, ...) suffix to the task slug and retry.
|
|
97
|
+
4. Touch nothing else in the vault — no `wiki/` edits, no other files. The ingest
|
|
98
|
+
pipeline owns the wiki.
|
|
99
|
+
5. Report the two final paths to the user.
|