workflow-toolkit 1.0.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.
Files changed (142) hide show
  1. package/.agents/skills/ponytail/SKILL.md +111 -0
  2. package/.agents/skills/ponytail-audit/SKILL.md +37 -0
  3. package/.agents/skills/ponytail-debt/SKILL.md +41 -0
  4. package/.agents/skills/ponytail-gain/SKILL.md +47 -0
  5. package/.agents/skills/ponytail-help/SKILL.md +70 -0
  6. package/.agents/skills/ponytail-review/SKILL.md +52 -0
  7. package/.agents/skills/prompt-review/SKILL.md +60 -0
  8. package/.agents/skills/wtk/SKILL.md +64 -0
  9. package/.agents/skills/wtk/references/artifacts.md +82 -0
  10. package/.agents/skills/wtk/references/evidence.md +68 -0
  11. package/.agents/skills/wtk/references/git.md +75 -0
  12. package/.agents/skills/wtk/references/test-contract.md +76 -0
  13. package/.agents/skills/wtk/references/validation.md +127 -0
  14. package/.agents/skills/wtk-config/SKILL.md +102 -0
  15. package/.agents/skills/wtk-config/assets/agents/claude/deep-reviewer.md +40 -0
  16. package/.agents/skills/wtk-config/assets/agents/claude/designer.md +57 -0
  17. package/.agents/skills/wtk-config/assets/agents/claude/explorer.md +41 -0
  18. package/.agents/skills/wtk-config/assets/agents/claude/implementer.md +57 -0
  19. package/.agents/skills/wtk-config/assets/agents/claude/planner.md +68 -0
  20. package/.agents/skills/wtk-config/assets/agents/claude/verifier.md +80 -0
  21. package/.agents/skills/wtk-config/assets/agents/codex/deep-reviewer.toml +41 -0
  22. package/.agents/skills/wtk-config/assets/agents/codex/designer.toml +55 -0
  23. package/.agents/skills/wtk-config/assets/agents/codex/explorer.toml +35 -0
  24. package/.agents/skills/wtk-config/assets/agents/codex/implementer.toml +52 -0
  25. package/.agents/skills/wtk-config/assets/agents/codex/planner.toml +66 -0
  26. package/.agents/skills/wtk-config/assets/agents/codex/verifier.toml +78 -0
  27. package/.agents/skills/wtk-config/assets/agents/cursor/deep-reviewer.md +38 -0
  28. package/.agents/skills/wtk-config/assets/agents/cursor/designer.md +55 -0
  29. package/.agents/skills/wtk-config/assets/agents/cursor/explorer.md +39 -0
  30. package/.agents/skills/wtk-config/assets/agents/cursor/implementer.md +55 -0
  31. package/.agents/skills/wtk-config/assets/agents/cursor/planner.md +66 -0
  32. package/.agents/skills/wtk-config/assets/agents/cursor/verifier.md +79 -0
  33. package/.agents/skills/wtk-config/scripts/ad-index.py +123 -0
  34. package/.agents/skills/wtk-config/scripts/repository_intelligence.py +671 -0
  35. package/.agents/skills/wtk-config/scripts/workflow_config.py +972 -0
  36. package/.agents/skills/wtk-deep-review/SKILL.md +164 -0
  37. package/.agents/skills/wtk-deep-review/assets/PROMPT.md +56 -0
  38. package/.agents/skills/wtk-deep-review/assets/REVIEW_UI.html +1340 -0
  39. package/.agents/skills/wtk-deep-review/assets/findings.schema.json +113 -0
  40. package/.agents/skills/wtk-deep-review/references/context-pack.md +88 -0
  41. package/.agents/skills/wtk-deep-review/references/orchestration.md +164 -0
  42. package/.agents/skills/wtk-deep-review/references/output-contracts.md +149 -0
  43. package/.agents/skills/wtk-deep-review/references/publish-github.md +83 -0
  44. package/.agents/skills/wtk-deep-review/references/state-and-learnings.md +64 -0
  45. package/.agents/skills/wtk-deep-review/references/subagent-runtimes.md +33 -0
  46. package/.agents/skills/wtk-deep-review/references/taxonomy.md +70 -0
  47. package/.agents/skills/wtk-deep-review/scripts/_common.py +371 -0
  48. package/.agents/skills/wtk-deep-review/scripts/build_jobs.py +561 -0
  49. package/.agents/skills/wtk-deep-review/scripts/build_knowledge.py +349 -0
  50. package/.agents/skills/wtk-deep-review/scripts/build_manifest.py +479 -0
  51. package/.agents/skills/wtk-deep-review/scripts/graft_context.py +160 -0
  52. package/.agents/skills/wtk-deep-review/scripts/graphify_context.py +75 -0
  53. package/.agents/skills/wtk-deep-review/scripts/merge_findings.py +313 -0
  54. package/.agents/skills/wtk-deep-review/scripts/render_html.py +240 -0
  55. package/.agents/skills/wtk-deep-review/scripts/render_review.py +282 -0
  56. package/.agents/skills/wtk-deep-review/scripts/run_jobs.py +436 -0
  57. package/.agents/skills/wtk-deep-review/scripts/token_metrics.py +369 -0
  58. package/.agents/skills/wtk-discover/SKILL.md +270 -0
  59. package/.agents/skills/wtk-discover/references/document-format.md +149 -0
  60. package/.agents/skills/wtk-implement/SKILL.md +95 -0
  61. package/.agents/skills/wtk-implement/references/checklist-format.md +116 -0
  62. package/.agents/skills/wtk-implement/references/screens.md +129 -0
  63. package/.agents/skills/wtk-implement/references/test-policy.md +152 -0
  64. package/.agents/skills/wtk-implement/references/verify.md +297 -0
  65. package/.agents/skills/wtk-knowledge-check/SKILL.md +16 -0
  66. package/.agents/skills/wtk-knowledge-check/scripts/check.ts +502 -0
  67. package/.agents/skills/wtk-knowledge-check/scripts/cli.ts +32 -0
  68. package/.agents/skills/wtk-knowledge-check/scripts/frontmatter.ts +44 -0
  69. package/.agents/skills/wtk-lean/.skill-meta.json +6 -0
  70. package/.agents/skills/wtk-lean/NOTICE.md +18 -0
  71. package/.agents/skills/wtk-lean/SKILL.md +137 -0
  72. package/.agents/skills/wtk-lean/references/build.md +168 -0
  73. package/.agents/skills/wtk-lean/references/checks.md +253 -0
  74. package/.agents/skills/wtk-lean/references/memory.md +156 -0
  75. package/.agents/skills/wtk-lean/references/plan.md +514 -0
  76. package/.agents/skills/wtk-lean/references/verify.md +336 -0
  77. package/.agents/skills/wtk-lean/scripts/check_commit.py +121 -0
  78. package/.agents/skills/wtk-lean/scripts/fixtures/checks.md +98 -0
  79. package/.agents/skills/wtk-lean/scripts/fixtures/plan.md +119 -0
  80. package/.agents/skills/wtk-lean/scripts/fixtures/verification.md +41 -0
  81. package/.agents/skills/wtk-lean/scripts/lessons.py +412 -0
  82. package/.agents/skills/wtk-lean/scripts/selftest.py +401 -0
  83. package/.agents/skills/wtk-lean/scripts/validate_checks.py +417 -0
  84. package/.agents/skills/wtk-lean/scripts/validate_plan.py +580 -0
  85. package/.agents/skills/wtk-lean/scripts/validate_verification.py +357 -0
  86. package/.agents/skills/wtk-plan/SKILL.md +102 -0
  87. package/.agents/skills/wtk-plan/references/document-format.md +222 -0
  88. package/.agents/skills/wtk-qa/SKILL.md +14 -0
  89. package/.agents/skills/wtk-qa-execute/SKILL.md +115 -0
  90. package/.agents/skills/wtk-qa-execute/references/fix-loop.md +23 -0
  91. package/.agents/skills/wtk-qa-execute/references/session-protocol.md +25 -0
  92. package/.agents/skills/wtk-qa-plan/SKILL.md +111 -0
  93. package/.agents/skills/wtk-qa-plan/references/profile.md +35 -0
  94. package/.agents/skills/wtk-ship/SKILL.md +49 -0
  95. package/.agents/skills/wtk-ship/remediation.py +170 -0
  96. package/.agents/skills/wtk-ship/scripts/close_feature.py +83 -0
  97. package/.agents/skills/wtk-ship/scripts/review_convergence.py +373 -0
  98. package/.wtk.toml.example +93 -0
  99. package/AGENTS.md +129 -0
  100. package/NOTICE.md +12 -0
  101. package/README.md +440 -0
  102. package/bin/wtk.js +25 -0
  103. package/docs/toolkit/README.md +56 -0
  104. package/docs/toolkit/decisions.md +61 -0
  105. package/docs/toolkit/guidelines/CONTEXT-BUDGET.md +64 -0
  106. package/docs/toolkit/guidelines/DX.md +67 -0
  107. package/docs/toolkit/guidelines/FRONTEND.md +73 -0
  108. package/docs/toolkit/guidelines/KNOWLEDGE-WIKI.md +128 -0
  109. package/docs/toolkit/guidelines/MODELING.md +134 -0
  110. package/docs/toolkit/guidelines/QA-EXECUTION.md +46 -0
  111. package/docs/toolkit/guidelines/QA-SCENARIOS.md +165 -0
  112. package/docs/toolkit/guidelines/REVIEW-ROUNDS.md +160 -0
  113. package/docs/toolkit/guidelines/SECURITY.md +160 -0
  114. package/docs/toolkit/guidelines/UI-UX.md +117 -0
  115. package/docs/toolkit/guidelines/WORKFLOW-MEMORY.md +73 -0
  116. package/docs/toolkit/guidelines.md +50 -0
  117. package/docs/toolkit/loop.md +77 -0
  118. package/docs/toolkit/purpose.md +51 -0
  119. package/docs/toolkit/repository-intelligence.md +53 -0
  120. package/docs/toolkit/reviews.md +77 -0
  121. package/knowledge/AGENTS.md +316 -0
  122. package/knowledge/raw/README.md +17 -0
  123. package/package.json +74 -0
  124. package/scripts/install_security_skills.py +816 -0
  125. package/scripts/installer/engine.js +254 -0
  126. package/scripts/installer/knowledge.js +28 -0
  127. package/scripts/installer/packets.js +77 -0
  128. package/scripts/installer/terminal.js +98 -0
  129. package/scripts/installer/transaction.js +75 -0
  130. package/skills-lock.json +92 -0
  131. package/templates/adoption/agents/core.md +23 -0
  132. package/templates/adoption/agents/quality.md +3 -0
  133. package/templates/adoption/knowledge/wiki/architecture/index.md +3 -0
  134. package/templates/adoption/knowledge/wiki/decisions/index.md +3 -0
  135. package/templates/adoption/knowledge/wiki/design/index.md +3 -0
  136. package/templates/adoption/knowledge/wiki/domain/index.md +3 -0
  137. package/templates/adoption/knowledge/wiki/index.md +3 -0
  138. package/templates/adoption/knowledge/wiki/log.md +3 -0
  139. package/templates/adoption/knowledge/wiki/open-questions/index.md +3 -0
  140. package/templates/adoption/knowledge/wiki/product/index.md +3 -0
  141. package/templates/adoption/knowledge/wiki/research/index.md +3 -0
  142. package/templates/adoption/product/AGENT-CONTEXT.md +25 -0
