skillwiki 0.2.1-beta.1 → 0.2.1-beta.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/dist/cli.js CHANGED
@@ -878,6 +878,10 @@ function hasOrphanedCitations(body) {
878
878
  }
879
879
  return false;
880
880
  }
881
+ function hasWikilinkCitations(body) {
882
+ const stripped = stripFences(body);
883
+ return /\[\[raw\/[^\]]+\]\]/.test(stripped);
884
+ }
881
885
 
882
886
  // src/commands/audit.ts
883
887
  async function runAudit(input) {
@@ -1172,9 +1176,12 @@ var VAULT_DIRS = [
1172
1176
  "queries",
1173
1177
  "meta",
1174
1178
  "projects",
1175
- ".obsidian"
1179
+ ".obsidian",
1180
+ "_Templates"
1176
1181
  ];
1177
1182
  var ATTACHMENT_FOLDER = "raw/assets";
1183
+ var NEW_FILE_FOLDER = "raw/transcripts";
1184
+ var TEMPLATE_FOLDER = "_Templates";
1178
1185
  function extractDomainFromSchema(text) {
1179
1186
  const m = text.match(/^##\s+Domain\s*\n([\s\S]*?)(?=\n\n|\n##|\s*$)/m);
1180
1187
  if (!m) return "";
@@ -1305,9 +1312,26 @@ async function runInit(input) {
1305
1312
  });
1306
1313
  if (err1) return err1;
1307
1314
  const errObsidian = await writeOrPreserve(".obsidian/app.json", async () => {
1308
- return JSON.stringify({ attachmentFolderPath: ATTACHMENT_FOLDER }, null, 2) + "\n";
1315
+ return JSON.stringify({ attachmentFolderPath: ATTACHMENT_FOLDER, newFileLocation: "folder", newFileFolderPath: NEW_FILE_FOLDER }, null, 2) + "\n";
1309
1316
  });
1310
1317
  if (errObsidian) return errObsidian;
1318
+ const errTemplatesJson = await writeOrPreserve(".obsidian/templates.json", async () => {
1319
+ return JSON.stringify({ folder: TEMPLATE_FOLDER }, null, 2) + "\n";
1320
+ });
1321
+ if (errTemplatesJson) return errTemplatesJson;
1322
+ const errTemplate = await writeOrPreserve(`${TEMPLATE_FOLDER}/tpl-ad-hoc-capture.md`, async () => {
1323
+ return [
1324
+ "---",
1325
+ "project: ",
1326
+ "tags: []",
1327
+ "priority: ",
1328
+ "ingested: {{date:YYYY-MM-DD}}",
1329
+ "---",
1330
+ "",
1331
+ ""
1332
+ ].join("\n");
1333
+ });
1334
+ if (errTemplate) return errTemplate;
1311
1335
  const err22 = await writeOrPreserve("log.md", async () => {
1312
1336
  const tpl = await readFile6(join7(input.templates, "log.md"), "utf8");
1313
1337
  return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
@@ -1354,7 +1378,8 @@ async function runInit(input) {
1354
1378
  env_skipped: skipEnv,
1355
1379
  imported_from_hermes: importedFromHermes,
1356
1380
  discovered_tags,
1357
- humanHint
1381
+ humanHint,
1382
+ templates_created: created.includes(`${TEMPLATE_FOLDER}/tpl-ad-hoc-capture.md`)
1358
1383
  })
1359
1384
  };
1360
1385
  }
@@ -1704,7 +1729,7 @@ function hasDuplicateFrontmatter(body) {
1704
1729
  }
1705
1730
  var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "tag_not_in_taxonomy"];
