skillwiki 0.2.5 → 0.3.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/dist/cli.js CHANGED
@@ -1845,6 +1845,7 @@ async function runStale(input) {
1845
1845
  const incompleteWorkItems = [];
1846
1846
  const archived = [];
1847
1847
  const workDirs = /* @__PURE__ */ new Map();
1848
+ const workDirsBySlug = /* @__PURE__ */ new Map();
1848
1849
  const projectsDir = join13(input.vault, "projects");
1849
1850
  let projectSlugs = [];
1850
1851
  try {
@@ -1859,6 +1860,7 @@ async function runStale(input) {
1859
1860
  } catch {
1860
1861
  continue;
1861
1862
  }
1863
+ const slugDirs = /* @__PURE__ */ new Map();
1862
1864
  for (const e of entries) {
1863
1865
  if (!e.isDirectory()) continue;
1864
1866
  const relDir = `projects/${slug}/work/${e.name}`;
@@ -1869,6 +1871,7 @@ async function runStale(input) {
1869
1871
  files = await readdir4(absDir);
1870
1872
  } catch {
1871
1873
  workDirs.set(relDir, "");
1874
+ slugDirs.set(e.name, "");
1872
1875
  continue;
1873
1876
  }
1874
1877
  for (const f of files) {
@@ -1883,16 +1886,106 @@ async function runStale(input) {
1883
1886
  }
1884
1887
  }
1885
1888
  workDirs.set(relDir, status);
1889
+ slugDirs.set(e.name, status);
1886
1890
  }
1891
+ workDirsBySlug.set(slug, slugDirs);
1887
1892
  }
1893
+ function extractSlug(projectField) {
1894
+ return projectField.replace(/^\[\[/, "").replace(/\]\]$/, "").replace(/^"|"$/g, "");
1895
+ }
1896
+ const KIND_FROM_FILENAME = /^(?:\d{4}-\d{2}-\d{2})-(task|bug|idea|note|observation)-.+\.md$/;
1897
+ const LOOP_CYCLE_PATTERN = /loop-cycle-/;
1888
1898
  const transcripts = scan.data.raw.filter((p) => p.relPath.startsWith("raw/transcripts/") && p.relPath.endsWith(".md"));
1899
+ const claimedPaths = /* @__PURE__ */ new Set();
1900
+ const transcriptMeta = /* @__PURE__ */ new Map();
1901
+ for (const t of transcripts) {
1902
+ try {
1903
+ const content = await readFile10(join13(input.vault, t.relPath), "utf8");
1904
+ const fm = extractFrontmatter(content);
1905
+ let kind = fm.ok && typeof fm.data.kind === "string" ? fm.data.kind : "";
1906
+ let project = fm.ok && typeof fm.data.project === "string" ? fm.data.project : "";
1907
+ let inferred = false;
1908
+ if (input.forceScan && !kind) {
1909
+ const basename = t.relPath.split("/").pop();
1910
+ if (!LOOP_CYCLE_PATTERN.test(basename)) {
1911
+ const m = basename.match(KIND_FROM_FILENAME);
1912
+ if (m) {
1913
+ kind = m[1];
1914
+ inferred = true;
1915
+ }
1916
+ }
1917
+ }
1918
+ if (input.forceScan && !project && kind) {
1919
+ const bodyStart = content.indexOf("---", 4);
1920
+ if (bodyStart > 0) {
1921
+ const body = content.slice(bodyStart);
1922
+ const wikilink2 = body.match(/\[\[([a-z0-9-]+)\]\]/);
1923
+ if (wikilink2) {
1924
+ const candidate = wikilink2[1];
1925
+ if (workDirsBySlug.has(candidate)) {
1926
+ project = `[[${candidate}]]`;
1927
+ inferred = true;
1928
+ }
1929
+ }
1930
+ }
1931
+ }
1932
+ transcriptMeta.set(t.relPath, { kind, project, slug: extractSlug(project), inferred });
1933
+ } catch {
1934
+ }
1935
+ }
1889
1936
  for (const t of transcripts) {
1890
1937
  const datePrefix = t.relPath.split("/").pop().slice(0, 10);
1891
- for (const [dir, status] of workDirs) {
1892
- if (dir.split("/").pop().startsWith(datePrefix) && (status === "done" || status === "invalid")) {
1893
- staleTranscripts.push({ path: t.relPath, reason: `work item ${dir} is ${status}` });
1894
- break;
1938
+ const meta = transcriptMeta.get(t.relPath);
1939
+ const slug = meta?.slug || "";
1940
+ if (slug && workDirsBySlug.has(slug)) {
1941
+ const slugDirs = workDirsBySlug.get(slug);
1942
+ const tSlug = t.relPath.split("/").pop().replace(/^\d{4}-\d{2}-\d{2}-/, "").replace(/\.md$/, "").replace(/^(task|bug|idea|note|observation)-/, "");
1943
+ for (const [dirName, status] of slugDirs) {
1944
+ if (!dirName.startsWith(datePrefix)) continue;
1945
+ const wSlug = dirName.replace(/^\d{4}-\d{2}-\d{2}-/, "");
1946
+ const tWords = new Set(tSlug.split("-").filter((w) => w.length >= 3));
1947
+ const wWords = wSlug.split("-").filter((w) => w.length >= 3);
1948
+ const overlap = wWords.filter((w) => tWords.has(w)).length;
1949
+ if (dirName.includes(tSlug) || tSlug.includes(wSlug) || overlap >= 1) {
1950
+ claimedPaths.add(t.relPath);
1951
+ if (status === "done" || status === "invalid") {
1952
+ staleTranscripts.push({ path: t.relPath, reason: `work item projects/${slug}/work/${dirName} is ${status}` });
1953
+ }
1954
+ break;
1955
+ }
1956
+ }
1957
+ } else if (!slug) {
1958
+ for (const [dir, status] of workDirs) {
1959
+ if (dir.split("/").pop().startsWith(datePrefix)) {
1960
+ claimedPaths.add(t.relPath);
1961
+ if (status === "done" || status === "invalid") {
1962
+ staleTranscripts.push({ path: t.relPath, reason: `work item ${dir} is ${status}` });
1963
+ }
1964
+ break;
1965
+ }
1966
+ }
1967
+ }
1968
+ }
1969
+ for (const [relDir] of workDirs) {
1970
+ const specPath = join13(input.vault, relDir, "spec.md");
1971
+ try {
1972
+ const specContent = await readFile10(specPath, "utf8");
1973
+ const specFm = extractFrontmatter(specContent);
1974
+ if (specFm.ok && typeof specFm.data.source === "string") {
1975
+ const sourcePath = specFm.data.source;
1976
+ if (sourcePath.startsWith("raw/transcripts/")) claimedPaths.add(sourcePath);
1895
1977
  }
1978
+ } catch {
1979
+ }
1980
+ }
1981
+ const unclaimedTranscripts = [];
1982
+ const CLAIMABLE_KINDS = /* @__PURE__ */ new Set(["task", "bug"]);
1983
+ for (const t of transcripts) {
1984
+ if (claimedPaths.has(t.relPath)) continue;
1985
+ const meta = transcriptMeta.get(t.relPath);
1986
+ if (!meta) continue;
1987
+ if (CLAIMABLE_KINDS.has(meta.kind) && meta.project) {
1988
+ unclaimedTranscripts.push({ path: t.relPath, reason: `${meta.kind} for ${meta.project} \u2014 no work item` });
1896
1989
  }
1897
1990
  }
1898
1991
  const doneWorkItems = [];
@@ -1987,17 +2080,19 @@ async function runStale(input) {
1987
2080
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1988
2081
  });
1989
2082
  }
1990
- const total = stale.length + staleTranscripts.length + incompleteWorkItems.length + doneWorkItems.length;
2083
+ const total = stale.length + staleTranscripts.length + unclaimedTranscripts.length + incompleteWorkItems.length + doneWorkItems.length;
1991
2084
  const hintLines = [];
1992
2085
  if (stale.length > 0) hintLines.push(`stale_pages: ${stale.length}`, ...stale.map((p) => ` ${p.page}: ${p.reason}`));
1993
2086
  if (staleTranscripts.length > 0) hintLines.push(`stale_transcripts: ${staleTranscripts.length}`, ...staleTranscripts.map((t) => ` ${t.path}: ${t.reason}`));
2087
+ if (unclaimedTranscripts.length > 0) hintLines.push(`unclaimed_transcripts: ${unclaimedTranscripts.length}`, ...unclaimedTranscripts.map((t) => ` ${t.path}: ${t.reason}`));
1994
2088
  if (incompleteWorkItems.length > 0) hintLines.push(`incomplete_work_items: ${incompleteWorkItems.length}`, ...incompleteWorkItems.map((w) => ` ${w.path}: ${w.reason}`));
1995
2089
  if (doneWorkItems.length > 0) hintLines.push(`done_work_items: ${doneWorkItems.length}`, ...doneWorkItems.map((w) => ` ${w.path}: ${w.reason}`));
1996
2090
  if (archived.length > 0) hintLines.push(`archived: ${archived.length}`, ...archived.map((a) => ` ${a}`));
1997
2091
  if (hintLines.length === 0) hintLines.push("no stale transcripts or incomplete work items");
1998
2092
  return { exitCode: total > 0 ? ExitCode.STALE_PAGE : ExitCode.OK, result: ok({
1999
- stale: [...stale, ...staleTranscripts.map((t) => ({ page: t.path, reason: t.reason })), ...incompleteWorkItems.map((w) => ({ page: w.path, reason: w.reason })), ...doneWorkItems.map((w) => ({ page: w.path, reason: w.reason }))],
2093
+ stale: [...stale, ...staleTranscripts.map((t) => ({ page: t.path, reason: t.reason })), ...unclaimedTranscripts.map((t) => ({ page: t.path, reason: t.reason })), ...incompleteWorkItems.map((w) => ({ page: w.path, reason: w.reason })), ...doneWorkItems.map((w) => ({ page: w.path, reason: w.reason }))],
2000
2094
  stale_transcripts: staleTranscripts,
2095
+ unclaimed_transcripts: unclaimedTranscripts,
2001
2096
  incomplete_work_items: incompleteWorkItems,
2002
2097
  done_work_items: doneWorkItems,
2003
2098
  archived,
@@ -2268,7 +2363,7 @@ async function runLint(input) {
2268
2363
  const staleResult = await runStale({ vault: input.vault, days: input.days });
2269
2364
  if (staleResult.result.ok) {
2270
2365
  const st = staleResult.result.data;
2271
- const staleList = [...st.stale_transcripts.map((t) => t.path), ...st.incomplete_work_items.map((w) => w.path), ...(st.done_work_items ?? []).map((w) => w.path)];
2366
+ const staleList = [...st.stale_transcripts.map((t) => t.path), ...(st.unclaimed_transcripts ?? []).map((t) => t.path), ...st.incomplete_work_items.map((w) => w.path), ...(st.done_work_items ?? []).map((w) => w.path)];
2272
2367
  if (staleList.length > 0) buckets.stale_page = staleList;
2273
2368
  }
2274
2369
  const pagesize = await runPagesize({ vault: input.vault, lines: input.lines });
@@ -4392,7 +4487,7 @@ function slugify2(text) {
4392
4487
  return words || "untitled";
4393
4488
  }
4394
4489
  async function runObserve(input) {
4395
- const kind = input.kind || "note";
4490
+ const kind = input.kind || "task";
4396
4491
  if (!ALLOWED_KINDS.has(kind)) {
4397
4492
  return {
4398
4493
  exitCode: ExitCode.SCHEME_REJECTED,
@@ -5944,10 +6039,10 @@ program.command("topic-map-check [vault]").description("check whether a topic ma
5944
6039
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
5945
6040
  else emit(await runTopicMapCheck({ vault: v.vault, threshold: opts.threshold }), v.vault);
5946
6041
  });
5947
- program.command("stale [vault]").description("identify stale transcripts and incomplete work items").option("--archive", "move stale items to _archive/", false).option("--days <n>", "staleness threshold in days", (s) => parseInt(s, 10), 3).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
6042
+ program.command("stale [vault]").description("identify stale transcripts and incomplete work items").option("--archive", "move stale items to _archive/", false).option("--days <n>", "staleness threshold in days", (s) => parseInt(s, 10), 3).option("--force-scan", "infer kind/project from filename and content when frontmatter is missing", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5948
6043
  const v = await resolveVaultArg(vault, opts.wiki);
5949
6044
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
5950
- else emit(await runStale({ vault: v.vault, days: opts.days, archive: !!opts.archive }), v.vault);
6045
+ else emit(await runStale({ vault: v.vault, days: opts.days, archive: !!opts.archive, forceScan: !!opts.forceScan }), v.vault);
5951
6046
  });
5952
6047
  program.command("pagesize [vault]").description("report page sizes and flag oversized pages").option("--lines <n>", "max body lines", (s) => parseInt(s, 10), 200).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5953
6048
  const v = await resolveVaultArg(vault, opts.wiki);
@@ -6109,7 +6204,7 @@ program.command("seed [vault]").description("populate a vault with example conte
6109
6204
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
6110
6205
  else emit(await runSeed({ vault: v.vault }), v.vault);
6111
6206
  });
6112
- program.command("observe [vault]").description("create a raw transcript observation entry").requiredOption("--text <text>", "observation text").option("--kind <kind>", "observation kind (note|bug|task|idea|session-log)", "note").option("--project <slug>", "associated project slug").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
6207
+ program.command("observe [vault]").description("create a raw transcript observation entry").requiredOption("--text <text>", "observation text").option("--kind <kind>", "observation kind (note|bug|task|idea|session-log)", "task").option("--project <slug>", "associated project slug (required for task/bug claim detection)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
6113
6208
  const v = await resolveVaultArg(vault, opts.wiki);
6114
6209
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
6115
6210
  else emit(await runObserve({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "skillwiki": "dist/cli.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "skills": "./",
5
5
  "description": "Project-aware Karpathy-style knowledge base for Claude Code: 18 prompt-only skills (wiki-*, proj-*, using-skillwiki) backed by the deterministic `skillwiki` CLI.",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillwiki/skills",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",
@@ -29,22 +29,23 @@ Run `skillwiki lang` at the start. Entry prose and `--human` summaries use the r
29
29
  0. **Resolve vault and language.** Run `skillwiki path` (fail if NO_VAULT_CONFIGURED) and `skillwiki lang`.
30
30
  1. **Parse arguments.** Extract from the user's message:
31
31
  - `text` — the idea/bug/task/note content (required)
32
- - `type` — one of: `idea`, `bug`, `task`, `note` (default: `idea`)
33
- - `project` — optional project slug to cross-reference (e.g., `llm-wiki`)
32
+ - `type` — one of: `idea`, `bug`, `task`, `note` (default: `task`)
33
+ - `project` — project slug for claim detection (e.g., `llm-wiki`). Required for `task` and `bug` types to appear in `skillwiki stale` unclaimed list.
34
34
  2. **Build filename.** Derive a slug from the first ~6 words of the text (lowercased, hyphens for spaces, non-alphanumeric stripped). The capture file is `raw/transcripts/YYYY-MM-DD-{type}-{slug}.md`. Each capture gets its own file — never append to an existing file.
35
35
  3. **Write frontmatter.** Create the file with ad-hoc capture frontmatter:
36
36
  ```yaml
37
37
  ---
38
38
  source_url:
39
- ingested: YYYY-MM-DD
39
+ created: YYYY-MM-DD
40
+ ingested: # filled by ingest pipeline
40
41
  kind: {type}
41
42
  project: "[[{slug}]]"
42
43
  ---
43
44
  ```
44
45
  - Set `kind` to the parsed type (`idea`, `bug`, `task`, `note`).
45
- - If a `project` slug was provided, set `project: "[[slug]]"`.
46
- - If no project, omit the `project` field entirely.
46
+ - Set `project: "[[slug]]"` **required for claim detection**. Without this field, the transcript is invisible to `skillwiki stale`. If no project context exists, ask the user which project this capture belongs to.
47
47
  - `source_url` is null (these are locally originated captures).
48
+ - `created` — today's date. `ingested` is left empty for the ingest pipeline.
48
49
  - No `sha256` — ad-hoc captures are mutable working notes, not immutable sources.
49
50
  4. **Write body.** Below the frontmatter, write:
50
51
  ```markdown
@@ -72,7 +73,8 @@ Each capture is a standalone file with ad-hoc capture frontmatter:
72
73
  ```yaml
73
74
  ---
74
75
  source_url:
75
- ingested: 2026-05-08
76
+ created: 2026-05-08
77
+ ingested:
76
78
  kind: idea
77
79
  project: "[[llm-wiki]]"
78
80
  ---
@@ -84,7 +86,7 @@ Fix the template mismatch between wiki-add-task and the vault template.
84
86
 
85
87
  The `kind` field uses the capture type and must be one of: `idea`, `bug`, `task`, `note` (plus the existing `postmortem`, `session-log`, `meeting-notes`, `other` for non-capture raw sources).
86
88
 
87
- The `project` and `kind` fields can be set independently — they do not require `work_item`. The `work_item` field is only used when the raw source is directly tied to a project work item (set by `proj-work`).
89
+ The `project` field is **required for claim detection**. Without it, `skillwiki stale` cannot surface the transcript as unclaimed work. The `kind` and `project` fields can be set independently — they do not require `work_item`. The `work_item` field is only used when the raw source is directly tied to a project work item (set by `proj-work`).
88
90
 
89
91
  Ad-hoc captures omit `sha256` — they are mutable working notes, not immutable sources. The `sha256` field is reserved for ingested raw sources that require integrity verification.
90
92
 
@@ -106,14 +108,25 @@ Ad-hoc captures omit `sha256` — they are mutable working notes, not immutable
106
108
  When you're not in a Claude session, drop files directly into `raw/transcripts/`:
107
109
 
108
110
  1. Create a `.md` file in `raw/transcripts/` — name it descriptively (e.g., `2026-05-08-idea-fix-template.md`)
109
- 2. Use ad-hoc capture frontmatter: `source_url:`, `ingested:`, `kind:`, and optionally `project:`
111
+ 2. Use ad-hoc capture frontmatter: `source_url:`, `ingested:`, `kind:`, and `project:` (required for `task`/`bug`)
110
112
  3. Write your idea/bug/task/note below the frontmatter
111
113
 
112
- 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.
114
+ **For claim detection**, `kind` must be `task` or `bug` AND `project: "[[slug]]"` must be set. Without both fields, the transcript won't appear in `skillwiki stale` unclaimed list.
115
+
116
+ No special format required — `skillwiki stale --days 0` will detect unclaimed task/bug transcripts on the next run. Mark the type with a heading like `## idea`, `## bug`, `## task`, or just write freeform.
113
117
 
114
118
  ## Dev-loop discovery
115
119
 
116
- 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:
117
- - Create a work item via `proj-work` (for tasks and bugs)
120
+ `skillwiki stale` detects unclaimed transcripts automatically. A transcript is **unclaimed** when:
121
+ - `kind` is `task` or `bug`
122
+ - `project` field is set (e.g., `project: "[[llm-wiki]]"`)
123
+ - No work item matches it (via date-prefix or `source:` frontmatter reference in spec.md)
124
+
125
+ Run `skillwiki stale --days 0 --human` to see all unclaimed transcripts. The output includes an `unclaimed_transcripts` section listing each capture that needs a work item.
126
+
127
+ The agent then decides whether to:
128
+ - Create a work item via `proj-work` (for tasks and bugs) — set `source:` in spec.md to the transcript path for cross-date linking
118
129
  - Ingest as a knowledge page via `wiki-ingest` (for ideas with sources)
119
130
  - Leave in place (for notes that don't need action yet)
131
+
132
+ Transcripts without `kind` or `project` fields (e.g., `loop-cycle-*` session logs) are excluded from claim detection.
@@ -0,0 +1,11 @@
1
+ ---
2
+ source_url:
3
+ created: {{date:YYYY-MM-DD}}
4
+ ingested: # filled by ingest pipeline
5
+ kind: {{kind}}
6
+ project: "[[{{project}}]]"
7
+ ---
8
+
9
+ # {{kind}}: {{title}}
10
+
11
+ {{description}}