mdkg 0.0.3 → 0.0.4

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 (42) hide show
  1. package/README.md +171 -169
  2. package/dist/cli.js +980 -624
  3. package/dist/commands/checkpoint.js +17 -6
  4. package/dist/commands/event.js +46 -0
  5. package/dist/commands/event_support.js +146 -0
  6. package/dist/commands/format.js +6 -7
  7. package/dist/commands/index.js +10 -4
  8. package/dist/commands/init.js +198 -16
  9. package/dist/commands/list.js +18 -1
  10. package/dist/commands/new.js +30 -5
  11. package/dist/commands/pack.js +194 -2
  12. package/dist/commands/query_output.js +84 -0
  13. package/dist/commands/search.js +22 -5
  14. package/dist/commands/show.js +26 -11
  15. package/dist/commands/skill.js +359 -0
  16. package/dist/commands/skill_support.js +121 -0
  17. package/dist/commands/task.js +270 -0
  18. package/dist/commands/validate.js +104 -7
  19. package/dist/graph/edges.js +2 -2
  20. package/dist/graph/frontmatter.js +1 -0
  21. package/dist/graph/indexer.js +21 -0
  22. package/dist/graph/node.js +20 -4
  23. package/dist/graph/skills_index_cache.js +94 -0
  24. package/dist/graph/skills_indexer.js +160 -0
  25. package/dist/init/core/rule-1-mdkg-conventions.md +9 -2
  26. package/dist/init/core/rule-3-cli-contract.md +73 -14
  27. package/dist/init/core/rule-4-repo-safety-and-ignores.md +9 -3
  28. package/dist/init/core/rule-6-templates-and-schemas.md +6 -2
  29. package/dist/init/skills/SKILL.md.example +41 -0
  30. package/dist/init/templates/default/bug.md +1 -0
  31. package/dist/init/templates/default/chk.md +1 -0
  32. package/dist/init/templates/default/epic.md +1 -0
  33. package/dist/init/templates/default/feat.md +1 -0
  34. package/dist/init/templates/default/task.md +1 -0
  35. package/dist/init/templates/default/test.md +1 -0
  36. package/dist/pack/export_md.js +6 -0
  37. package/dist/pack/export_xml.js +6 -0
  38. package/dist/pack/pack.js +35 -0
  39. package/dist/util/argparse.js +25 -0
  40. package/dist/util/filter.js +18 -0
  41. package/dist/util/id.js +23 -0
  42. package/package.json +5 -2
