skillwiki 0.4.2 → 0.4.4-beta.1

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
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-TPS5XD2J.js";
9
9
 
10
10
  // src/cli.ts
11
- import { readFileSync as readFileSync9 } from "fs";
11
+ import { readFileSync as readFileSync10 } from "fs";
12
12
  import { join as join38 } from "path";
13
13
  import { Command } from "commander";
14
14
 
@@ -2402,6 +2402,46 @@ async function runDedup(input) {
2402
2402
  };
2403
2403
  }
2404
2404
 
2405
+ // src/commands/raw-body-dedup.ts
2406
+ import { createHash as createHash2 } from "crypto";
2407
+ async function runRawBodyDedup(vault) {
2408
+ const scan = await scanVault(vault);
2409
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
2410
+ const bodyHashMap = /* @__PURE__ */ new Map();
2411
+ let totalFiles = 0;
2412
+ for (const raw of scan.data.raw) {
2413
+ const text = await readPage(raw);
2414
+ const split = splitFrontmatter(text);
2415
+ if (!split.ok) continue;
2416
+ totalFiles++;
2417
+ const bodyHash = createHash2("sha256").update(split.data.body).digest("hex");
2418
+ const fm = extractFrontmatter(text);
2419
+ let fmSha256 = null;
2420
+ if (fm.ok && typeof fm.data.sha256 === "string" && fm.data.sha256.length === 64) {
2421
+ fmSha256 = fm.data.sha256;
2422
+ }
2423
+ const existing = bodyHashMap.get(bodyHash);
2424
+ if (existing) {
2425
+ existing.push({ relPath: raw.relPath, sha256: fmSha256 });
2426
+ } else {
2427
+ bodyHashMap.set(bodyHash, [{ relPath: raw.relPath, sha256: fmSha256 }]);
2428
+ }
2429
+ }
2430
+ const duplicates = [];
2431
+ for (const [bodyHash, files] of bodyHashMap) {
2432
+ if (files.length < 2) continue;
2433
+ const uniqueShas = new Set(files.map((f) => f.sha256));
2434
+ const allHaveSameValidSha = uniqueShas.size === 1 && files.every((f) => f.sha256 !== null);
2435
+ if (!allHaveSameValidSha) {
2436
+ duplicates.push({ bodyHash, files });
2437
+ }
2438
+ }
2439
+ return {
2440
+ exitCode: 0,
2441
+ result: ok({ scanned: totalFiles, duplicates })
2442
+ };
2443
+ }
2444
+
2405
2445
  // src/commands/lint.ts
2406
2446
  var STRUCT_MIN_BODY_LINES = 60;
2407
2447
  var STRUCT_MIN_SECTIONS = 3;
@@ -2434,7 +2474,7 @@ function extractSourceEntries(rawFm) {
2434
2474
  return entries;
2435
2475
  }
2436
2476
  var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "broken_sources", "tag_not_in_taxonomy"];