@@ -0,0 +1,502 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import type { Dirent } from "node:fs";
3
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
4
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
5
+ import { readFrontmatter } from "./frontmatter.js";
6
+
7
+ /**
8
+ * What a finding is about.
9
+ *
10
+ * - `conformance`: the bundle breaks one of the three OKF v0.2 conformance rules.
11
+ * - `drift`: a concept declares a repository source that changed after the concept recorded it.
12
+ * - `gap`: durable knowledge accumulated in `.specs/` without a concept harvesting it.
13
+ * - `naming`: a raw source that breaks the `YYYY-MM-DD-<slug>` convention.
14
+ */
15
+ export type KnowledgeFindingKind = "conformance" | "drift" | "gap" | "naming";
16
+
17
+ export type KnowledgeFinding = {
18
+ kind: KnowledgeFindingKind;
19
+ /** `error` fails the check; `warning` is reported and tolerated. */
20
+ severity: "error" | "warning";
21
+ /** Offending file, relative to the scanned root, with forward slashes. */
22
+ file: string;
23
+ message: string;
24
+ };
25
+
26
+ export type CheckKnowledgeOptions = {
27
+ /** Bundle directory, relative to the root. Defaults to `knowledge/wiki`. */
28
+ bundle?: string;
29
+ /** Raw source directory, relative to the root. Defaults to `knowledge/raw`. */
30
+ raw?: string;
31
+ /**
32
+ * Resolves the last change date (`YYYY-MM-DD`) of a repository file, or null when unknown.
33
+ * Defaults to the author date of the file's last git commit, which a rebase preserves.
34
+ */
35
+ sourceLastChanged?: (absolutePath: string) => string | null;
36
+ };
37
+
38
+ type GitChange = {
39
+ commit: string;
40
+ date: string;
41
+ };
42
+
43
+ /**
44
+ * The bundle is `knowledge/wiki`, not `knowledge`. Everything inside it must conform, so the
45
+ * untouched originals in `knowledge/raw` deliberately sit outside it (§11.1 admits no exception
46
+ * for a markdown file without frontmatter).
47
+ */
48
+ const DEFAULT_BUNDLE = join("knowledge", "wiki");
49
+ const DEFAULT_RAW = join("knowledge", "raw");
50
+ /** Everything in `raw/` sorts chronologically, so the ISO date leads the name. */
51
+ const RAW_ISO_PREFIX = /^\d{4}-\d{2}-\d{2}-\S/;
52
+ /** The `DD-MM-YYYY` form neither matches the rest of the bundle nor sorts correctly. */
53
+ const RAW_REVERSED_PREFIX = /^\d{2}-\d{2}-\d{4}-/;
54
+ /** Explains the directory itself rather than being a source. */
55
+ const RAW_EXEMPT = new Set(["README.md"]);
56
+ const SKIPPED_DIRECTORIES = new Set(["node_modules", ".git"]);
57
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
58
+ const URI_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
59
+ const STATE_PATH = join(".specs", "STATE.md");
60
+ const FEATURES_PATH = join(".specs", "features");
61
+ const SPINE_PATH = join("docs", "architecture", "ARCHITECTURE-SPINE.md");
62
+
63
+ type SourceEntry = {
64
+ id: string | null;
65
+ resource: string | null;
66
+ lastModified: string | null;
67
+ };
68
+
69
+ function toPosix(path: string): string {
70
+ return path.split(sep).join("/");
71
+ }
72
+
73
+ function readSources(data: Record<string, unknown>): SourceEntry[] {
74
+ const raw = data.sources;
75
+ if (!Array.isArray(raw)) return [];
76
+ const entries: SourceEntry[] = [];
77
+ for (const item of raw) {
78
+ if (typeof item !== "object" || item === null || Array.isArray(item)) continue;
79
+ const entry = item as Record<string, unknown>;
80
+ entries.push({
81
+ id: typeof entry.id === "string" ? entry.id : null,
82
+ resource: typeof entry.resource === "string" ? entry.resource : null,
83
+ lastModified: typeof entry.last_modified === "string" ? entry.last_modified : null,
84
+ });
85
+ }
86
+ return entries;
87
+ }
88
+
89
+ function listMarkdownFiles(directory: string): string[] {
90
+ const files: string[] = [];
91
+ let entries: Dirent[];
92
+ try {
93
+ entries = readdirSync(directory, { withFileTypes: true });
94
+ } catch {
95
+ return files;
96
+ }
97
+ for (const entry of entries) {
98
+ if (SKIPPED_DIRECTORIES.has(entry.name)) continue;
99
+ const full = join(directory, entry.name);
100
+ if (entry.isDirectory()) files.push(...listMarkdownFiles(full));
101
+ else if (entry.name.endsWith(".md")) files.push(full);
102
+ }
103
+ return files;
104
+ }
105
+
106
+ function listFiles(directory: string): string[] {
107
+ const files: string[] = [];
108
+ let entries: Dirent[];
109
+ try {
110
+ entries = readdirSync(directory, { withFileTypes: true });
111
+ } catch {
112
+ return files;
113
+ }
114
+ for (const entry of entries) {
115
+ if (SKIPPED_DIRECTORIES.has(entry.name)) continue;
116
+ const full = join(directory, entry.name);
117
+ if (entry.isDirectory()) files.push(...listFiles(full));
118
+ else files.push(full);
119
+ }
120
+ return files;
121
+ }
122
+
123
+ /**
124
+ * Raw sources are named `YYYY-MM-DD-<slug>` so a flat directory sorts chronologically. They sit
125
+ * outside the bundle and so have no OKF conformance to break — this is a house convention, and it
126
+ * only warns.
127
+ */
128
+ function checkRawNaming(rootDirectory: string, rawDirectory: string): KnowledgeFinding[] {
129
+ const findings: KnowledgeFinding[] = [];
130
+ for (const absoluteFile of listFiles(rawDirectory)) {
131
+ const name = absoluteFile.slice(absoluteFile.lastIndexOf(sep) + 1);
132
+ if (RAW_EXEMPT.has(name) || name.startsWith(".")) continue;
133
+ if (RAW_ISO_PREFIX.test(name)) continue;
134
+
135
+ const file = toPosix(relative(rootDirectory, absoluteFile));
136
+ findings.push({
137
+ kind: "naming",
138
+ severity: "warning",
139
+ file,
140
+ message: RAW_REVERSED_PREFIX.test(name)
141
+ ? "dated DD-MM-YYYY; the bundle dates everything ISO 8601, and DD-MM-YYYY does not sort"
142
+ : "is not prefixed with its ISO date, expected YYYY-MM-DD-<slug>",
143
+ });
144
+ }
145
+ return findings;
146
+ }
147
+
148
+ function gitLastChange(absolutePath: string): GitChange | null {
149
+ try {
150
+ const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
151
+ cwd: dirname(absolutePath),
152
+ encoding: "utf8",
153
+ stdio: ["ignore", "pipe", "ignore"],
154
+ }).trim();
155
+ const prefix = execFileSync("git", ["rev-parse", "--show-prefix"], {
156
+ cwd: dirname(absolutePath),
157
+ encoding: "utf8",
158
+ stdio: ["ignore", "pipe", "ignore"],
159
+ }).trim();
160
+ const path = toPosix(join(prefix, basename(absolutePath)));
161
+
162
+ const blobAt = (revision: string): string | null => {
163
+ try {
164
+ return execFileSync("git", ["rev-parse", `${revision}:${path}`], {
165
+ cwd: root,
166
+ encoding: "utf8",
167
+ stdio: ["ignore", "pipe", "ignore"],
168
+ }).trim();
169
+ } catch {
170
+ return null;
171
+ }
172
+ };
173
+
174
+ const lastChangeAt = (revision: string): GitChange | null => {
175
+ const stdout = execFileSync(
176
+ "git",
177
+ ["log", "-1", "--format=%H%x00%as%x00%P", revision, "--", path],
178
+ {
179
+ cwd: root,
180
+ encoding: "utf8",
181
+ stdio: ["ignore", "pipe", "ignore"],
182
+ },
183
+ );
184
+ const [commit, date, parentList = ""] = stdout.trim().split("\0");
185
+ if (commit === undefined || date === undefined || !ISO_DATE.test(date)) return null;
186
+
187
+ const parents = parentList.split(" ").filter(Boolean);
188
+ if (parents.length > 1) {
189
+ const blob = blobAt(commit);
190
+ const inherited = parents
191
+ .filter((parent) => blob !== null && blobAt(parent) === blob)
192
+ .map(lastChangeAt)
193
+ .filter((change): change is GitChange => change !== null)
194
+ .sort((left, right) => right.date.localeCompare(left.date));
195
+ if (inherited[0] !== undefined) return inherited[0];
196
+ }
197
+
198
+ return { commit, date };
199
+ };
200
+
201
+ return lastChangeAt("HEAD");
202
+ } catch {
203
+ return null;
204
+ }
205
+ }
206
+
207
+ /** §11.2 — every concept carries a non-empty `type`. */
208
+ function checkConcept(file: string, source: string): KnowledgeFinding[] {
209
+ const { present, data, error } = readFrontmatter(source);
210
+ const base = { kind: "conformance", severity: "error", file } as const;
211
+
212
+ if (!present) return [{ ...base, message: "concept has no YAML frontmatter block" }];
213
+ if (data === null) return [{ ...base, message: error ?? "unparseable frontmatter" }];
214
+
215
+ const type = data.type;
216
+ if (typeof type !== "string" || type.trim() === "") {
217
+ return [{ ...base, message: "frontmatter has no non-empty `type`" }];
218
+ }
219
+ return [];
220
+ }
221
+
222
+ /** §8 — index files carry no frontmatter, except `okf_version` at the bundle root. */
223
+ function checkIndex(file: string, source: string, isBundleRoot: boolean): KnowledgeFinding[] {
224
+ const { present, data, error } = readFrontmatter(source);
225
+ if (!present) return [];
226
+
227
+ const base = { kind: "conformance", severity: "error", file } as const;
228
+ if (!isBundleRoot) {
229
+ return [{ ...base, message: "only the bundle-root index.md may carry frontmatter" }];
230
+ }
231
+ if (data === null) return [{ ...base, message: error ?? "unparseable frontmatter" }];
232
+
233
+ const extra = Object.keys(data).filter((key) => key !== "okf_version");
234
+ if (extra.length > 0) {
235
+ return [
236
+ {
237
+ ...base,
238
+ message: `bundle-root index.md may only declare okf_version, found ${extra.join(", ")}`,
239
+ },
240
+ ];
241
+ }
242
+ return [];
243
+ }
244
+
245
+ /** §9 — date-grouped entries under ISO `## YYYY-MM-DD` headings, newest first. */
246
+ function checkLog(file: string, source: string): KnowledgeFinding[] {
247
+ const findings: KnowledgeFinding[] = [];
248
+ const base = { kind: "conformance", severity: "error", file } as const;
249
+
250
+ if (readFrontmatter(source).present) {
251
+ findings.push({ ...base, message: "log.md must not carry frontmatter" });
252
+ }
253
+
254
+ const dates: string[] = [];
255
+ for (const line of source.replace(/\r\n/g, "\n").split("\n")) {
256
+ if (!line.startsWith("## ")) continue;
257
+ const heading = line.slice(3).trim();
258
+ if (!ISO_DATE.test(heading)) {
259
+ findings.push({ ...base, message: `date heading "${heading}" is not ISO 8601 YYYY-MM-DD` });
260
+ continue;
261
+ }
262
+ dates.push(heading);
263
+ }
264
+
265
+ for (let index = 1; index < dates.length; index += 1) {
266
+ const previous = dates[index - 1];
267
+ const current = dates[index];
268
+ if (previous !== undefined && current !== undefined && current > previous) {
269
+ findings.push({
270
+ ...base,
271
+ message: `${current} appears after ${previous}; entries run newest first`,
272
+ });
273
+ }
274
+ }
275
+ return findings;
276
+ }
277
+
278
+ /**
279
+ * A concept's recorded `last_modified` against what the repository says. `sources[].last_modified`
280
+ * is a recency signal for the *source*, distinct from `generated.at`, which records when the
281
+ * concept itself was written — so comparing the two detects a source that moved on without the
282
+ * concept following it.
283
+ */
284
+ function checkDrift(
285
+ file: string,
286
+ absoluteFile: string,
287
+ rootDirectory: string,
288
+ bundleDirectory: string,
289
+ data: Record<string, unknown>,
290
+ sourceLastChanged: (absolutePath: string, conceptPath: string) => string | null,
291
+ ): KnowledgeFinding[] {
292
+ const findings: KnowledgeFinding[] = [];
293
+
294
+ for (const entry of readSources(data)) {
295
+ const { resource, lastModified } = entry;
296
+ // A scope descriptor or an external URL is not a path we can stat (§5.1, §6.2).
297
+ if (resource === null || URI_SCHEME.test(resource)) continue;
298
+
299
+ const absoluteSource = resource.startsWith("/")
300
+ ? join(bundleDirectory, resource.slice(1))
301
+ : resolve(dirname(absoluteFile), resource);
302
+
303
+ // Only repository files are checkable; anything above the root is out of scope.
304
+ const inside = relative(rootDirectory, absoluteSource);
305
+ if (inside.startsWith("..") || inside === "") continue;
306
+
307
+ if (!existsSync(absoluteSource)) {
308
+ findings.push({
309
+ kind: "drift",
310
+ severity: "warning",
311
+ file,
312
+ message: `source "${resource}" does not exist`,
313
+ });
314
+ continue;
315
+ }
316
+
317
+ if (lastModified === null) continue;
318
+ if (!ISO_DATE.test(lastModified)) {
319
+ findings.push({
320
+ kind: "conformance",
321
+ severity: "error",
322
+ file,
323
+ message: `source "${resource}" has last_modified "${lastModified}", not YYYY-MM-DD`,
324
+ });
325
+ continue;
326
+ }
327
+
328
+ const changed = sourceLastChanged(absoluteSource, absoluteFile);
329
+ if (changed !== null && changed > lastModified) {
330
+ findings.push({
331
+ kind: "drift",
332
+ severity: "error",
333
+ file,
334
+ message: `source "${resource}" changed on ${changed}, concept records ${lastModified}`,
335
+ });
336
+ }
337
+ }
338
+ return findings;
339
+ }
340
+
341
+ /** Decisions and verified features that accumulated in `.specs/` without a concept harvesting them. */
342
+ function checkGaps(
343
+ rootDirectory: string,
344
+ harvestedIds: Set<string>,
345
+ harvestedPaths: Set<string>,
346
+ ): KnowledgeFinding[] {
347
+ const findings: KnowledgeFinding[] = [];
348
+
349
+ const statePath = join(rootDirectory, STATE_PATH);
350
+ if (existsSync(statePath)) {
351
+ const state = readFileSync(statePath, "utf8");
352
+ const seenStateLabels = new Set<string>();
353
+ for (const match of state.matchAll(/^### (AD-\d+)\s*$/gm)) {
354
+ const label = match[1];
355
+ if (label === undefined) continue;
356
+ if (seenStateLabels.has(label)) {
357
+ findings.push({
358
+ kind: "conformance",
359
+ severity: "error",
360
+ file: toPosix(STATE_PATH),
361
+ message: `${label} appears more than once`,
362
+ });
363
+ continue;
364
+ }
365
+ seenStateLabels.add(label);
366
+ if (harvestedIds.has(`state-${label.toLowerCase()}`)) continue;
367
+ findings.push({
368
+ kind: "gap",
369
+ severity: "warning",
370
+ file: toPosix(STATE_PATH),
371
+ message: `${label} has no concept; expected a source entry with id "state-${label.toLowerCase()}"`,
372
+ });
373
+ }
374
+ }
375
+
376
+ const spinePath = join(rootDirectory, SPINE_PATH);
377
+ if (existsSync(spinePath)) {
378
+ const seenSpineLabels = new Set<string>();
379
+ for (const match of readFileSync(spinePath, "utf8").matchAll(/^### (AD-\d+)\b/gm)) {
380
+ const label = match[1];
381
+ if (label === undefined) continue;
382
+ if (seenSpineLabels.has(label)) {
383
+ findings.push({
384
+ kind: "conformance",
385
+ severity: "error",
386
+ file: toPosix(SPINE_PATH),
387
+ message: `${label} appears more than once`,
388
+ });
389
+ continue;
390
+ }
391
+ seenSpineLabels.add(label);
392
+ }
393
+ }
394
+
395
+ const featuresPath = join(rootDirectory, FEATURES_PATH);
396
+ let features: Dirent[];
397
+ try {
398
+ features = readdirSync(featuresPath, { withFileTypes: true });
399
+ } catch {
400
+ return findings;
401
+ }
402
+ for (const feature of features) {
403
+ if (!feature.isDirectory()) continue;
404
+ const validation = join(FEATURES_PATH, feature.name, "validation.md");
405
+ if (!existsSync(join(rootDirectory, validation))) continue;
406
+ if (harvestedPaths.has(toPosix(validation))) continue;
407
+ findings.push({
408
+ kind: "gap",
409
+ severity: "warning",
410
+ file: toPosix(validation),
411
+ message: `feature "${feature.name}" is verified but no concept cites its validation`,
412
+ });
413
+ }
414
+ return findings;
415
+ }
416
+
417
+ /**
418
+ * Checks the knowledge bundle for OKF v0.2 conformance, for concepts that drifted from the
419
+ * repository sources they derive from, and for durable knowledge in `.specs/` that no concept has
420
+ * harvested yet.
421
+ */
422
+ export function checkKnowledge(
423
+ rootDirectory: string,
424
+ options: CheckKnowledgeOptions = {},
425
+ ): KnowledgeFinding[] {
426
+ const bundleDirectory = join(rootDirectory, options.bundle ?? DEFAULT_BUNDLE);
427
+ const rawDirectory = join(rootDirectory, options.raw ?? DEFAULT_RAW);
428
+
429
+ // Concepts cite the same handful of files over and over, and the default resolver spawns a git
430
+ // process per call. Memoising keeps a bundle of any size at one lookup per distinct path.
431
+ const cache = new Map<string, string | null>();
432
+ const gitCache = new Map<string, GitChange | null>();
433
+ const sourceLastChanged = (absolutePath: string, conceptPath: string): string | null => {
434
+ if (options.sourceLastChanged !== undefined) {
435
+ if (cache.has(absolutePath)) return cache.get(absolutePath) ?? null;
436
+ const resolved = options.sourceLastChanged(absolutePath);
437
+ cache.set(absolutePath, resolved);
438
+ return resolved;
439
+ }
440
+
441
+ const lastChange = (path: string): GitChange | null => {
442
+ if (gitCache.has(path)) return gitCache.get(path) ?? null;
443
+ const resolved = gitLastChange(path);
444
+ gitCache.set(path, resolved);
445
+ return resolved;
446
+ };
447
+ const sourceChange = lastChange(absolutePath);
448
+ const conceptChange = lastChange(conceptPath);
449
+ if (sourceChange?.commit === conceptChange?.commit) return null;
450
+ return sourceChange?.date ?? null;
451
+ };
452
+
453
+ const findings: KnowledgeFinding[] = [];
454
+ const harvestedIds = new Set<string>();
455
+ const harvestedPaths = new Set<string>();
456
+
457
+ for (const absoluteFile of listMarkdownFiles(bundleDirectory)) {
458
+ const file = toPosix(relative(rootDirectory, absoluteFile));
459
+ const name = absoluteFile.slice(absoluteFile.lastIndexOf(sep) + 1);
460
+ const source = readFileSync(absoluteFile, "utf8");
461
+
462
+ if (name === "index.md") {
463
+ const isBundleRoot = dirname(absoluteFile) === bundleDirectory;
464
+ findings.push(...checkIndex(file, source, isBundleRoot));
465
+ continue;
466
+ }
467
+ if (name === "log.md") {
468
+ findings.push(...checkLog(file, source));
469
+ continue;
470
+ }
471
+
472
+ const conformance = checkConcept(file, source);
473
+ findings.push(...conformance);
474
+ if (conformance.length > 0) continue;
475
+
476
+ const data = readFrontmatter(source).data;
477
+ if (data === null) continue;
478
+
479
+ for (const entry of readSources(data)) {
480
+ if (entry.resource === null || URI_SCHEME.test(entry.resource)) continue;
481
+ const absoluteSource = entry.resource.startsWith("/")
482
+ ? join(bundleDirectory, entry.resource.slice(1))
483
+ : resolve(dirname(absoluteFile), entry.resource);
484
+ const source = toPosix(relative(rootDirectory, absoluteSource));
485
+ harvestedPaths.add(source);
486
+ // An id only marks a decision harvested when it labels the ledger the decision lives in.
487
+ // Without that pairing, any concept citing an unrelated source as "state-ad-001" would
488
+ // silently close the gap for a decision nobody read.
489
+ if (entry.id !== null && source === toPosix(STATE_PATH)) {
490
+ harvestedIds.add(entry.id.toLowerCase());
491
+ }
492
+ }
493
+
494
+ findings.push(
495
+ ...checkDrift(file, absoluteFile, rootDirectory, bundleDirectory, data, sourceLastChanged),
496
+ );
497
+ }
498
+
499
+ findings.push(...checkRawNaming(rootDirectory, rawDirectory));
500
+ findings.push(...checkGaps(rootDirectory, harvestedIds, harvestedPaths));
501
+ return findings;
502
+ }
@@ -0,0 +1,32 @@
1
+ import { checkKnowledge, type KnowledgeFinding } from "./check.js";
2
+
3
+ const HEADINGS: Record<KnowledgeFinding["kind"], string> = {
4
+ conformance: "conformance — OKF v0.2 requires a parseable frontmatter with a non-empty `type`",
5
+ drift:
6
+ "drift — the source moved on after the concept recorded it; re-read it and bump last_modified.\n This compares committer dates, which a rebase moves, so an uncommitted edit does not show here\n and a green run is not final. A source and the concept citing it are exempt while their last\n change is the same commit; set last_modified to the day you commit rather than leaning on that.",
7
+ naming: "naming — raw sources are named YYYY-MM-DD-<slug> so the directory sorts chronologically",
8
+ gap: "gap — durable knowledge in .specs/ that no concept has harvested yet",
9
+ };
10
+
11
+ const rootDirectory = process.argv[2] ?? process.cwd();
12
+ const findings = checkKnowledge(rootDirectory);
13
+
14
+ if (findings.length === 0) {
15
+ console.log(`knowledge: bundle is conformant, in sync and fully harvested in ${rootDirectory}`);
16
+ } else {
17
+ for (const kind of ["conformance", "drift", "naming", "gap"] as const) {
18
+ const group = findings.filter((finding) => finding.kind === kind);
19
+ if (group.length === 0) continue;
20
+
21
+ const write = group.some((finding) => finding.severity === "error")
22
+ ? console.error
23
+ : console.warn;
24
+ write(`\nknowledge: ${HEADINGS[kind]}`);
25
+ for (const finding of group) write(` ${finding.file}: ${finding.message}`);
26
+ }
27
+
28
+ const errors = findings.filter((finding) => finding.severity === "error").length;
29
+ const warnings = findings.length - errors;
30
+ console.error(`\nknowledge: ${errors} error(s), ${warnings} warning(s)`);
31
+ if (errors > 0) process.exitCode = 1;
32
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The result of reading a document's leading `---` delimited YAML block.
3
+ *
4
+ * Absence and malformation are different answers on purpose: a caller may tolerate a file
5
+ * with no block at all while still refusing one whose block cannot be read.
6
+ */
7
+ export type Frontmatter = {
8
+ /** Whether the file opens with a `---` delimited block at all. */
9
+ present: boolean;
10
+ data: Record<string, unknown> | null;
11
+ /** Why the block could not be turned into a mapping, when it could not. */
12
+ error: string | null;
13
+ };
14
+
15
+ /**
16
+ * Reads the frontmatter of a Markdown document.
17
+ *
18
+ * Three shapes fail, each with its own message: a block that is never closed, a block whose
19
+ * YAML does not parse, and a block that parses to something other than a mapping. An empty
20
+ * block is not a failure — it yields an empty mapping.
21
+ */
22
+ export function readFrontmatter(source: string): Frontmatter {
23
+ const lines = source.replace(/\r\n/g, "\n").split("\n");
24
+ if (lines[0] !== "---") return { present: false, data: null, error: null };
25
+
26
+ const closing = lines.indexOf("---", 1);
27
+ if (closing === -1) {
28
+ return { present: true, data: null, error: "the frontmatter block is never closed" };
29
+ }
30
+
31
+ let parsed: unknown;
32
+ try {
33
+ parsed = Bun.YAML.parse(lines.slice(1, closing).join("\n"));
34
+ } catch (cause) {
35
+ const detail = cause instanceof Error ? cause.message.split("\n")[0] : String(cause);
36
+ return { present: true, data: null, error: `unparseable YAML: ${detail}` };
37
+ }
38
+
39
+ if (parsed === null || parsed === undefined) return { present: true, data: {}, error: null };
40
+ if (typeof parsed !== "object" || Array.isArray(parsed)) {
41
+ return { present: true, data: null, error: "the frontmatter is not a YAML mapping" };
42
+ }
43
+ return { present: true, data: parsed as Record<string, unknown>, error: null };
44
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "source": "https://github.com/tech-leads-club/agent-skills/tree/main/skills/tlc-spec-lean",
3
+ "sourceCommit": "0ab82f644cd9caf94c65347a50ad934800b0cbc4",
4
+ "contentHash": "56db00c3b8c5e76c41f05aeddf1ba1c21be420f4afee48a5695c693a7391a50f",
5
+ "license": "CC-BY-4.0"
6
+ }
@@ -0,0 +1,18 @@
1
+ ## Attribution
2
+
3
+ This skill is an adapted version of Tech Lead's Club's `tlc-spec-lean` skill,
4
+ created by Felipe Rodrigues and published at:
5
+
6
+ https://github.com/tech-leads-club/agent-skills/tree/main/skills/tlc-spec-lean
7
+
8
+ The original work is licensed under CC BY 4.0. This workflow-owned adaptation
9
+ retains the original planning and validation foundations and materially changes
10
+ execution to use whole observable slices, one sequential builder, one fresh verifier
11
+ over the complete feature, and transient feature-artifact cleanup.
12
+
13
+ ## License
14
+
15
+ Creative Commons Attribution 4.0 International (CC BY 4.0).
16
+
17
+ Attribution is required. This notice identifies the original author, source,
18
+ license, and material modifications.