@@ -0,0 +1,270 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runTaskStartCommand = runTaskStartCommand;
7
+ exports.runTaskUpdateCommand = runTaskUpdateCommand;
8
+ exports.runTaskDoneCommand = runTaskDoneCommand;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const config_1 = require("../core/config");
12
+ const indexer_1 = require("../graph/indexer");
13
+ const index_cache_1 = require("../graph/index_cache");
14
+ const frontmatter_1 = require("../graph/frontmatter");
15
+ const skills_indexer_1 = require("../graph/skills_indexer");
16
+ const date_1 = require("../util/date");
17
+ const errors_1 = require("../util/errors");
18
+ const qid_1 = require("../util/qid");
19
+ const id_1 = require("../util/id");
20
+ const event_support_1 = require("./event_support");
21
+ const checkpoint_1 = require("./checkpoint");
22
+ const MUTABLE_TASK_TYPES = new Set(["task", "bug", "test"]);
23
+ const SKILL_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
24
+ function parseCsvList(raw) {
25
+ if (!raw) {
26
+ return [];
27
+ }
28
+ return raw
29
+ .split(",")
30
+ .map((value) => value.trim())
31
+ .filter(Boolean);
32
+ }
33
+ function normalizeWorkspace(value) {
34
+ if (!value) {
35
+ return undefined;
36
+ }
37
+ const normalized = value.toLowerCase();
38
+ if (normalized === "all") {
39
+ throw new errors_1.UsageError("--ws all is not valid here");
40
+ }
41
+ return normalized;
42
+ }
43
+ function normalizeId(value, flag) {
44
+ const normalized = value.trim().toLowerCase();
45
+ if (!(0, id_1.isCanonicalId)(normalized)) {
46
+ throw new errors_1.UsageError(`${flag} entries must match <prefix>-<number> or reserved id: ${value}`);
47
+ }
48
+ return normalized;
49
+ }
50
+ function normalizeIdRef(value, flag) {
51
+ const normalized = value.trim().toLowerCase();
52
+ if (!(0, id_1.isCanonicalIdRef)(normalized)) {
53
+ throw new errors_1.UsageError(`${flag} entries must match <id> or <ws>:<id>: ${value}`);
54
+ }
55
+ return normalized;
56
+ }
57
+ function normalizeLowercaseList(raw) {
58
+ return parseCsvList(raw).map((value) => value.toLowerCase());
59
+ }
60
+ function normalizeIdList(raw, flag) {
61
+ return parseCsvList(raw).map((value) => normalizeId(value, flag));
62
+ }
63
+ function normalizeIdRefList(raw, flag) {
64
+ return parseCsvList(raw).map((value) => normalizeIdRef(value, flag));
65
+ }
66
+ function normalizeSkillList(raw) {
67
+ return parseCsvList(raw).map((value) => {
68
+ const normalized = value.toLowerCase();
69
+ if (!SKILL_SLUG_RE.test(normalized)) {
70
+ throw new errors_1.UsageError(`--add-skills entries must be kebab-case: ${value}`);
71
+ }
72
+ return normalized;
73
+ });
74
+ }
75
+ function appendUnique(existing, additions) {
76
+ const next = [...existing];
77
+ const seen = new Set(existing);
78
+ for (const value of additions) {
79
+ if (!seen.has(value)) {
80
+ next.push(value);
81
+ seen.add(value);
82
+ }
83
+ }
84
+ return next;
85
+ }
86
+ function toStringList(value) {
87
+ if (Array.isArray(value)) {
88
+ return value.map((item) => String(item));
89
+ }
90
+ return [];
91
+ }
92
+ function maybeReindex(root, config) {
93
+ if (!config.index.auto_reindex) {
94
+ return;
95
+ }
96
+ const outputPath = path_1.default.resolve(root, config.index.global_index_path);
97
+ (0, index_cache_1.writeIndex)(outputPath, (0, indexer_1.buildIndex)(root, config, { tolerant: config.index.tolerant }));
98
+ }
99
+ function loadMutableTaskNode(root, idOrQid, wsHint) {
100
+ const config = (0, config_1.loadConfig)(root);
101
+ const { index } = (0, index_cache_1.loadIndex)({ root, config });
102
+ const resolved = (0, qid_1.resolveQid)(index, idOrQid, normalizeWorkspace(wsHint));
103
+ if (resolved.status !== "ok") {
104
+ throw new errors_1.NotFoundError((0, qid_1.formatResolveError)("task", idOrQid, resolved, normalizeWorkspace(wsHint)));
105
+ }
106
+ const node = index.nodes[resolved.qid];
107
+ if (!node) {
108
+ throw new errors_1.NotFoundError(`task not found: ${idOrQid}`);
109
+ }
110
+ if (!MUTABLE_TASK_TYPES.has(node.type)) {
111
+ throw new errors_1.UsageError(`mdkg task only supports task, bug, and test nodes; use markdown editing for ${node.type}:${node.id}`);
112
+ }
113
+ const filePath = path_1.default.resolve(root, node.path);
114
+ const content = fs_1.default.readFileSync(filePath, "utf8");
115
+ const parsed = (0, frontmatter_1.parseFrontmatter)(content, filePath);
116
+ return {
117
+ config,
118
+ index,
119
+ qid: node.qid,
120
+ ws: node.ws,
121
+ id: node.id,
122
+ type: node.type,
123
+ filePath,
124
+ frontmatter: { ...parsed.frontmatter },
125
+ body: parsed.body,
126
+ };
127
+ }
128
+ function writeNodeFile(root, filePath, frontmatter, body) {
129
+ const lines = (0, frontmatter_1.formatFrontmatter)(frontmatter, frontmatter_1.DEFAULT_FRONTMATTER_KEY_ORDER);
130
+ const frontmatterBlock = ["---", ...lines, "---"].join("\n");
131
+ const content = body.length > 0 ? `${frontmatterBlock}\n${body}` : frontmatterBlock;
132
+ fs_1.default.writeFileSync(filePath, content, "utf8");
133
+ }
134
+ function ensureStatusAllowed(config, statusRaw, flag = "--status") {
135
+ const normalized = statusRaw.trim().toLowerCase();
136
+ const allowed = new Set(config.work.status_enum.map((value) => value.toLowerCase()));
137
+ if (!allowed.has(normalized)) {
138
+ throw new errors_1.UsageError(`${flag} must be one of ${Array.from(allowed).join(", ")}`);
139
+ }
140
+ return normalized;
141
+ }
142
+ function ensurePriorityAllowed(config, priority) {
143
+ if (!Number.isInteger(priority) ||
144
+ priority < config.work.priority_min ||
145
+ priority > config.work.priority_max) {
146
+ throw new errors_1.UsageError(`--priority must be between ${config.work.priority_min} and ${config.work.priority_max}`);
147
+ }
148
+ return priority;
149
+ }
150
+ function ensureNodeRefsExist(index, values, ws, label) {
151
+ for (const value of values) {
152
+ const resolved = (0, qid_1.resolveQid)(index, value, ws);
153
+ if (resolved.status !== "ok") {
154
+ throw new errors_1.NotFoundError((0, qid_1.formatResolveError)(label, value, resolved, ws));
155
+ }
156
+ }
157
+ }
158
+ function ensureSkillsExist(root, node, slugs) {
159
+ if (slugs.length === 0) {
160
+ return;
161
+ }
162
+ const skillsIndex = (0, skills_indexer_1.buildSkillsIndex)(root, node.config);
163
+ for (const slug of slugs) {
164
+ if (!skillsIndex.skills[slug]) {
165
+ throw new errors_1.NotFoundError(`skill not found: ${slug}`);
166
+ }
167
+ }
168
+ }
169
+ function updateUpdatedDate(frontmatter, now) {
170
+ frontmatter.updated = (0, date_1.formatDate)(now);
171
+ }
172
+ function runTaskStartCommand(options) {
173
+ const loaded = loadMutableTaskNode(options.root, options.id, options.ws);
174
+ const now = options.now ?? new Date();
175
+ loaded.frontmatter.status = ensureStatusAllowed(loaded.config, "progress");
176
+ updateUpdatedDate(loaded.frontmatter, now);
177
+ writeNodeFile(options.root, loaded.filePath, loaded.frontmatter, loaded.body);
178
+ maybeReindex(options.root, loaded.config);
179
+ (0, event_support_1.appendAutomaticEvent)({
180
+ root: options.root,
181
+ ws: loaded.ws,
182
+ kind: "TASK_STARTED",
183
+ status: "ok",
184
+ refs: [loaded.id],
185
+ notes: options.note ?? `status set to progress via mdkg task start`,
186
+ runId: options.runId,
187
+ now,
188
+ });
189
+ console.log(`task started: ${loaded.qid}`);
190
+ }
191
+ function runTaskUpdateCommand(options) {
192
+ const loaded = loadMutableTaskNode(options.root, options.id, options.ws);
193
+ const now = options.now ?? new Date();
194
+ if (options.status) {
195
+ loaded.frontmatter.status = ensureStatusAllowed(loaded.config, options.status);
196
+ }
197
+ if (options.priority !== undefined) {
198
+ loaded.frontmatter.priority = String(ensurePriorityAllowed(loaded.config, options.priority));
199
+ }
200
+ const nextLinks = appendUnique(toStringList(loaded.frontmatter.links), parseCsvList(options.addLinks));
201
+ const nextArtifacts = appendUnique(toStringList(loaded.frontmatter.artifacts), parseCsvList(options.addArtifacts));
202
+ const nextRefs = appendUnique(toStringList(loaded.frontmatter.refs), normalizeIdList(options.addRefs, "--add-refs"));
203
+ const nextTags = appendUnique(toStringList(loaded.frontmatter.tags), normalizeLowercaseList(options.addTags));
204
+ const nextSkills = appendUnique(toStringList(loaded.frontmatter.skills), normalizeSkillList(options.addSkills));
205
+ const blockedByAdditions = normalizeIdRefList(options.addBlockedBy, "--add-blocked-by");
206
+ ensureNodeRefsExist(loaded.index, blockedByAdditions, loaded.ws, "blocked_by");
207
+ const nextBlockedBy = options.clearBlockedBy
208
+ ? blockedByAdditions
209
+ : appendUnique(toStringList(loaded.frontmatter.blocked_by), blockedByAdditions);
210
+ ensureSkillsExist(options.root, loaded, nextSkills);
211
+ loaded.frontmatter.links = nextLinks;
212
+ loaded.frontmatter.artifacts = nextArtifacts;
213
+ loaded.frontmatter.refs = nextRefs;
214
+ loaded.frontmatter.tags = nextTags;
215
+ loaded.frontmatter.skills = nextSkills;
216
+ loaded.frontmatter.blocked_by = nextBlockedBy;
217
+ updateUpdatedDate(loaded.frontmatter, now);
218
+ writeNodeFile(options.root, loaded.filePath, loaded.frontmatter, loaded.body);
219
+ maybeReindex(options.root, loaded.config);
220
+ (0, event_support_1.appendAutomaticEvent)({
221
+ root: options.root,
222
+ ws: loaded.ws,
223
+ kind: "TASK_UPDATED",
224
+ status: "ok",
225
+ refs: [loaded.id],
226
+ artifacts: nextArtifacts,
227
+ notes: options.note ?? `task metadata updated via mdkg task update`,
228
+ runId: options.runId,
229
+ now,
230
+ });
231
+ console.log(`task updated: ${loaded.qid}`);
232
+ }
233
+ function runTaskDoneCommand(options) {
234
+ const loaded = loadMutableTaskNode(options.root, options.id, options.ws);
235
+ const now = options.now ?? new Date();
236
+ const nextLinks = appendUnique(toStringList(loaded.frontmatter.links), parseCsvList(options.addLinks));
237
+ const nextArtifacts = appendUnique(toStringList(loaded.frontmatter.artifacts), parseCsvList(options.addArtifacts));
238
+ const nextRefs = appendUnique(toStringList(loaded.frontmatter.refs), normalizeIdList(options.addRefs, "--add-refs"));
239
+ loaded.frontmatter.status = ensureStatusAllowed(loaded.config, "done");
240
+ loaded.frontmatter.links = nextLinks;
241
+ loaded.frontmatter.artifacts = nextArtifacts;
242
+ loaded.frontmatter.refs = nextRefs;
243
+ updateUpdatedDate(loaded.frontmatter, now);
244
+ writeNodeFile(options.root, loaded.filePath, loaded.frontmatter, loaded.body);
245
+ (0, event_support_1.appendAutomaticEvent)({
246
+ root: options.root,
247
+ ws: loaded.ws,
248
+ kind: "TASK_DONE",
249
+ status: "ok",
250
+ refs: [loaded.id],
251
+ artifacts: nextArtifacts,
252
+ notes: options.note ?? `status set to done via mdkg task done`,
253
+ runId: options.runId,
254
+ now,
255
+ });
256
+ if (options.checkpoint) {
257
+ (0, checkpoint_1.runCheckpointNewCommand)({
258
+ root: options.root,
259
+ title: options.checkpoint,
260
+ ws: loaded.ws,
261
+ relates: loaded.id,
262
+ scope: loaded.id,
263
+ runId: options.runId,
264
+ note: `checkpoint created from mdkg task done for ${loaded.id}`,
265
+ now,
266
+ });
267
+ }
268
+ maybeReindex(options.root, loaded.config);
269
+ console.log(`task done: ${loaded.qid}`);
270
+ }
@@ -9,6 +9,7 @@ const path_1 = __importDefault(require("path"));
9
9
  const config_1 = require("../core/config");