2437
- var WARNING_ORDER = ["index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "orphans", "compound_refs", "legacy_citation_style", "orphaned_citations", "duplicate_frontmatter", "work_item_health", "orphaned_project_pages", "missing_overview"];
2477
+ var WARNING_ORDER = ["raw_body_duplicate", "raw_subdirectory_duplicate", "index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "orphans", "compound_refs", "legacy_citation_style", "orphaned_citations", "duplicate_frontmatter", "work_item_health", "orphaned_project_pages", "missing_overview"];
2438
2478
  var INFO_ORDER = ["bridges", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation", "missing_tldr", "missing_diagram"];
2439
2479
  async function runLint(input) {
2440
2480
  const buckets = {};
@@ -2484,12 +2524,41 @@ async function runLint(input) {
2484
2524
  }
2485
2525
  const dedup = await runDedup({ vault: input.vault });
2486
2526
  if (dedup.result.ok && dedup.result.data.duplicates.length > 0) buckets.raw_dedup = dedup.result.data.duplicates;
2527
+ const bodyDedup = await runRawBodyDedup(input.vault);
2528
+ if (bodyDedup.result.ok && bodyDedup.result.data.duplicates.length > 0) {
2529
+ buckets.raw_body_duplicate = bodyDedup.result.data.duplicates.map((d) => ({
2530
+ body_hash: d.bodyHash.slice(0, 12),
2531
+ files: d.files.map((f) => `${f.relPath} (sha256: ${f.sha256 ?? "none"})`)
2532
+ }));
2533
+ }
2487
2534
  const compoundRefs = await validateCompoundReferences(input.vault);
2488
2535
  if (compoundRefs.ok && compoundRefs.data.length > 0) buckets.compound_refs = compoundRefs.data;
2489
2536
  const scan = await scanVault(input.vault);
2490
2537
  const allPages = scan.ok ? [...scan.data.typedKnowledge, ...scan.data.raw, ...scan.data.workItems, ...scan.data.compound] : [];
2491
2538
  const slugs = scan.ok ? buildSlugMap(allPages) : /* @__PURE__ */ new Map();
2492
2539
  if (scan.ok) {
2540
+ const subDirDupes = [];
2541
+ const flatStems = /* @__PURE__ */ new Map();
2542
+ const deepFiles = [];
2543
+ for (const raw of scan.data.raw) {
2544
+ const parts = raw.relPath.split("/");
2545
+ if (parts.length === 3) {
2546
+ const stem = parts[2].replace(/\.md$/, "");
2547
+ flatStems.set(`${parts[1]}/${stem}`, raw.relPath);
2548
+ } else if (parts.length > 3) {
2549
+ const stem = parts[parts.length - 1].replace(/\.md$/, "");
2550
+ deepFiles.push({ relPath: raw.relPath, stem, parentType: parts[1] });
2551
+ }
2552
+ }
2553
+ for (const df of deepFiles) {
2554
+ const flatPath = flatStems.get(`${df.parentType}/${df.stem}`);
2555
+ if (flatPath) {
2556
+ subDirDupes.push(`${df.relPath} -> duplicate of ${flatPath}`);
2557
+ }
2558
+ }
2559
+ if (subDirDupes.length > 0) {
2560
+ buckets.raw_subdirectory_duplicate = subDirDupes;
2561
+ }
2493
2562
  const legacyPages = [];
2494
2563
  const orphanedPages = [];
2495
2564
  const structFlags = [];
@@ -2975,9 +3044,10 @@ async function runConfigPath(input) {
2975
3044
  }
2976
3045
 
2977
3046
  // src/commands/doctor.ts
2978
- import { existsSync as existsSync6, lstatSync, readlinkSync, readdirSync, statSync as statSync2 } from "fs";
3047
+ import { existsSync as existsSync6, lstatSync, readlinkSync, readdirSync, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
2979
3048
  import { join as join22, resolve as resolve4 } from "path";
2980
3049
  import { execSync } from "child_process";
3050
+ import { platform } from "os";
2981
3051
 
2982
3052
  // src/utils/auto-update.ts
2983
3053
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
@@ -3397,6 +3467,88 @@ function checkSyncLastPush(resolvedPath) {
3397
3467
  }
3398
3468
  return check("pass", "sync_last_push", "Vault sync recency", `Last push: ${dateStr} (${daysSince2} day(s) ago)`);
3399
3469
  }
3470
+ function detectFuseMount(vaultPath) {
3471
+ const os = platform();
3472
+ try {
3473
+ if (os === "linux") {
3474
+ const mounts = readFileSync6("/proc/mounts", "utf8");
3475
+ let best = null;
3476
+ for (const line of mounts.split("\n")) {
3477
+ const parts = line.split(" ");
3478
+ if (parts.length < 3) continue;
3479
+ const point = parts[1];
3480
+ const fs = parts[2];
3481
+ if (vaultPath.startsWith(point) && (!best || point.length > best.point.length)) {
3482
+ best = { point, fs };
3483
+ }
3484
+ }
3485
+ if (best && best.fs.includes("fuse")) return best.point;
3486
+ } else if (os === "darwin") {
3487
+ const out = execSync("mount", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
3488
+ let best = null;
3489
+ for (const line of out.split("\n")) {
3490
+ const match = line.match(/^(\S+) on (\S+) \((.*?)\)/);
3491
+ if (!match) continue;
3492
+ const point = match[2];
3493
+ const opts = match[3];
3494
+ if (opts.includes("fuse") && vaultPath.startsWith(point) && (!best || point.length > best.point.length)) {
3495
+ best = { point };
3496
+ }
3497
+ }
3498
+ if (best) return best.point;
3499
+ }
3500
+ } catch {
3501
+ }
3502
+ return null;
3503
+ }
3504
+ function checkS3MountPerf(resolvedPath) {
3505
+ if (resolvedPath === void 0) {
3506
+ return check("pass", "s3_mount_perf", "S3 mount performance", "No vault path \u2014 check skipped");
3507
+ }
3508
+ const mountPoint = detectFuseMount(resolvedPath);
3509
+ if (!mountPoint) {
3510
+ return check("pass", "s3_mount_perf", "S3 mount performance", "local disk");
3511
+ }
3512
+ const conceptsDir = join22(resolvedPath, "concepts");
3513
+ if (!existsSync6(conceptsDir)) {
3514
+ return check("pass", "s3_mount_perf", "S3 mount performance", `S3 FUSE mount (${mountPoint}), no concepts/ to benchmark`);
3515
+ }
3516
+ const start = Date.now();
3517
+ let timedOut = false;
3518
+ try {
3519
+ execSync(`rg -l "." "${conceptsDir}"`, {
3520
+ timeout: 5e3,
3521
+ encoding: "utf8",
3522
+ stdio: ["pipe", "pipe", "pipe"]
3523
+ });
3524
+ } catch (e) {
3525
+ if (e.killed || e.status === null && e.signal === "SIGTERM") {
3526
+ timedOut = true;
3527
+ } else if (e.code === "ENOENT") {
3528
+ return check(
3529
+ "info",
3530
+ "s3_mount_perf",
3531
+ "S3 mount performance",
3532
+ `S3 FUSE mount (${mountPoint}) \u2014 rg not found at runtime, benchmark skipped`
3533
+ );
3534
+ }
3535
+ }
3536
+ const elapsed = (Date.now() - start) / 1e3;
3537
+ if (timedOut || elapsed >= 3) {
3538
+ return check(
3539
+ "warn",
3540
+ "s3_mount_perf",
3541
+ "S3 mount performance",
3542
+ `S3 FUSE mount (${mountPoint}) with cold cache (rg scan: >3s). Vault scans may exceed 60s. Consider running wiki-cache-warm or checking rclone-wiki.service.`
3543
+ );
3544
+ }
3545
+ return check(
3546
+ "pass",
3547
+ "s3_mount_perf",
3548
+ "S3 mount performance",
3549
+ `S3 FUSE mount, cache warm (rg scan: ${elapsed.toFixed(3)}s)`
3550
+ );
3551
+ }
3400
3552
  function findSkillMd(dir) {
3401
3553
  const results = [];
3402
3554
  let entries;
@@ -3449,6 +3601,7 @@ async function runDoctor(input) {
3449
3601
  checks.push(checkVaultGitRemote(resolvedPath));
3450
3602
  checks.push(checkSyncLastPush(resolvedPath));
3451
3603
  checks.push(checkDotStoreClean(resolvedPath));
3604
+ checks.push(checkS3MountPerf(resolvedPath));
3452
3605
  checks.push(checkSkillsInstalled(input.home, input.cwd));
3453
3606
  checks.push(checkDuplicateSkills(input.home));
3454
3607
  checks.push(checkNpmUpdate(input.home, input.currentVersion));
@@ -3522,7 +3675,7 @@ async function runArchive(input) {
3522
3675
  }
3523
3676
 
3524
3677
  // src/commands/drift.ts
3525
- import { createHash as createHash2 } from "crypto";
3678
+ import { createHash as createHash3 } from "crypto";
3526
3679
  import { writeFile as writeFile10 } from "fs/promises";
3527
3680
 
3528
3681
  // src/utils/fetch.ts
@@ -3602,7 +3755,7 @@ async function runDrift(input) {
3602
3755
  });
3603
3756
  continue;
3604
3757
  }
3605
- const currentHash = createHash2("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
3758
+ const currentHash = createHash3("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
3606
3759
  const drifted2 = currentHash !== storedHash;
3607
3760
  if (drifted2 && input.apply) {
3608
3761
  const newFm = rawFrontmatter.replace(/^sha256:\s*[a-f0-9]+$/m, `sha256: ${currentHash}`);
@@ -3880,7 +4033,7 @@ ${newBody}`;
3880
4033
 
3881
4034
  // src/commands/update.ts
3882
4035
  import { execSync as execSync2 } from "child_process";
3883
- import { readFileSync as readFileSync6 } from "fs";
4036
+ import { readFileSync as readFileSync7 } from "fs";
3884
4037
  import { join as join24 } from "path";
3885
4038
  function resolveGlobalSkillsRoot() {
3886
4039
  try {
@@ -3910,7 +4063,7 @@ async function refreshInstalledSkills(target) {
3910
4063
  }
3911
4064
  async function runUpdate(input) {
3912
4065
  const pkg2 = JSON.parse(
3913
- readFileSync6(new URL("../../package.json", import.meta.url), "utf8")
4066
+ readFileSync7(new URL("../../package.json", import.meta.url), "utf8")
3914
4067
  );
3915
4068
  const currentVersion = pkg2.version;
3916
4069
  const tag = input.distTag ?? "latest";
@@ -3984,12 +4137,12 @@ async function runUpdate(input) {
3984
4137
 
3985
4138
  // src/commands/self-update.ts
3986
4139
  import { execSync as execSync3 } from "child_process";
3987
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
4140
+ import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
3988
4141
  import { join as join25 } from "path";
3989
4142
  var DEFAULT_SOURCE_ROOT_SUFFIX = "/Desktop/code/llm-wiki";
3990
4143
  async function runSelfUpdate(input) {
3991
4144
  const currentVersion = JSON.parse(
3992
- readFileSync7(new URL("../../package.json", import.meta.url), "utf8")
4145
+ readFileSync8(new URL("../../package.json", import.meta.url), "utf8")
3993
4146
  ).version;
3994
4147
  const sourceRoot = input.sourceRoot ?? `${input.home}${DEFAULT_SOURCE_ROOT_SUFFIX}`;
3995
4148
  const localPkgPath = join25(sourceRoot, "packages", "cli", "package.json");
@@ -4000,7 +4153,7 @@ async function runSelfUpdate(input) {
4000
4153
  if (hasLocalSource) {
4001
4154
  source = "local";
4002
4155
  try {
4003
- availableVersion = JSON.parse(readFileSync7(localPkgPath, "utf8")).version ?? null;
4156
+ availableVersion = JSON.parse(readFileSync8(localPkgPath, "utf8")).version ?? null;
4004
4157
  } catch {
4005
4158
  availableVersion = null;
4006
4159
  }
@@ -4058,7 +4211,7 @@ async function runSelfUpdate(input) {
4058
4211
  }
4059
4212
  const newVersion = (() => {
4060
4213
  try {
4061
- return JSON.parse(readFileSync7(localPkgPath, "utf8")).version ?? "unknown";
4214
+ return JSON.parse(readFileSync8(localPkgPath, "utf8")).version ?? "unknown";
4062
4215
  } catch {
4063
4216
  return "unknown";
4064
4217
  }
@@ -4587,7 +4740,7 @@ no compound entries found`;
4587
4740
  import { mkdir as mkdir11, writeFile as writeFile15 } from "fs/promises";
4588
4741
  import { existsSync as existsSync9, statSync as statSync3 } from "fs";
4589
4742
  import { join as join29 } from "path";
4590
- import { createHash as createHash3 } from "crypto";
4743
+ import { createHash as createHash4 } from "crypto";
4591
4744
  var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
4592
4745
  function slugify2(text) {
4593
4746
  const words = text.trim().split(/\s+/).slice(0, 6).join("-").toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
@@ -4631,7 +4784,7 @@ async function runObserve(input) {
4631
4784
  const body = `
4632
4785
  ${input.text.trim()}
4633
4786
  `;
4634
- const sha256 = createHash3("sha256").update(Buffer.from(body, "utf8")).digest("hex");
4787
+ const sha256 = createHash4("sha256").update(Buffer.from(body, "utf8")).digest("hex");
4635
4788
  const frontmatterLines = [
4636
4789
  "---",
4637
4790
  "source_url:",
@@ -4669,7 +4822,7 @@ ${input.text.trim()}
4669
4822
  // src/commands/ingest.ts
4670
4823
  import { readFile as readFile20, writeFile as writeFile16, mkdir as mkdir12 } from "fs/promises";
4671
4824
  import { join as join30 } from "path";
4672
- import { createHash as createHash4 } from "crypto";
4825
+ import { createHash as createHash5 } from "crypto";
4673
4826
  var ALLOWED_TYPES = /* @__PURE__ */ new Set(["entity", "concept", "comparison", "query"]);
4674
4827
  var TYPE_DIR = {
4675
4828
  entity: "entities",
@@ -4835,7 +4988,7 @@ async function runIngest(input) {
4835
4988
  };
4836
4989
  }
4837
4990
  }
4838
- const sha256 = createHash4("sha256").update(Buffer.from(sourceContent, "utf8")).digest("hex");
4991
+ const sha256 = createHash5("sha256").update(Buffer.from(sourceContent, "utf8")).digest("hex");
4839
4992
  const today = todayIso();
4840
4993
  const slug = slugify3(input.title);
4841
4994
  const tags = input.tags && input.tags.length > 0 ? input.tags : [];
@@ -5324,7 +5477,7 @@ async function runSyncPull(input) {
5324
5477
  }
5325
5478
 
5326
5479
  // src/commands/backup.ts
5327
- import { statSync as statSync4, readdirSync as readdirSync2, readFileSync as readFileSync8, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
5480
+ import { statSync as statSync4, readdirSync as readdirSync2, readFileSync as readFileSync9, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
5328
5481
  import { join as join32, relative as relative3, dirname as dirname11 } from "path";
5329
5482
  import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
5330
5483
 
@@ -5391,7 +5544,7 @@ async function runBackupSync(input) {
5391
5544
  continue;
5392
5545
  }
5393
5546
  try {
5394
- const body = readFileSync8(absPath);
5547
+ const body = readFileSync9(absPath);
5395
5548
  await client.send(new PutObjectCommand({ Bucket: input.bucket, Key: relPath, Body: body }));
5396
5549
  uploaded++;
5397
5550
  } catch {
@@ -6018,7 +6171,7 @@ async function postCommit(vault, exitCode) {
6018
6171
  }
6019
6172
 
6020
6173
  // src/cli.ts
6021
- var pkg = JSON.parse(readFileSync9(new URL("../package.json", import.meta.url), "utf8"));
6174
+ var pkg = JSON.parse(readFileSync10(new URL("../package.json", import.meta.url), "utf8"));
6022
6175
  var program = new Command();
6023
6176
  program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version(pkg.version);
6024
6177
  program.option("--human", "render terminal-readable output instead of JSON");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.4.2",
3
+ "version": "0.4.4-beta.1",
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.4.2",
3
+ "version": "0.4.4-beta.1",
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",
3
- "version": "0.4.2",
3
+ "version": "0.4.4-beta.1",
4
4
  "description": "Project-aware Karpathy-style knowledge base for Codex with 18 prompt-only skills backed by the deterministic skillwiki CLI.",
5
5
  "author": {
6
6
  "name": "karlorz",
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: proj-decide
3
+ description: Use this agent when recording architectural decisions during automated maintenance cycles. Typical triggers include dev-loop IDLE DISCOVERY maintenance, capturing design decisions from work items, or generalizing decisions into concept pages. See "When to invoke" in the agent body for worked scenarios.
4
+ model: sonnet
5
+ color: yellow
6
+ tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
7
+ ---
8
+
9
+ You are an architectural decision recorder specializing in writing ADRs and optionally promoting generalizable decisions to concept pages. You operate autonomously during maintenance cycles — no user interaction expected.
10
+
11
+ ## When to invoke
12
+
13
+ - **ADR capture.** Dev-loop spawns you to record an architectural decision from a completed work item.
14
+ - **Decision generalization.** A decision recorded in a project likely applies beyond it — create both ADR and concept page.
15
+ - **Periodic distillation.** Part of dev-loop IDLE DISCOVERY: scan work item retros for undocumented decisions.
16
+
17
+ **Your Core Responsibilities:**
18
+ 1. Compose an ADR in `projects/{slug}/architecture/YYYY-MM-DD-{adr-slug}.md`
19
+ 2. Validate the ADR with `skillwiki validate`
20
+ 3. Check if the decision generalizes beyond the project — if so, create a concepts/ page
21
+ 4. Apply all writes in order
22
+
23
+ **Execution Process:**
24
+
25
+ 1. **Identify context.** Determine project slug from the task prompt. If no project context, default to `playground`.
26
+ 2. **Compose the ADR.** Write to `projects/{slug}/architecture/YYYY-MM-DD-{adr-slug}.md`:
27
+ - Frontmatter: `kind: decision`, `status: in-progress` (or `completed` if already implemented), `project: "[[slug]]"`
28
+ - Body sections: **Context** (why this decision matters), **Decision** (what was chosen), **Consequences** (what follows from this choice), **Alternatives Considered** (rejected options and why)
29
+ 3. **Validate.** Run `skillwiki validate <adr>`. If non-zero, fix and re-validate. Do NOT proceed until validation passes.
30
+ 4. **Generalization check.** If the decision applies beyond this project, create a `concepts/` page with:
31
+ - `provenance: project` (or `mixed` if also research-informed)
32
+ - `provenance_projects: ["[[slug]]"]`
33
+ - `## TL;DR` as first section
34
+ - Body summarizing the decision pattern generically
35
+ - `^[raw/...]` citations where applicable
36
+ - Validate this page too before proceeding.
37
+ 5. **Apply writes in order:** ADR → (optional) concept page → vault `index.md` → vault `log.md` → project `log.md`.
38
+
39
+ **Output Format:**
40
+ Return:
41
+ - ADR path and slug
42
+ - Decision summary (1-2 sentences)
43
+ - Whether a concept page was also created (and path if so)
44
+ - Validation results for both pages
45
+ - All log entries appended
46
+
47
+ **Stop Conditions:**
48
+ - `skillwiki validate` returns non-zero on either page (after retry)
49
+ - Insufficient context to compose a meaningful ADR
50
+
51
+ **Forbidden:**
52
+ - Filing a concept page without explicit `provenance:`
53
+ - Skipping the generalization check
54
+ - Updating index/logs before `validate` passes
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: proj-distill
3
+ description: Use this agent when promoting project compound entries or retro patterns into vault concept pages during automated maintenance cycles. Typical triggers include dev-loop IDLE DISCOVERY maintenance, compound-entry generalization, or retro-sourced pattern extraction. See "When to invoke" in the agent body for worked scenarios.
4
+ model: sonnet
5
+ color: green
6
+ tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
7
+ ---
8
+
9
+ You are a pattern distiller specializing in the E4 2-step process: analyze project compound entries (or retros) for generalizable patterns, then generate vault concept pages with provenance. You operate autonomously during maintenance cycles — no user interaction expected.
10
+
11
+ ## When to invoke
12
+
13
+ - **Compound promotion.** Dev-loop spawns you to check `projects/{slug}/compound/` for entries ready for generalization.
14
+ - **Retro mining.** Project retro entries in vault `log.md` contain `Generalize?: yes` flags — extract recurring patterns.
15
+ - **Periodic distillation.** Part of dev-loop IDLE DISCOVERY: scan for unwritten compound entries and promote them.
16
+
17
+ **Your Core Responsibilities:**
18
+ 1. Read source compound entries or retro logs and identify generalizable patterns
19
+ 2. Output a candidate concept outline — STOP if no clear universal pattern
20
+ 3. Compose the vault concept page with project provenance
21
+ 4. Set backlinks and apply all writes in order
22
+
23
+ **Execution Process:**
24
+
25
+ ### Step 1 — Analyze
26
+ 1. Check `projects/{slug}/compound/` first. If no project context, use `playground`.
27
+ 2. Read the source compound entry + linked work items. If no compound entries, fall back to retro entries in `{vault}/log.md` (lines matching `## [YYYY-MM-DD] retro`).
28
+ 3. For retro-sourced analysis: collect all retros for the project, focus on `Generalize?: yes` entries, group by recurring theme (≥2 occurrences = candidate concept).
29
+ 4. Output a candidate concept outline. **STOP if no clear universal pattern is found** — surface the reasoning instead of forcing a page.
30
+
31
+ ### Step 2 — Generate (only if Step 1 found a pattern)
32
+ 5. Compose the vault concept page:
33
+ - `provenance: project` and `provenance_projects: ["[[slug]]"]`
34
+ - `tags:` from `{vault}/SCHEMA.md` taxonomy only. Never derive tags from prose text. If no relevant taxonomy tag, use `[dev-loop]`.
35
+ - `## TL;DR` as first section
36
+ - Body with `^[raw/...]` citations
37
+ 6. Validate with `skillwiki validate <page>`. If non-zero, fix and re-validate.
38
+
39
+ ### Step 3 — Backlink
40
+ 7. Set `promoted_to: "[[concept-slug]]"` on the source compound entry. For retro-sourced distillation, skip backlink (log.md is append-only) — instead add `sources:` citing the vault log with date range.
41
+
42
+ ### Step 4 — Apply writes in order
43
+ 8. Vault concept page → backlink update → project `log.md` → vault `index.md` → vault `log.md`.
44
+
45
+ **Output Format:**
46
+ Return:
47
+ - Source analyzed (compound entry path or retro date range)
48
+ - Pattern identified (theme, recurrence count)
49
+ - Whether distillation proceeded or stopped at Step 1 (with reasoning)
50
+ - If generated: concept page path, validation result, backlink applied
51
+ - All log entries appended
52
+
53
+ **Stop Conditions:**
54
+ - No clear universal pattern found in Step 1
55
+ - `skillwiki validate` returns non-zero (after retry)
56
+
57
+ **Forbidden:**
58
+ - Skipping Step 1 (analysis before generation)
59
+ - Inventing new tags not in SCHEMA.md taxonomy
60
+ - Updating index/logs before `validate` passes
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: proj-init
3
+ description: Use this agent when bootstrapping a new project workspace during automated setup cycles. Typical triggers include dev-loop project initialization, new-project scaffolding, or creating a workspace for an existing codebase. See "When to invoke" in the agent body for worked scenarios.
4
+ model: sonnet
5
+ color: green
6
+ tools: ["Read", "Write", "Bash", "Grep", "Glob"]
7
+ ---
8
+
9
+ You are a project workspace bootstrapper specializing in creating the `projects/{slug}/` directory structure with README, requirements/, architecture/, work/, and compound/. You operate autonomously — the project slug and intent are in your task prompt.
10
+
11
+ ## When to invoke
12
+
13
+ - **New project.** Dev-loop spawns you to scaffold a workspace for a new project.
14
+ - **Vault onboarding.** An existing codebase needs a vault project workspace.
15
+ - **Playground init.** Ensure the `playground` project exists for unclassified work.
16
+
17
+ **Your Core Responsibilities:**
18
+ 1. Verify the project slug doesn't conflict with existing projects
19
+ 2. Create the directory structure
20
+ 3. Render README.md from template
21
+ 4. Update vault index.md and log.md
22
+
23
+ **Execution Process:**
24
+
25
+ 1. **Resolve vault.** Run `skillwiki path`. If NO_VAULT_CONFIGURED, report failure and STOP.
26
+ 2. **Verify slug.** The slug is in your task prompt. Check that `projects/{slug}/` does not exist. If it does, report and STOP.
27
+ 3. **Create structure:**
28
+ ```
29
+ projects/{slug}/
30
+ ├── requirements/
31
+ ├── architecture/
32
+ ├── work/
33
+ └── compound/
34
+ ```
35
+ 4. **Render README.** Create `projects/{slug}/README.md` with:
36
+ - Project name and one-line intent
37
+ - Section: Knowledge Pages (placeholder for future index entries)
38
+ - Section: Work Items (placeholder)
39
+ - Section: Architecture (placeholder for ADRs)
40
+ - Creation date
41
+ 5. **Update index.** Add to `{vault}/index.md` Projects section: `- [[projects/{slug}]]`.
42
+ 6. **Log.** Append to `{vault}/log.md`: `## [{date}] project-init | {slug} initialized.`
43
+
44
+ **Output Format:**
45
+ Return:
46
+ - Project slug and path
47
+ - Directories created
48
+ - README.md path
49
+ - Index.md entry added
50
+ - Log entry appended
51
+
52
+ **Stop Conditions:**
53
+ - `projects/{slug}/` already exists
54
+ - `skillwiki path` returns NO_VAULT_CONFIGURED
55
+
56
+ **Forbidden:**
57
+ - Modifying any other project's files
58
+ - Creating the workspace without updating index.md
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: proj-work
3
+ description: Use this agent when creating or executing work items during automated development cycles. Typical triggers include dev-loop work item creation from captured tasks, executing existing work items, or managing status transitions. See "When to invoke" in the agent body for worked scenarios.
4
+ model: sonnet
5
+ color: green
6
+ tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
7
+ ---
8
+
9
+ You are a project work item manager specializing in creating and executing work items under `projects/{slug}/work/YYYY-MM-DD-{slug}/`. You handle both creation (scaffolding spec + plan + tasks) and execution (verifying DONE claims against disk, applying missing fixes). You operate autonomously during maintenance cycles.
10
+
11
+ ## When to invoke
12
+
13
+ - **Work item creation.** Dev-loop spawns you to scaffold a new feature/bugfix/refactor work item.
14
+ - **Work item execution.** Dev-loop spawns you to run through an existing work item's task list.
15
+ - **Status management.** Transition work items through planned → in-progress → completed.
16
+
17
+ **Your Core Responsibilities:**
18
+ 1. For creation: scaffold work folder with spec.md + tasks.md
19
+ 2. For execution: read spec/tasks, verify every DONE claim against disk, apply missing fixes
20
+ 3. Validate all pages, manage status transitions, update logs
21
+
22
+ **Execution Process:**
23
+
24
+ ### Creating a New Work Item
25
+ 1. **Resolve vault.** Run `skillwiki path`.
26
+ 2. **Determine slug and kind.** From task prompt: kind (`feature` | `issue` | `refactor` | `decision`) and work slug.
27
+ 3. **Create folder.** `projects/{slug}/work/YYYY-MM-DD-{work-slug}/`.
28
+ 4. **Write spec.md.** Frontmatter with kind, status=planned, project wikilink. Body with context and scope.
29
+ 5. **Write tasks.md.** Break work into task checklist.
30
+ 6. **Validate.** `skillwiki validate <spec.md>`. If non-zero, fix and STOP.
31
+ 7. **Emit redirect paths.** These are where PRD skills should write their output:
32
+ - spec → `<vault>/projects/{slug}/work/YYYY-MM-DD-{work-slug}/spec.md`
33
+ - plan → `<vault>/projects/{slug}/work/YYYY-MM-DD-{work-slug}/plan.md`
34
+ 8. **Log.** Append to vault `log.md`.
35
+
36
+ ### Executing an Existing Work Item
37
+ 1. **Resolve work folder.** `<vault>/projects/{slug}/work/YYYY-MM-DD-{slug}/`.
38
+ 2. **Read spec.md and tasks.md** in full.
39
+ 3. **Verify every DONE claim against disk.** This is critical:
40
+ - Check file existence, content, config values on disk
41
+ - Cross-reference crontab entries, script timeouts, config settings
42
+ - Trust nothing in the wiki alone — validate against filesystem
43
+ 4. **Apply missing fixes.** For items claimed DONE but not actually applied, apply the fix.
44
+ 5. **Update status.** Set `status: complete` with `completed:` date only when ALL fixes verified.
45
+ 6. **Log.** Append to vault `log.md` on status transitions.
46
+
47
+ **Output Format:**
48
+ Return:
49
+ - Work item path
50
+ - Kind and slug
51
+ - If creating: spec.md and tasks.md paths, redirect paths for PRD skills
52
+ - If executing: DONE claims verified (count), fixes applied (count), final status
53
+ - Log entries appended
54
+
55
+ **Stop Conditions:**
56
+ - `validate` non-zero
57
+ - Conflicting work folder name
58
+ - No project context and no `playground` fallback
59
+
60
+ **Forbidden:**
61
+ - Writing spec/plan files outside the work folder
62
+ - Marking `status: completed` without a `completed:` date
63
+ - Accepting tasks.md DONE labels without independent disk verification
64
+ - Re-marking tasks as DONE without actually applying the fix