1706
1731
  var WARNING_ORDER = ["index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "orphans", "legacy_citation_style", "orphaned_citations", "duplicate_frontmatter", "missing_overview"];
1707
- var INFO_ORDER = ["bridges", "page_structure", "topic_map_recommended", "frontmatter_wikilink"];
1732
+ var INFO_ORDER = ["bridges", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation"];
1708
1733
  async function runLint(input) {
1709
1734
  const buckets = {};
1710
1735
  const links = await runLinks({ vault: input.vault });
@@ -1757,6 +1782,7 @@ async function runLint(input) {
1757
1782
  const dupFrontmatter = [];
1758
1783
  const noOverview = [];
1759
1784
  const fmWikilinkFlags = [];
1785
+ const wikilinkCitationFlags = [];
1760
1786
  for (const page of scan.data.typedKnowledge) {
1761
1787
  const text = await readPage(page);
1762
1788
  const split = splitFrontmatter(text);
@@ -1766,6 +1792,7 @@ async function runLint(input) {
1766
1792
  if (hasDuplicateFrontmatter(body)) dupFrontmatter.push(page.relPath);
1767
1793
  if (isLegacyCitationStyle(body)) legacyPages.push(page.relPath);
1768
1794
  if (hasOrphanedCitations(body)) orphanedPages.push(page.relPath);
1795
+ if (hasWikilinkCitations(body)) wikilinkCitationFlags.push(page.relPath);
1769
1796
  const fmLinks = rawFm.match(/\[\[([^\[\]|]+)(?:\|[^\[\]]*)?\]\]/g) ?? [];
1770
1797
  for (const link of fmLinks) {
1771
1798
  const target = link.replace(/^\[\[/, "").replace(/(?:\|[^\[\]]*)?\]\]$/, "").trim();
@@ -1794,6 +1821,7 @@ async function runLint(input) {
1794
1821
  if (dupFrontmatter.length > 0) buckets.duplicate_frontmatter = dupFrontmatter;
1795
1822
  if (noOverview.length > 0) buckets.missing_overview = noOverview;
1796
1823
  if (fmWikilinkFlags.length > 0) buckets.frontmatter_wikilink = fmWikilinkFlags;
1824
+ if (wikilinkCitationFlags.length > 0) buckets.wikilink_citation = wikilinkCitationFlags;
1797
1825
  }
1798
1826
  const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
1799
1827
  const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
@@ -2170,29 +2198,35 @@ import { join as join18, dirname as dirname7 } from "path";
2170
2198
  async function runArchive(input) {
2171
2199
  const scan = await scanVault(input.vault);
2172
2200
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
2173
- let relPath;
2174
- if (input.page.includes("/")) {
2175
- relPath = scan.data.typedKnowledge.find((p) => p.relPath === input.page)?.relPath;
2176
- } else {
2177
- relPath = scan.data.typedKnowledge.find((p) => p.relPath.replace(/\.md$/, "").split("/").pop() === input.page)?.relPath;
2201
+ const lookup = (pages) => {
2202
+ if (input.page.includes("/")) return pages.find((p) => p.relPath === input.page)?.relPath;
2203
+ return pages.find((p) => p.relPath.replace(/\.md$/, "").split("/").pop() === input.page)?.relPath;
2204
+ };
2205
+ let relPath = lookup(scan.data.typedKnowledge);
2206
+ let isRaw = false;
2207
+ if (!relPath) {
2208
+ relPath = lookup(scan.data.raw);
2209
+ isRaw = relPath != null;
2178
2210
  }
2179
2211
  if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
2180
2212
  if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
2181
2213
  const archivePath = join18("_archive", relPath);
2182
2214
  await mkdir5(dirname7(join18(input.vault, archivePath)), { recursive: true });
2183
2215
  let indexUpdated = false;
2184
- const indexPath = join18(input.vault, "index.md");
2185
- try {
2186
- const idx = await readFile13(indexPath, "utf8");
2187
- const slug = relPath.replace(/\.md$/, "").split("/").pop();
2188
- const originalLines = idx.split("\n");
2189
- const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
2190
- if (filtered.length !== originalLines.length) {
2191
- await writeFile6(indexPath, filtered.join("\n"), "utf8");
2192
- indexUpdated = true;
2216
+ if (!isRaw) {
2217
+ const indexPath = join18(input.vault, "index.md");
2218
+ try {
2219
+ const idx = await readFile13(indexPath, "utf8");
2220
+ const slug = relPath.replace(/\.md$/, "").split("/").pop();
2221
+ const originalLines = idx.split("\n");
2222
+ const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
2223
+ if (filtered.length !== originalLines.length) {
2224
+ await writeFile6(indexPath, filtered.join("\n"), "utf8");
2225
+ indexUpdated = true;
2226
+ }
2227
+ } catch (e) {
2228
+ if (e?.code !== "ENOENT") throw e;
2193
2229
  }
2194
- } catch (e) {
2195
- if (e?.code !== "ENOENT") throw e;
2196
2230
  }
2197
2231
  await rename3(join18(input.vault, relPath), join18(input.vault, archivePath));
2198
2232
  return { exitCode: ExitCode.OK, result: ok({ archived_from: relPath, archived_to: archivePath, index_updated: indexUpdated, humanHint: `${relPath} -> ${archivePath}${indexUpdated ? " (index updated)" : ""}` }) };
@@ -2241,16 +2275,31 @@ async function runDrift(input) {
2241
2275
  const scan = await scanVault(input.vault);
2242
2276
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
2243
2277
  const results = [];
2278
+ const newResults = [];
2244
2279
  for (const raw of scan.data.raw) {
2245
2280
  const text = await readPage(raw);
2246
2281
  const split = splitFrontmatter(text);
2247
2282
  if (!split.ok) continue;
2248
2283
  const { rawFrontmatter, body } = split.data;
2284
+ const ingestedMatch = rawFrontmatter.match(/^ingested:\s*(.+)$/m);
2285
+ const ingestedRaw = ingestedMatch?.[1]?.trim() ?? "";
2286
+ const ingested = ingestedRaw.replace(/^["']|["']$/g, "");
2287
+ if (input.newSince && ingested && ingested >= input.newSince) {
2288
+ newResults.push({
2289
+ raw_path: raw.relPath,
2290
+ source_url: "",
2291
+ stored_sha256: "",
2292
+ current_sha256: null,
2293
+ status: "new",
2294
+ ingested
2295
+ });
2296
+ }
2249
2297
  const sourceUrlMatch = rawFrontmatter.match(/^source_url:\s*(.+)$/m);
2250
2298
  const storedHashMatch = rawFrontmatter.match(/^sha256:\s*([a-f0-9]+)$/m);
2251
2299
  if (!sourceUrlMatch || !storedHashMatch) continue;
2252
2300
  const sourceUrl = sourceUrlMatch[1].trim();
2253
2301
  const storedHash = storedHashMatch[1];
2302
+ if (!sourceUrl.startsWith("http://") && !sourceUrl.startsWith("https://")) continue;
2254
2303
  const resp = await doFetch(sourceUrl, FETCH_OPTS);
2255
2304
  if (!resp.ok) {
2256
2305
  results.push({
@@ -2295,12 +2344,13 @@ ${body}`;
2295
2344
  const unchanged = results.filter((r) => r.status === "unchanged").length;
2296
2345
  const exitCode = drifted.length > 0 ? ExitCode.DRIFT_DETECTED : ExitCode.OK;
2297
2346
  const hintLines = [`scanned: ${results.length}, unchanged: ${unchanged}`];
2347
+ if (newResults.length > 0) hintLines.push(`new: ${newResults.length}`, ...newResults.map((n) => ` ${n.raw_path} (ingested: ${n.ingested})`));
2298
2348
  if (drifted.length > 0) hintLines.push(`drifted: ${drifted.length}`, ...drifted.map((d) => ` ${d.raw_path}`));
2299
2349
  if (fetchFailed.length > 0) hintLines.push(`fetch_failed: ${fetchFailed.length}`, ...fetchFailed.map((f) => ` ${f.raw_path}: ${f.fetch_error}`));
2300
2350
  if (updated.length > 0) hintLines.push(`updated: ${updated.length}`, ...updated.map((u) => ` ${u.raw_path}`));
2301
2351
  return {
2302
2352
  exitCode,
2303
- result: ok({ scanned: results.length, drifted, fetch_failed: fetchFailed, updated, unchanged, humanHint: hintLines.join("\n") })
2353
+ result: ok({ scanned: results.length, drifted, fetch_failed: fetchFailed, updated, newFiles: newResults, unchanged, humanHint: hintLines.join("\n") })
2304
2354
  };
2305
2355
  }
2306
2356
 
@@ -2575,6 +2625,37 @@ async function runUpdate(input) {
2575
2625
  };
2576
2626
  }
2577
2627
 
2628
+ // src/commands/transcripts.ts
2629
+ import { readdir as readdir4, stat as stat6, readFile as readFile14 } from "fs/promises";
2630
+ import { join as join19 } from "path";
2631
+ async function runTranscripts(input) {
2632
+ const dir = join19(input.vault, "raw", "transcripts");
2633
+ let entries;
2634
+ try {
2635
+ entries = await readdir4(dir, { withFileTypes: true });
2636
+ } catch {
2637
+ return { exitCode: ExitCode.VAULT_PATH_INVALID, result: { ok: false, error: "VAULT_PATH_INVALID", detail: `raw/transcripts/ not found: ${dir}` } };
2638
+ }
2639
+ const transcripts = [];
2640
+ for (const entry of entries) {
2641
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
2642
+ const filePath = join19(dir, entry.name);
2643
+ const content = await readFile14(filePath, "utf8");
2644
+ const fm = extractFrontmatter(content);
2645
+ if (!fm.ok) continue;
2646
+ const ingested = typeof fm.data.ingested === "string" ? fm.data.ingested : "";
2647
+ if (input.since && ingested && ingested < input.since) continue;
2648
+ const s = await stat6(filePath);
2649
+ transcripts.push({
2650
+ file: `raw/transcripts/${entry.name}`,
2651
+ ingested,
2652
+ size: s.size
2653
+ });
2654
+ }
2655
+ const hint = transcripts.length > 0 ? transcripts.map((t) => `${t.file} (ingested: ${t.ingested || "unknown"}, ${t.size}B)`).join("\n") : "no transcript files found";
2656
+ return { exitCode: ExitCode.OK, result: ok({ transcripts, humanHint: hint }) };
2657
+ }
2658
+
2578
2659
  // src/cli.ts
2579
2660
  var pkg = JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8"));
2580
2661
  var program = new Command();
@@ -2710,10 +2791,10 @@ program.command("archive <page> [vault]").description("archive a typed-knowledge
2710
2791
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
2711
2792
  else emit(await runArchive({ vault: v.vault, page }));
2712
2793
  });
2713
- program.command("drift [vault]").description("detect content drift in raw sources").option("--apply", "update sha256 in drifted sources").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
2794
+ program.command("drift [vault]").description("detect content drift in raw sources").option("--apply", "update sha256 in drifted sources").option("--new <date>", "list raw files ingested on/after this date (YYYY-MM-DD)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
2714
2795
  const v = await resolveVaultArg(vault, opts.wiki);
2715
2796
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
2716
- else emit(await runDrift({ vault: v.vault, apply: opts.apply }));
2797
+ else emit(await runDrift({ vault: v.vault, apply: opts.apply, newSince: opts.new }));
2717
2798
  });
2718
2799
  program.command("dedup [vault]").description("detect duplicate raw sources by sha256").option("--apply", "rewire citations and remove duplicate raw files", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
2719
2800
  const v = await resolveVaultArg(vault, opts.wiki);
@@ -2734,6 +2815,11 @@ program.command("update").description("update skillwiki CLI to the latest versio
2734
2815
  home: process.env.HOME ?? "",
2735
2816
  distTag: opts.tag
2736
2817
  })));
2818
+ program.command("transcripts [vault]").description("list transcript files in raw/transcripts/").option("--since <date>", "only files ingested on or after this date (YYYY-MM-DD)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
2819
+ const v = await resolveVaultArg(vault, opts.wiki);
2820
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
2821
+ else emit(await runTranscripts({ vault: v.vault, since: opts.since }));
2822
+ });
2737
2823
  triggerAutoUpdate(process.env.HOME ?? "", pkg.version);
2738
2824
  program.parseAsync(process.argv).catch((e) => {
2739
2825
  process.stdout.write(JSON.stringify({ ok: false, error: "INTERNAL", detail: { message: String(e) } }) + "\n");
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.2.1-beta.1",
4
- "version": "0.2.1-beta.1",
3
+ "version": "0.2.1-beta.10",
5
4
  "type": "module",
6
5
  "bin": {
7
6
  "skillwiki": "dist/cli.js"
@@ -1,9 +1,8 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.2.1-beta.1",
4
- "version": "0.2.1-beta.1",
3
+ "version": "0.2.1-beta.10",
5
4
  "skills": "./",
6
- "description": "Project-aware Karpathy-style knowledge base for Claude Code: 11 prompt-only skills (wiki-*, proj-*, using-skillwiki) backed by the deterministic `skillwiki` CLI (8 subcommands, JSON-by-default).",
5
+ "description": "Project-aware Karpathy-style knowledge base for Claude Code: 15 prompt-only skills (wiki-*, proj-*, using-skillwiki) backed by the deterministic `skillwiki` CLI.",
7
6
  "author": {
8
7
  "name": "karlorz",
9
8
  "url": "https://github.com/karlorz"
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "@skillwiki/skills",
3
- "version": "0.2.1-beta.1",
4
- "version": "0.2.1-beta.1",
3
+ "version": "0.2.1-beta.10",
5
4
  "private": true,
6
5
  "files": [
7
6
  "wiki-*",
@@ -20,6 +20,7 @@ Invoke a skillwiki skill when the user:
20
20
  - Wants a health check or lint on their vault
21
21
  - Mentions crystallizing a session into a note
22
22
  - Talks about project workspaces, ADRs, or distillation
23
+ - Wants to quickly capture an idea, bug, task, or note without interrupting their workflow
23
24
  - Wants to archive or clean up old vault pages
24
25
  - Needs to detect source drift or re-ingest updated content
25
26
  - Has a spec/plan in a non-skillwiki format (CodeStable, RFC, AIDE)
@@ -27,15 +28,15 @@ Invoke a skillwiki skill when the user:
27
28
 
28
29
  ## Vault Structure
29
30
 
30
- A skillwiki vault has two layers:
31
+ A skillwiki vault has three layers. The canonical architecture lives in `SCHEMA.md` at the vault root — read it before creating any new directories.
31
32
 
32
- **Layer 1 — Raw (`raw/`):** Immutable source material. Never modify after ingest.
33
+ **Layer 1 — Raw (`raw/`):** Immutable source material. Never modify after ingest. `raw/transcripts/` doubles as the ad-hoc capture point for meeting notes and unprocessed ideas.
33
34
 
34
35
  ```
35
36
  raw/
36
37
  ├── articles/ # Web articles, clippings
37
38
  ├── papers/ # PDFs, arxiv papers
38
- ├── transcripts/ # Meeting notes, interviews
39
+ ├── transcripts/ # Meeting notes, interviews, ad-hoc captures
39
40
  └── assets/ # Images, diagrams referenced by sources
40
41
  ```
41
42
 
@@ -48,7 +49,19 @@ sha256: # computed by skillwiki hash over body bytes after closing ---
48
49
  ---
49
50
  ```
50
51
 
51
- **Layer 2 — Agent-owned pages:** `entities/`, `concepts/`, `comparisons/`, `queries/`, `meta/`, `projects/`. Citations use `^[raw/articles/source-file.md]` markers at paragraph-end.
52
+ **Layer 2 — Typed Knowledge:** `entities/`, `concepts/`, `comparisons/`, `queries/`, `meta/`. Agent-owned pages with `^[raw/...]` citation markers at paragraph-end. Global scope — project association via `provenance_projects:` frontmatter, not directory nesting.
53
+
54
+ **Layer 3 — Project Workspaces (`projects/{slug}/`):** Per-project lifecycle directories with `work/` (spec + plan + retro), `compound/` (distilled lessons/patterns), `architecture/` (ADRs), and `history/` (archived specs/plans).
55
+
56
+ **No `inbox/` directory.** Ad-hoc captures go to `raw/transcripts/` or directly into a project work item via `proj-work`. Do not invent new top-level directories — extend Layer 2 via SCHEMA.md tag taxonomy if needed.
57
+
58
+ ### Ad-hoc capture: three entry points
59
+
60
+ | Entry | When | What happens |
61
+ |-------|------|-------------|
62
+ | `/wiki-add-task <text>` | You're in a Claude session | Appends entry to `raw/transcripts/YYYY-MM-DD-ad-hoc-captures.md` |
63
+ | Filesystem drop | You're NOT in a Claude session (Obsidian, editor, sync) | Create/edit any `.md` file in `raw/transcripts/` — dev-loop discovers it on next cycle |
64
+ | Dev-loop discovery | Automatic, next cycle | Scans `raw/transcripts/` for new files since last cycle, surfaces as claimable work |
52
65
 
53
66
  ## Skill Map
54
67
 
@@ -62,6 +75,7 @@ sha256: # computed by skillwiki hash over body bytes after closing ---
62
75
  | `wiki-audit` | Verify raw provenance references and source frontmatter integrity |
63
76
  | `wiki-archive` | Archive a typed-knowledge page — move to `_archive/`, remove from index |
64
77
  | `wiki-reingest` | Detect drift in raw sources (sha256 comparison) and re-ingest updated content |
78
+ | `wiki-add-task` | Quick-capture ideas, bugs, tasks, notes into `raw/transcripts/` without leaving the current workflow |
65
79
  | `wiki-adapter-prd` | Map foreign PRD formats (CodeStable, RFC, AIDE, Hermes) into vault pages |
66
80
  | `proj-init` | Bootstrap a project workspace (README, requirements, architecture) |
67
81
  | `proj-work` | Open or run a work item under a project's work/ directory |
@@ -0,0 +1,104 @@
1
+ ---
2
+ name: wiki-add-task
3
+ description: Capture ad-hoc ideas, bugs, tasks, or notes into the vault via /wiki-add-task or filesystem drop.
4
+ ---
5
+
6
+ # wiki-add-task
7
+
8
+ Capture ad-hoc ideas, bugs, tasks, and notes into the vault. Three entry points depending on where you are:
9
+
10
+ | Entry | When | What happens |
11
+ |-------|------|-------------|
12
+ | `/wiki-add-task <text>` | You're in a Claude session | Appends entry to `raw/transcripts/YYYY-MM-DD-ad-hoc-captures.md` |
13
+ | Filesystem drop | You're NOT in a Claude session (Obsidian, editor, sync) | Create/edit any file in `raw/transcripts/` — dev-loop discovers it on next cycle |
14
+ | Dev-loop discovery | Automatic, next cycle | Scans `raw/transcripts/` for new files since last cycle, surfaces as claimable work |
15
+
16
+ ## When This Skill Activates
17
+
18
+ - User invokes `/wiki-add-task` with a description.
19
+ - User says "add task", "capture this", "note this", "remember this", "log this idea", or similar.
20
+ - User provides a short text description and optionally a type tag.
21
+
22
+ ## Output language
23
+
24
+ Run `skillwiki lang` at the start. Entry prose and `--human` summaries use the resolved language. Frontmatter keys, file names, and structural markers stay English.
25
+
26
+ ## Steps
27
+
28
+ 0. **Resolve vault and language.** Run `skillwiki path` (fail if NO_VAULT_CONFIGURED) and `skillwiki lang`.
29
+ 1. **Parse arguments.** Extract from the user's message:
30
+ - `text` — the idea/bug/task/note content (required)
31
+ - `type` — one of: `idea`, `bug`, `task`, `note` (default: `idea`)
32
+ - `project` — optional project slug to cross-reference (e.g., `llm-wiki`)
33
+ 2. **Determine target file.** The capture file is `raw/transcripts/YYYY-MM-DD-ad-hoc-captures.md` where YYYY-MM-DD is today's date. If the file exists, append; otherwise create it with standard raw frontmatter.
34
+ 3. **Write the entry.** Append to the capture file:
35
+ ```markdown
36
+ ### HH:MM — [type]
37
+
38
+ [text]
39
+
40
+ <!---meta: {"captured_at": "YYYY-MM-DDTHH:MM:SS", "type": "[type]"}--->
41
+ ```
42
+ - Use 24-hour time for HH:MM.
43
+ - Do not overwrite or modify existing entries.
44
+ 4. **Cross-reference (optional).** If a `project` slug was provided:
45
+ - Check that `projects/{slug}/` exists in the vault.
46
+ - Append a one-line reference to the project's work log or compound notes:
47
+ `- [YYYY-MM-DD] capture: [text] → raw/transcripts/YYYY-MM-DD-ad-hoc-captures.md`
48
+ - Do NOT create a full work item (that's `proj-work`'s job).
49
+ 5. **Update log.md.** Append: `## [YYYY-MM-DD] capture | [type]: [text (first 60 chars)]`
50
+ 6. **Confirm to user.** Report what was captured and where. Suggest next steps:
51
+ - If `type: idea` → "Consider ingesting related sources to develop this idea."
52
+ - If `type: bug` → "Use proj-work to create a bug-fix work item."
53
+ - If `type: task` → "Use proj-work to track this task through the dev loop."
54
+ - If `type: note` → "Will be available for future wiki-query searches."
55
+
56
+ ## Ad-hoc captures file format
57
+
58
+ The file `raw/transcripts/YYYY-MM-DD-ad-hoc-captures.md` is a standard raw source with frontmatter:
59
+
60
+ ```yaml
61
+ ---
62
+ source_url:
63
+ ingested: YYYY-MM-DD
64
+ sha256:
65
+ ---
66
+ ```
67
+
68
+ The `sha256` is computed over the body after the closing `---`. On each append, recompute and update `sha256`. This keeps source-drift detection functional even though the file grows throughout the day.
69
+
70
+ ## Stop conditions
71
+
72
+ - `skillwiki path` returns NO_VAULT_CONFIGURED.
73
+ - No `text` provided (prompt user once, then stop).
74
+
75
+ ## Forbidden
76
+
77
+ - Creating an `inbox/` directory. All captures go to `raw/transcripts/`.
78
+ - Modifying existing entries in the captures file — only append.
79
+ - Creating a work item — this is capture-only. Use `proj-work` for full work items.
80
+ - Writing to any Layer 2 or Layer 3 location. Captures are Layer 1 (raw).
81
+
82
+ ## Filesystem drop (offline capture)
83
+
84
+ When you're not in a Claude session, drop files directly into `raw/transcripts/`:
85
+
86
+ 1. Create any `.md` file in `raw/transcripts/` — name it descriptively (e.g., `2026-05-07-idea-xyz.md`)
87
+ 2. Add raw frontmatter at the top:
88
+ ```yaml
89
+ ---
90
+ source_url:
91
+ ingested: YYYY-MM-DD
92
+ sha256:
93
+ ---
94
+ ```
95
+ 3. Write your idea/bug/task/note below the frontmatter
96
+
97
+ No special format required — the dev-loop QUERY step will discover new files on the next cycle and surface them as claimable work. Mark the type with a heading like `## idea`, `## bug`, `## task`, or just write freeform.
98
+
99
+ ## Dev-loop discovery
100
+
101
+ When the dev-loop QUERY step runs, it should scan `raw/transcripts/` for files with `ingested:` date newer than the last cycle. New files are surfaced as claimable work items. The agent then decides whether to:
102
+ - Create a work item via `proj-work` (for tasks and bugs)
103
+ - Ingest as a knowledge page via `wiki-ingest` (for ideas with sources)
104
+ - Leave in place (for notes that don't need action yet)