10
10
  const template_schema_1 = require("../graph/template_schema");
11
11
  const node_1 = require("../graph/node");
12
+ const skills_indexer_1 = require("../graph/skills_indexer");
12
13
  const workspace_files_1 = require("../graph/workspace_files");
13
14
  const validate_graph_1 = require("../graph/validate_graph");
14
15
  const errors_1 = require("../util/errors");
@@ -116,6 +117,7 @@ function buildIndexNode(root, ws, filePath, node) {
116
117
  artifacts: node.artifacts,
117
118
  refs: node.refs,
118
119
  aliases: node.aliases,
120
+ skills: node.skills,
119
121
  path: path_1.default.relative(root, filePath),
120
122
  edges: normalizeEdges(node.edges, ws),
121
123
  };
@@ -128,6 +130,65 @@ function buildWorkspaceMap(config) {
128
130
  }
129
131
  return workspaces;
130
132
  }
133
+ function listDirectories(dirPath) {
134
+ if (!fs_1.default.existsSync(dirPath)) {
135
+ return [];
136
+ }
137
+ return fs_1.default
138
+ .readdirSync(dirPath, { withFileTypes: true })
139
+ .filter((entry) => entry.isDirectory())
140
+ .map((entry) => path_1.default.join(dirPath, entry.name))
141
+ .sort();
142
+ }
143
+ function validateEventsJsonl(root, config, errors) {
144
+ for (const [alias, workspace] of Object.entries(config.workspaces)) {
145
+ if (!workspace.enabled) {
146
+ continue;
147
+ }
148
+ const eventsPath = path_1.default.resolve(root, workspace.path, workspace.mdkg_dir, "work", "events", "events.jsonl");
149
+ if (!fs_1.default.existsSync(eventsPath)) {
150
+ continue;
151
+ }
152
+ const lines = fs_1.default.readFileSync(eventsPath, "utf8").split(/\r?\n/);
153
+ for (let i = 0; i < lines.length; i += 1) {
154
+ const raw = lines[i].trim();
155
+ if (!raw) {
156
+ continue;
157
+ }
158
+ let parsed;
159
+ try {
160
+ parsed = JSON.parse(raw);
161
+ }
162
+ catch {
163
+ errors.push(`${eventsPath}:${i + 1}: invalid JSON`);
164
+ continue;
165
+ }
166
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
167
+ errors.push(`${eventsPath}:${i + 1}: event must be a JSON object`);
168
+ continue;
169
+ }
170
+ const event = parsed;
171
+ for (const key of ["ts", "run_id", "workspace", "agent", "kind", "status"]) {
172
+ const value = event[key];
173
+ if (typeof value !== "string" || value.trim().length === 0) {
174
+ errors.push(`${eventsPath}:${i + 1}: ${key} is required and must be a non-empty string`);
175
+ }
176
+ }
177
+ if (!Array.isArray(event.refs)) {
178
+ errors.push(`${eventsPath}:${i + 1}: refs is required and must be a list`);
179
+ }
180
+ if (!Array.isArray(event.artifacts)) {
181
+ errors.push(`${eventsPath}:${i + 1}: artifacts is required and must be a list`);
182
+ }
183
+ if (typeof event.notes !== "string") {
184
+ errors.push(`${eventsPath}:${i + 1}: notes is required and must be a string`);
185
+ }
186
+ if (typeof event.workspace === "string" && event.workspace !== alias) {
187
+ errors.push(`${eventsPath}:${i + 1}: workspace must match ${alias}`);
188
+ }
189
+ }
190
+ }
191
+ }
131
192
  function runValidateCommand(options) {
132
193
  const config = (0, config_1.loadConfig)(options.root);
133
194
  const templateSchemas = (0, template_schema_1.loadTemplateSchemas)(options.root, config, node_1.ALLOWED_TYPES);
@@ -196,9 +257,45 @@ function runValidateCommand(options) {
196
257
  };
197
258
  const graphErrors = (0, validate_graph_1.collectGraphErrors)(index, { allowMissing: false });
198
259
  errors.push(...graphErrors);
260
+ try {
261
+ const skillsIndex = (0, skills_indexer_1.buildSkillsIndex)(options.root, config);
262
+ const knownSkills = new Set(Object.keys(skillsIndex.skills));
263
+ for (const node of Object.values(nodes)) {
264
+ for (const slug of node.skills) {
265
+ if (!knownSkills.has(slug)) {
266
+ errors.push(`${node.qid}: skills reference missing slug: ${slug}`);
267
+ }
268
+ }
269
+ }
270
+ }
271
+ catch (err) {
272
+ const message = err instanceof Error ? err.message : "unknown skill validation error";
273
+ errors.push(message);
274
+ }
275
+ const skillsRoot = (0, skills_indexer_1.resolveSkillsRoot)(options.root, config);
276
+ for (const dirPath of listDirectories(skillsRoot)) {
277
+ const canonicalPath = path_1.default.join(dirPath, "SKILL.md");
278
+ const compatPath = path_1.default.join(dirPath, "SKILLS.md");
279
+ const hasCanonical = fs_1.default.existsSync(canonicalPath);
280
+ const hasCompat = fs_1.default.existsSync(compatPath);
281
+ if (hasCanonical && hasCompat) {
282
+ errors.push(`${dirPath}: both SKILL.md and SKILLS.md exist`);
283
+ continue;
284
+ }
285
+ if (!hasCanonical && !hasCompat) {
286
+ errors.push(`${dirPath}: missing SKILL.md or SKILLS.md`);
287
+ continue;
288
+ }
289
+ if (hasCompat) {
290
+ warnings.push(`${path_1.default.relative(options.root, compatPath)}: using legacy SKILLS.md compatibility file`);
291
+ }
292
+ }
293
+ validateEventsJsonl(options.root, config, errors);
294
+ const uniqueWarnings = Array.from(new Set(warnings));
295
+ const uniqueErrors = Array.from(new Set(errors));
199
296
  const reportLines = [
200
- ...warnings.map((warning) => `warning: ${warning}`),
201
- ...errors,
297
+ ...uniqueWarnings.map((warning) => `warning: ${warning}`),
298
+ ...uniqueErrors,
202
299
  ];
203
300
  let outPath = undefined;
204
301
  if (options.out) {
@@ -207,20 +304,20 @@ function runValidateCommand(options) {
207
304
  fs_1.default.writeFileSync(outPath, reportLines.join("\n"), "utf8");
208
305
  }
209
306
  if (!options.quiet) {
210
- for (const warning of warnings) {
307
+ for (const warning of uniqueWarnings) {
211
308
  console.error(`warning: ${warning}`);
212
309
  }
213
310
  }
214
- if (errors.length > 0) {
311
+ if (uniqueErrors.length > 0) {
215
312
  if (outPath) {
216
- console.error(`validation failed: ${errors.length} error(s). details written to ${outPath}`);
313
+ console.error(`validation failed: ${uniqueErrors.length} error(s). details written to ${outPath}`);
217
314
  }
218
315
  else {
219
- for (const error of errors) {
316
+ for (const error of uniqueErrors) {
220
317
  console.error(error);
221
318
  }
222
319
  }
223
- throw new errors_1.ValidationError(`validation failed with ${errors.length} error(s)`);
320
+ throw new errors_1.ValidationError(`validation failed with ${uniqueErrors.length} error(s)`);
224
321
  }
225
322
  if (outPath) {
226
323
  console.log(`validation report written: ${outPath}`);
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.extractEdges = extractEdges;
4
- const ID_REF_RE = /^([a-z][a-z0-9_]*:)?[a-z]+-[0-9]+$/;
4
+ const id_1 = require("../util/id");
5
5
  function formatError(filePath, key, message) {
6
6
  return new Error(`${filePath}: ${key} ${message}`);
7
7
  }
@@ -10,7 +10,7 @@ function normalizeIdRef(value, filePath, key) {
10
10
  if (normalized !== value) {
11
11
  throw formatError(filePath, key, "must be lowercase");
12
12
  }
13
- if (!ID_REF_RE.test(normalized)) {
13
+ if (!(0, id_1.isCanonicalIdRef)(normalized)) {
14
14
  throw formatError(filePath, key, `invalid id reference: ${value}`);
15
15
  }
16
16
  return normalized;
@@ -24,6 +24,7 @@ exports.DEFAULT_FRONTMATTER_KEY_ORDER = [
24
24
  "blocks",
25
25
  "refs",
26
26
  "aliases",
27
+ "skills",
27
28
  "cases",
28
29
  "scope",
29
30
  "created",
@@ -81,6 +81,7 @@ function buildIndex(root, config, options = {}) {
81
81
  artifacts: node.artifacts,
82
82
  refs: node.refs,
83
83
  aliases: node.aliases,
84
+ skills: node.skills,
84
85
  path: relPath,
85
86
  edges: normalizedEdges,
86
87
  };
@@ -140,5 +141,25 @@ function buildIndex(root, config, options = {}) {
140
141
  reverse_edges,
141
142
  };
142
143
  (0, validate_graph_1.validateGraph)(index, { allowMissing: tolerant });
144
+ const latestCheckpointByWorkspace = {};
145
+ for (const alias of workspaceAliases) {
146
+ const candidates = Object.values(nodes)
147
+ .filter((node) => node.ws === alias && node.type === "checkpoint")
148
+ .sort((a, b) => {
149
+ if (a.updated !== b.updated) {
150
+ return b.updated.localeCompare(a.updated);
151
+ }
152
+ if (a.created !== b.created) {
153
+ return b.created.localeCompare(a.created);
154
+ }
155
+ return b.qid.localeCompare(a.qid);
156
+ });
157
+ if (candidates.length > 0) {
158
+ latestCheckpointByWorkspace[alias] = candidates[0].qid;
159
+ }
160
+ }
161
+ if (Object.keys(latestCheckpointByWorkspace).length > 0) {
162
+ index.meta.latest_checkpoint_qid = latestCheckpointByWorkspace;
163
+ }
143
164
  return index;
144
165
  }
@@ -4,7 +4,7 @@ exports.ALLOWED_TYPES = exports.DEC_TYPES = exports.WORK_TYPES = void 0;
4
4
  exports.parseNode = parseNode;
5
5
  const frontmatter_1 = require("./frontmatter");
6
6
  const edges_1 = require("./edges");
7
- const ID_RE = /^[a-z]+-[0-9]+$/;
7
+ const id_1 = require("../util/id");
8
8
  const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
9
9
  const DEC_ID_RE = /^dec-[0-9]+$/;
10
10
  exports.WORK_TYPES = new Set(["epic", "feat", "task", "bug", "checkpoint", "test"]);
@@ -23,6 +23,7 @@ exports.ALLOWED_TYPES = new Set([
23
23
  "test",
24
24
  ]);
25
25
  const DEC_STATUS = new Set(["proposed", "accepted", "rejected", "superseded"]);
26
+ const SKILL_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
26
27
  function formatError(filePath, message) {
27
28
  return new Error(`${filePath}: ${message}`);
28
29
  }
@@ -74,11 +75,11 @@ function requireLowercaseList(values, key, filePath) {
74
75
  });
75
76
  }
76
77
  function isValidId(value) {
77
- return ID_RE.test(value) || value === "rule-guide";
78
+ return (0, id_1.isCanonicalId)(value);
78
79
  }
79
80
  function requireIdFormat(value, key, filePath) {
80
81
  if (!isValidId(value)) {
81
- throw formatError(filePath, `${key} must match <prefix>-<number>`);
82
+ throw formatError(filePath, `${key} must match <prefix>-<number> or reserved id`);
82
83
  }
83
84
  return value;
84
85
  }
@@ -104,11 +105,20 @@ function normalizeIdList(values, key, filePath) {
104
105
  throw formatError(filePath, `${key} entries must be lowercase`);
105
106
  }
106
107
  if (!isValidId(value)) {
107
- throw formatError(filePath, `${key} entries must match <prefix>-<number>`);
108
+ throw formatError(filePath, `${key} entries must match <prefix>-<number> or reserved id`);
108
109
  }
109
110
  return value;
110
111
  });
111
112
  }
113
+ function normalizeSkillList(values, filePath) {
114
+ return values.map((value, index) => {
115
+ const normalized = value.toLowerCase();
116
+ if (!SKILL_SLUG_RE.test(normalized)) {
117
+ throw formatError(filePath, `skills[${index}] must be kebab-case`);
118
+ }
119
+ return normalized;
120
+ });
121
+ }
112
122
  function requireTemplateSchema(type, templateSchemas, filePath) {
113
123
  const schema = templateSchemas[type];
114
124
  if (!schema) {
@@ -192,6 +202,11 @@ function parseNode(content, filePath, options) {
192
202
  const artifacts = optionalList(frontmatter, "artifacts", filePath);
193
203
  const refs = normalizeIdList(optionalList(frontmatter, "refs", filePath), "refs", filePath);
194
204
  const aliases = requireLowercaseList(optionalList(frontmatter, "aliases", filePath), "aliases", filePath);
205
+ const skillsRaw = optionalList(frontmatter, "skills", filePath);
206
+ const skills = normalizeSkillList(skillsRaw, filePath);
207
+ if (skills.length > 0 && !exports.WORK_TYPES.has(type)) {
208
+ throw formatError(filePath, "skills is only allowed for work items");
209
+ }
195
210
  normalizeIdList(optionalList(frontmatter, "scope", filePath), "scope", filePath);
196
211
  const supersedesValue = optionalString(frontmatter, "supersedes", filePath);
197
212
  if (supersedesValue !== undefined) {
@@ -218,6 +233,7 @@ function parseNode(content, filePath, options) {
218
233
  artifacts,
219
234
  refs,
220
235
  aliases,
236
+ skills,
221
237
  edges,
222
238
  body,
223
239
  frontmatter,
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.writeSkillsIndex = writeSkillsIndex;
7
+ exports.loadSkillsIndex = loadSkillsIndex;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const paths_1 = require("../core/paths");
11
+ const skills_indexer_1 = require("./skills_indexer");
12
+ function mtimeMs(filePath) {
13
+ return fs_1.default.statSync(filePath).mtimeMs;
14
+ }
15
+ function listFilesAndDirectories(dir) {
16
+ if (!fs_1.default.existsSync(dir)) {
17
+ return [];
18
+ }
19
+ const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
20
+ const items = [dir];
21
+ for (const entry of entries) {
22
+ const fullPath = path_1.default.join(dir, entry.name);
23
+ if (entry.isDirectory()) {
24
+ items.push(...listFilesAndDirectories(fullPath));
25
+ continue;
26
+ }
27
+ if (entry.isFile()) {
28
+ items.push(fullPath);
29
+ }
30
+ }
31
+ return items;
32
+ }
33
+ function isSkillsIndexStale(root, config) {
34
+ const indexPath = (0, skills_indexer_1.resolveSkillsIndexPath)(root);
35
+ if (!fs_1.default.existsSync(indexPath)) {
36
+ return true;
37
+ }
38
+ const indexMtime = mtimeMs(indexPath);
39
+ const cfgPath = (0, paths_1.configPath)(root);
40
+ if (fs_1.default.existsSync(cfgPath) && mtimeMs(cfgPath) > indexMtime) {
41
+ return true;
42
+ }
43
+ const skillsRoot = (0, skills_indexer_1.resolveSkillsRoot)(root, config);
44
+ for (const item of listFilesAndDirectories(skillsRoot)) {
45
+ if (mtimeMs(item) > indexMtime) {
46
+ return true;
47
+ }
48
+ }
49
+ return false;
50
+ }
51
+ function readSkillsIndex(indexPath) {
52
+ try {
53
+ const raw = fs_1.default.readFileSync(indexPath, "utf8");
54
+ return JSON.parse(raw);
55
+ }
56
+ catch (err) {
57
+ const message = err instanceof Error ? err.message : "unknown error";
58
+ throw new Error(`failed to read skills index: ${message}`);
59
+ }
60
+ }
61
+ function writeSkillsIndex(indexPath, index) {
62
+ const sortedSkills = {};
63
+ for (const slug of Object.keys(index.skills).sort()) {
64
+ sortedSkills[slug] = index.skills[slug];
65
+ }
66
+ const sortedIndex = {
67
+ ...index,
68
+ skills: sortedSkills,
69
+ };
70
+ fs_1.default.mkdirSync(path_1.default.dirname(indexPath), { recursive: true });
71
+ fs_1.default.writeFileSync(indexPath, JSON.stringify(sortedIndex, null, 2), "utf8");
72
+ }
73
+ function loadSkillsIndex(options) {
74
+ const useCache = options.useCache ?? true;
75
+ const allowReindex = options.allowReindex ?? options.config.index.auto_reindex;
76
+ const indexPath = (0, skills_indexer_1.resolveSkillsIndexPath)(options.root);
77
+ if (!useCache) {
78
+ const index = (0, skills_indexer_1.buildSkillsIndex)(options.root, options.config);
79
+ return { index, rebuilt: true, stale: false };
80
+ }
81
+ const stale = isSkillsIndexStale(options.root, options.config);
82
+ if (fs_1.default.existsSync(indexPath) && !stale) {
83
+ return { index: readSkillsIndex(indexPath), rebuilt: false, stale: false };
84
+ }
85
+ if (allowReindex) {
86
+ const index = (0, skills_indexer_1.buildSkillsIndex)(options.root, options.config);
87
+ writeSkillsIndex(indexPath, index);
88
+ return { index, rebuilt: true, stale };
89
+ }
90
+ if (fs_1.default.existsSync(indexPath)) {
91
+ return { index: readSkillsIndex(indexPath), rebuilt: false, stale: true };
92
+ }
93
+ throw new Error("skills index missing and auto-reindex is disabled");
94
+ }