mdkg 0.0.2 → 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.
- package/README.md +171 -151
- package/dist/cli.js +920 -422
- package/dist/commands/checkpoint.js +17 -6
- package/dist/commands/doctor.js +156 -0
- package/dist/commands/event.js +46 -0
- package/dist/commands/event_support.js +146 -0
- package/dist/commands/format.js +6 -7
- package/dist/commands/index.js +10 -4
- package/dist/commands/init.js +202 -11
- package/dist/commands/list.js +18 -1
- package/dist/commands/new.js +30 -5
- package/dist/commands/pack.js +332 -10
- package/dist/commands/query_output.js +84 -0
- package/dist/commands/search.js +22 -5
- package/dist/commands/show.js +26 -11
- package/dist/commands/skill.js +359 -0
- package/dist/commands/skill_support.js +121 -0
- package/dist/commands/task.js +270 -0
- package/dist/commands/validate.js +104 -7
- package/dist/graph/edges.js +2 -2
- package/dist/graph/frontmatter.js +1 -0
- package/dist/graph/indexer.js +21 -0
- package/dist/graph/node.js +20 -4
- package/dist/graph/skills_index_cache.js +94 -0
- package/dist/graph/skills_indexer.js +160 -0
- package/dist/init/README.md +43 -0
- package/dist/init/core/rule-1-mdkg-conventions.md +9 -2
- package/dist/init/core/rule-3-cli-contract.md +73 -14
- package/dist/init/core/rule-4-repo-safety-and-ignores.md +9 -3
- package/dist/init/core/rule-6-templates-and-schemas.md +6 -2
- package/dist/init/skills/SKILL.md.example +41 -0
- package/dist/init/templates/default/bug.md +1 -0
- package/dist/init/templates/default/chk.md +1 -0
- package/dist/init/templates/default/epic.md +1 -0
- package/dist/init/templates/default/feat.md +1 -0
- package/dist/init/templates/default/task.md +1 -0
- package/dist/init/templates/default/test.md +1 -0
- package/dist/pack/budget.js +186 -0
- package/dist/pack/export_md.js +17 -1
- package/dist/pack/export_xml.js +15 -0
- package/dist/pack/metrics.js +66 -0
- package/dist/pack/pack.js +35 -0
- package/dist/pack/profile.js +222 -0
- package/dist/pack/stats.js +37 -0
- package/dist/templates/headings.js +34 -0
- package/dist/util/argparse.js +47 -1
- package/dist/util/filter.js +18 -0
- package/dist/util/id.js +23 -0
- package/dist/util/output.js +2 -2
- package/package.json +6 -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
|
-
...
|
|
201
|
-
...
|
|
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
|
|
307
|
+
for (const warning of uniqueWarnings) {
|
|
211
308
|
console.error(`warning: ${warning}`);
|
|
212
309
|
}
|
|
213
310
|
}
|
|
214
|
-
if (
|
|
311
|
+
if (uniqueErrors.length > 0) {
|
|
215
312
|
if (outPath) {
|
|
216
|
-
console.error(`validation failed: ${
|
|
313
|
+
console.error(`validation failed: ${uniqueErrors.length} error(s). details written to ${outPath}`);
|
|
217
314
|
}
|
|
218
315
|
else {
|
|
219
|
-
for (const error of
|
|
316
|
+
for (const error of uniqueErrors) {
|
|
220
317
|
console.error(error);
|
|
221
318
|
}
|
|
222
319
|
}
|
|
223
|
-
throw new errors_1.ValidationError(`validation failed with ${
|
|
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}`);
|
package/dist/graph/edges.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.extractEdges = extractEdges;
|
|
4
|
-
const
|
|
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 (!
|
|
13
|
+
if (!(0, id_1.isCanonicalIdRef)(normalized)) {
|
|
14
14
|
throw formatError(filePath, key, `invalid id reference: ${value}`);
|
|
15
15
|
}
|
|
16
16
|
return normalized;
|
package/dist/graph/indexer.js
CHANGED
|
@@ -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
|
}
|
package/dist/graph/node.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
+
}
|