mdkg 0.3.1 → 0.3.3
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/CHANGELOG.md +81 -0
- package/CLI_COMMAND_MATRIX.md +26 -8
- package/README.md +55 -5
- package/dist/cli.js +44 -7
- package/dist/command-contract.json +171 -4
- package/dist/commands/fix.js +37 -27
- package/dist/commands/goal.js +149 -13
- package/dist/commands/new.js +4 -3
- package/dist/commands/next.js +2 -2
- package/dist/commands/task.js +2 -2
- package/dist/commands/validate.js +11 -0
- package/dist/graph/goal_scope.js +1 -1
- package/dist/graph/node.js +6 -4
- package/dist/graph/validate_graph.js +21 -0
- package/dist/init/AGENT_START.md +2 -2
- package/dist/init/CLI_COMMAND_MATRIX.md +13 -3
- package/dist/init/README.md +20 -3
- package/dist/init/core/rule-5-release-and-versioning.md +13 -4
- package/dist/init/init-manifest.json +11 -6
- package/dist/init/skills/default/pursue-mdkg-goal/SKILL.md +2 -1
- package/dist/init/templates/default/spike.md +81 -0
- package/dist/pack/order.js +3 -2
- package/package.json +4 -2
package/dist/commands/fix.js
CHANGED
|
@@ -288,7 +288,43 @@ function normalizeGraphRef(value, sourceWorkspace, knownWorkspaces, externalWork
|
|
|
288
288
|
function frontmatterRefEntries(index, externalWorkspaces) {
|
|
289
289
|
const knownWorkspaces = new Set(index.meta.workspaces);
|
|
290
290
|
const entries = [];
|
|
291
|
+
const pushListEntries = (node, field, raw) => {
|
|
292
|
+
for (const [indexValue, value] of raw.entries()) {
|
|
293
|
+
const indexedField = `${field}[${indexValue}]`;
|
|
294
|
+
if (LOCAL_LIST_REF_FIELDS.has(field)) {
|
|
295
|
+
const target = normalizeGraphRef(value, node.ws, knownWorkspaces, externalWorkspaces);
|
|
296
|
+
if (target) {
|
|
297
|
+
entries.push({
|
|
298
|
+
qid: node.qid,
|
|
299
|
+
path: node.path,
|
|
300
|
+
field: indexedField,
|
|
301
|
+
value,
|
|
302
|
+
target,
|
|
303
|
+
refKind: "graph",
|
|
304
|
+
locationKind: "frontmatter",
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (value.startsWith("archive://") && ARCHIVE_REF_LIST_FIELDS.has(field)) {
|
|
309
|
+
entries.push({
|
|
310
|
+
qid: node.qid,
|
|
311
|
+
path: node.path,
|
|
312
|
+
field: indexedField,
|
|
313
|
+
value,
|
|
314
|
+
refKind: "archive",
|
|
315
|
+
locationKind: "frontmatter",
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
};
|
|
291
320
|
for (const node of Object.values(index.nodes).sort((a, b) => a.qid.localeCompare(b.qid))) {
|
|
321
|
+
for (const [field, raw] of [
|
|
322
|
+
["links", node.links],
|
|
323
|
+
["artifacts", node.artifacts],
|
|
324
|
+
["refs", node.refs],
|
|
325
|
+
]) {
|
|
326
|
+
pushListEntries(node, field, raw);
|
|
327
|
+
}
|
|
292
328
|
for (const [field, raw] of Object.entries(node.attributes).sort(([a], [b]) => a.localeCompare(b))) {
|
|
293
329
|
if (typeof raw === "string") {
|
|
294
330
|
if (LOCAL_SCALAR_REF_FIELDS.has(field)) {
|
|
@@ -320,33 +356,7 @@ function frontmatterRefEntries(index, externalWorkspaces) {
|
|
|
320
356
|
if (!Array.isArray(raw)) {
|
|
321
357
|
continue;
|
|
322
358
|
}
|
|
323
|
-
|
|
324
|
-
const indexedField = `${field}[${indexValue}]`;
|
|
325
|
-
if (LOCAL_LIST_REF_FIELDS.has(field)) {
|
|
326
|
-
const target = normalizeGraphRef(value, node.ws, knownWorkspaces, externalWorkspaces);
|
|
327
|
-
if (target) {
|
|
328
|
-
entries.push({
|
|
329
|
-
qid: node.qid,
|
|
330
|
-
path: node.path,
|
|
331
|
-
field: indexedField,
|
|
332
|
-
value,
|
|
333
|
-
target,
|
|
334
|
-
refKind: "graph",
|
|
335
|
-
locationKind: "frontmatter",
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
if (value.startsWith("archive://") && ARCHIVE_REF_LIST_FIELDS.has(field)) {
|
|
340
|
-
entries.push({
|
|
341
|
-
qid: node.qid,
|
|
342
|
-
path: node.path,
|
|
343
|
-
field: indexedField,
|
|
344
|
-
value,
|
|
345
|
-
refKind: "archive",
|
|
346
|
-
locationKind: "frontmatter",
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
}
|
|
359
|
+
pushListEntries(node, field, raw);
|
|
350
360
|
}
|
|
351
361
|
}
|
|
352
362
|
return entries;
|
package/dist/commands/goal.js
CHANGED
|
@@ -7,12 +7,14 @@ exports.runGoalShowCommand = runGoalShowCommand;
|
|
|
7
7
|
exports.runGoalEvaluateCommand = runGoalEvaluateCommand;
|
|
8
8
|
exports.runGoalNextCommand = runGoalNextCommand;
|
|
9
9
|
exports.runGoalSelectCommand = runGoalSelectCommand;
|
|
10
|
+
exports.runGoalActivateCommand = runGoalActivateCommand;
|
|
10
11
|
exports.runGoalCurrentCommand = runGoalCurrentCommand;
|
|
11
12
|
exports.runGoalClearCommand = runGoalClearCommand;
|
|
12
13
|
exports.runGoalClaimCommand = runGoalClaimCommand;
|
|
13
14
|
exports.runGoalPauseCommand = runGoalPauseCommand;
|
|
14
15
|
exports.runGoalResumeCommand = runGoalResumeCommand;
|
|
15
16
|
exports.runGoalDoneCommand = runGoalDoneCommand;
|
|
17
|
+
exports.runGoalArchiveCommand = runGoalArchiveCommand;
|
|
16
18
|
const fs_1 = __importDefault(require("fs"));
|
|
17
19
|
const path_1 = __importDefault(require("path"));
|
|
18
20
|
const config_1 = require("../core/config");
|
|
@@ -29,17 +31,19 @@ const qid_1 = require("../util/qid");
|
|
|
29
31
|
const sort_1 = require("../util/sort");
|
|
30
32
|
const event_support_1 = require("./event_support");
|
|
31
33
|
const node_card_1 = require("./node_card");
|
|
32
|
-
const CONCRETE_GOAL_NEXT_TYPES = new Set(["feat", "task", "bug", "test"]);
|
|
34
|
+
const CONCRETE_GOAL_NEXT_TYPES = new Set(["feat", "task", "bug", "test", "spike"]);
|
|
33
35
|
const SELECTED_GOAL_STATE_PATH = path_1.default.join(".mdkg", "state", "selected-goal.json");
|
|
34
36
|
const GOAL_STATE_BY_ACTION = {
|
|
35
37
|
pause: "paused",
|
|
36
38
|
resume: "active",
|
|
37
39
|
done: "achieved",
|
|
40
|
+
archive: "archived",
|
|
38
41
|
};
|
|
39
42
|
const STATUS_BY_ACTION = {
|
|
40
43
|
pause: "blocked",
|
|
41
44
|
resume: "progress",
|
|
42
45
|
done: "done",
|
|
46
|
+
archive: "archived",
|
|
43
47
|
};
|
|
44
48
|
function normalizeWorkspace(value) {
|
|
45
49
|
if (!value) {
|
|
@@ -95,6 +99,22 @@ function writeSelectedGoalState(root, node, now) {
|
|
|
95
99
|
};
|
|
96
100
|
(0, atomic_1.atomicWriteFile)(selectedGoalPath(root), `${JSON.stringify(state, null, 2)}\n`);
|
|
97
101
|
}
|
|
102
|
+
function readNodeFrontmatter(root, node) {
|
|
103
|
+
const filePath = path_1.default.resolve(root, node.path);
|
|
104
|
+
const parsed = (0, frontmatter_1.parseFrontmatter)(fs_1.default.readFileSync(filePath, "utf8"), filePath);
|
|
105
|
+
return {
|
|
106
|
+
filePath,
|
|
107
|
+
frontmatter: { ...parsed.frontmatter },
|
|
108
|
+
body: parsed.body,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function writeNodeFrontmatterFile(filePath, frontmatter, body, now) {
|
|
112
|
+
frontmatter.updated = (0, date_1.formatDate)(now);
|
|
113
|
+
const lines = (0, frontmatter_1.formatFrontmatter)(frontmatter, frontmatter_1.DEFAULT_FRONTMATTER_KEY_ORDER);
|
|
114
|
+
const frontmatterBlock = ["---", ...lines, "---"].join("\n");
|
|
115
|
+
const content = body.length > 0 ? `${frontmatterBlock}\n${body}` : frontmatterBlock;
|
|
116
|
+
(0, atomic_1.atomicWriteFile)(filePath, content);
|
|
117
|
+
}
|
|
98
118
|
function removeSelectedGoalState(root) {
|
|
99
119
|
const filePath = selectedGoalPath(root);
|
|
100
120
|
if (!fs_1.default.existsSync(filePath)) {
|
|
@@ -125,6 +145,14 @@ function activeGoalCandidates(index, wsHint) {
|
|
|
125
145
|
.filter((node) => node.status === "progress" && node.attributes.goal_state === "active")
|
|
126
146
|
.sort((a, b) => a.qid.localeCompare(b.qid));
|
|
127
147
|
}
|
|
148
|
+
function activeGoalConflicts(index, target) {
|
|
149
|
+
return activeGoalCandidates(index, target.ws).filter((node) => node.qid !== target.qid);
|
|
150
|
+
}
|
|
151
|
+
function isArchivedGoal(node) {
|
|
152
|
+
return Boolean(node &&
|
|
153
|
+
node.type === "goal" &&
|
|
154
|
+
(node.status === "archived" || node.attributes.goal_state === "archived"));
|
|
155
|
+
}
|
|
128
156
|
function resolveGoalSelection(root, index, idOrQid, wsHint) {
|
|
129
157
|
const warnings = [];
|
|
130
158
|
if (idOrQid) {
|
|
@@ -137,19 +165,24 @@ function resolveGoalSelection(root, index, idOrQid, wsHint) {
|
|
|
137
165
|
const selected = readSelectedGoalState(root, warnings);
|
|
138
166
|
if (selected) {
|
|
139
167
|
const node = index.nodes[selected.qid];
|
|
140
|
-
if (node && node.type === "goal" && !node.source?.imported) {
|
|
168
|
+
if (node && node.type === "goal" && !node.source?.imported && !isArchivedGoal(node)) {
|
|
141
169
|
return { node, source: "selected", warnings };
|
|
142
170
|
}
|
|
143
|
-
|
|
171
|
+
if (isArchivedGoal(node)) {
|
|
172
|
+
warnings.push(`selected goal ${selected.qid} is archived; run \`mdkg goal activate <goal-id>\` or \`mdkg goal clear\``);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
warnings.push(`selected goal ${selected.qid} is not available; run \`mdkg goal select <goal-id>\``);
|
|
176
|
+
}
|
|
144
177
|
}
|
|
145
178
|
const active = activeGoalCandidates(index, wsHint);
|
|
146
179
|
if (active.length === 1) {
|
|
147
180
|
return { node: active[0], source: "unique_active", warnings };
|
|
148
181
|
}
|
|
149
182
|
if (active.length > 1) {
|
|
150
|
-
throw new errors_1.UsageError(`multiple active goals found: ${active.map((node) => node.qid).join(", ")}; run \`mdkg goal
|
|
183
|
+
throw new errors_1.UsageError(`multiple active goals found: ${active.map((node) => node.qid).join(", ")}; run \`mdkg goal activate <goal-id>\``);
|
|
151
184
|
}
|
|
152
|
-
throw new errors_1.NotFoundError("no selected goal or unique active goal found; run `mdkg goal
|
|
185
|
+
throw new errors_1.NotFoundError("no selected goal or unique active goal found; run `mdkg goal activate <goal-id>`");
|
|
153
186
|
}
|
|
154
187
|
function loadGoal(root, idOrQid, wsHint) {
|
|
155
188
|
const config = (0, config_1.loadConfig)(root);
|
|
@@ -201,11 +234,7 @@ function goalReceipt(root, loaded) {
|
|
|
201
234
|
};
|
|
202
235
|
}
|
|
203
236
|
function writeGoalFile(loaded, now) {
|
|
204
|
-
loaded.frontmatter.
|
|
205
|
-
const lines = (0, frontmatter_1.formatFrontmatter)(loaded.frontmatter, frontmatter_1.DEFAULT_FRONTMATTER_KEY_ORDER);
|
|
206
|
-
const frontmatterBlock = ["---", ...lines, "---"].join("\n");
|
|
207
|
-
const content = loaded.body.length > 0 ? `${frontmatterBlock}\n${loaded.body}` : frontmatterBlock;
|
|
208
|
-
(0, atomic_1.atomicWriteFile)(loaded.filePath, content);
|
|
237
|
+
writeNodeFrontmatterFile(loaded.filePath, loaded.frontmatter, loaded.body, now);
|
|
209
238
|
}
|
|
210
239
|
function maybeReindex(root, config) {
|
|
211
240
|
if (!config.index.auto_reindex) {
|
|
@@ -215,6 +244,9 @@ function maybeReindex(root, config) {
|
|
|
215
244
|
}
|
|
216
245
|
function ensureStatusAllowed(config, status) {
|
|
217
246
|
const normalized = status.toLowerCase();
|
|
247
|
+
if (normalized === "archived") {
|
|
248
|
+
return normalized;
|
|
249
|
+
}
|
|
218
250
|
const allowed = new Set(config.work.status_enum.map((value) => value.toLowerCase()));
|
|
219
251
|
if (!allowed.has(normalized)) {
|
|
220
252
|
throw new errors_1.UsageError(`goal status ${normalized} is not allowed by work.status_enum`);
|
|
@@ -301,6 +333,24 @@ function runGoalEvaluateCommand(options) {
|
|
|
301
333
|
}
|
|
302
334
|
function runGoalNextCommand(options) {
|
|
303
335
|
const loaded = loadGoal(options.root, options.id, options.ws);
|
|
336
|
+
if (isArchivedGoal(loaded.node)) {
|
|
337
|
+
const warnings = [...loaded.warnings, `${loaded.node.qid} is archived and has no actionable next work`];
|
|
338
|
+
if (options.json) {
|
|
339
|
+
console.log(JSON.stringify({
|
|
340
|
+
action: "selected",
|
|
341
|
+
goal: goalReceipt(options.root, loaded),
|
|
342
|
+
goal_source: loaded.resolutionSource,
|
|
343
|
+
node: null,
|
|
344
|
+
warnings,
|
|
345
|
+
}, null, 2));
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
for (const warning of warnings) {
|
|
349
|
+
console.error(`warning: ${warning}`);
|
|
350
|
+
}
|
|
351
|
+
console.error("no actionable local work found for goal");
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
304
354
|
const statusPreference = loaded.config.work.next.status_preference.map((status) => status.toLowerCase());
|
|
305
355
|
const statusRanks = new Set(statusPreference);
|
|
306
356
|
const warnings = [...loaded.warnings];
|
|
@@ -362,6 +412,9 @@ function runGoalSelectCommand(options) {
|
|
|
362
412
|
const config = (0, config_1.loadConfig)(options.root);
|
|
363
413
|
return (0, lock_1.withMutationLock)(options.root, config.index.lock_timeout_ms, () => {
|
|
364
414
|
const loaded = loadGoal(options.root, options.id, options.ws);
|
|
415
|
+
if (isArchivedGoal(loaded.node)) {
|
|
416
|
+
throw new errors_1.UsageError(`cannot select archived goal ${loaded.node.qid}`);
|
|
417
|
+
}
|
|
365
418
|
const now = options.now ?? new Date();
|
|
366
419
|
writeSelectedGoalState(options.root, loaded.node, now);
|
|
367
420
|
const receipt = {
|
|
@@ -379,6 +432,74 @@ function runGoalSelectCommand(options) {
|
|
|
379
432
|
console.log(`selected goal: ${loaded.node.qid}`);
|
|
380
433
|
});
|
|
381
434
|
}
|
|
435
|
+
function runGoalActivateCommand(options) {
|
|
436
|
+
const config = (0, config_1.loadConfig)(options.root);
|
|
437
|
+
return (0, lock_1.withMutationLock)(options.root, config.index.lock_timeout_ms, () => {
|
|
438
|
+
const loaded = loadGoal(options.root, options.id, options.ws);
|
|
439
|
+
const currentState = String(loaded.frontmatter.goal_state ?? "");
|
|
440
|
+
const currentStatus = String(loaded.frontmatter.status ?? "");
|
|
441
|
+
if (loaded.node.status === "done" || currentStatus === "done" || currentState === "achieved") {
|
|
442
|
+
throw new errors_1.UsageError(`cannot activate achieved goal ${loaded.node.qid}`);
|
|
443
|
+
}
|
|
444
|
+
if (loaded.node.status === "archived" || currentStatus === "archived" || currentState === "archived") {
|
|
445
|
+
throw new errors_1.UsageError(`cannot activate archived goal ${loaded.node.qid}`);
|
|
446
|
+
}
|
|
447
|
+
const now = options.now ?? new Date();
|
|
448
|
+
const pausedGoals = [];
|
|
449
|
+
const conflicts = activeGoalConflicts(loaded.index, loaded.node);
|
|
450
|
+
for (const conflict of conflicts) {
|
|
451
|
+
const conflictFile = readNodeFrontmatter(options.root, conflict);
|
|
452
|
+
conflictFile.frontmatter.goal_state = "paused";
|
|
453
|
+
conflictFile.frontmatter.status = ensureStatusAllowed(config, "blocked");
|
|
454
|
+
writeNodeFrontmatterFile(conflictFile.filePath, conflictFile.frontmatter, conflictFile.body, now);
|
|
455
|
+
pausedGoals.push({
|
|
456
|
+
workspace: conflict.ws,
|
|
457
|
+
id: conflict.id,
|
|
458
|
+
qid: conflict.qid,
|
|
459
|
+
path: conflict.path,
|
|
460
|
+
previous_status: conflict.status ?? "",
|
|
461
|
+
previous_goal_state: String(conflict.attributes.goal_state ?? ""),
|
|
462
|
+
status: "blocked",
|
|
463
|
+
goal_state: "paused",
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
loaded.frontmatter.goal_state = "active";
|
|
467
|
+
loaded.frontmatter.status = ensureStatusAllowed(config, "progress");
|
|
468
|
+
writeGoalFile(loaded, now);
|
|
469
|
+
writeSelectedGoalState(options.root, loaded.node, now);
|
|
470
|
+
maybeReindex(options.root, loaded.config);
|
|
471
|
+
(0, event_support_1.appendAutomaticEvent)({
|
|
472
|
+
root: options.root,
|
|
473
|
+
ws: loaded.node.ws,
|
|
474
|
+
kind: "GOAL_ACTIVATE",
|
|
475
|
+
status: "ok",
|
|
476
|
+
refs: [loaded.node.id, ...conflicts.map((node) => node.id)],
|
|
477
|
+
notes: `goal activate via mdkg goal activate`,
|
|
478
|
+
now,
|
|
479
|
+
});
|
|
480
|
+
const receipt = {
|
|
481
|
+
action: "activated",
|
|
482
|
+
goal: goalReceipt(options.root, loaded),
|
|
483
|
+
activated_goal: goalReceipt(options.root, loaded),
|
|
484
|
+
paused_goals: pausedGoals,
|
|
485
|
+
selection: {
|
|
486
|
+
path: SELECTED_GOAL_STATE_PATH,
|
|
487
|
+
selected_at: now.toISOString(),
|
|
488
|
+
},
|
|
489
|
+
warnings: conflicts.length > 0
|
|
490
|
+
? [`paused ${conflicts.length} competing active goal(s) in workspace ${loaded.node.ws}`]
|
|
491
|
+
: [],
|
|
492
|
+
};
|
|
493
|
+
if (options.json) {
|
|
494
|
+
console.log(JSON.stringify(receipt, null, 2));
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
for (const warning of receipt.warnings) {
|
|
498
|
+
console.error(`warning: ${warning}`);
|
|
499
|
+
}
|
|
500
|
+
console.log(`goal activate: ${loaded.node.qid}`);
|
|
501
|
+
});
|
|
502
|
+
}
|
|
382
503
|
function runGoalCurrentCommand(options) {
|
|
383
504
|
const config = (0, config_1.loadConfig)(options.root);
|
|
384
505
|
const { index } = (0, index_cache_1.loadIndex)({ root: options.root, config });
|
|
@@ -389,12 +510,17 @@ function runGoalCurrentCommand(options) {
|
|
|
389
510
|
const selected = readSelectedGoalState(options.root, warnings);
|
|
390
511
|
if (selected) {
|
|
391
512
|
const selectedNode = index.nodes[selected.qid];
|
|
392
|
-
if (selectedNode && selectedNode.type === "goal" && !selectedNode.source?.imported) {
|
|
513
|
+
if (selectedNode && selectedNode.type === "goal" && !selectedNode.source?.imported && !isArchivedGoal(selectedNode)) {
|
|
393
514
|
node = selectedNode;
|
|
394
515
|
source = "selected";
|
|
395
516
|
}
|
|
396
517
|
else {
|
|
397
|
-
|
|
518
|
+
if (isArchivedGoal(selectedNode)) {
|
|
519
|
+
warnings.push(`selected goal ${selected.qid} is archived; run \`mdkg goal activate <goal-id>\` or \`mdkg goal clear\``);
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
warnings.push(`selected goal ${selected.qid} is not available; run \`mdkg goal select <goal-id>\``);
|
|
523
|
+
}
|
|
398
524
|
}
|
|
399
525
|
}
|
|
400
526
|
if (!node) {
|
|
@@ -405,7 +531,7 @@ function runGoalCurrentCommand(options) {
|
|
|
405
531
|
}
|
|
406
532
|
else if (active.length > 1) {
|
|
407
533
|
source = "ambiguous";
|
|
408
|
-
warnings.push(`multiple active goals found: ${active.map((goal) => goal.qid).join(", ")}
|
|
534
|
+
warnings.push(`multiple active goals found: ${active.map((goal) => goal.qid).join(", ")}; run \`mdkg goal activate <goal-id>\``);
|
|
409
535
|
}
|
|
410
536
|
}
|
|
411
537
|
const receipt = {
|
|
@@ -464,6 +590,9 @@ function runGoalClaimCommand(options) {
|
|
|
464
590
|
const config = (0, config_1.loadConfig)(options.root);
|
|
465
591
|
return (0, lock_1.withMutationLock)(options.root, config.index.lock_timeout_ms, () => {
|
|
466
592
|
const loaded = loadGoal(options.root, options.id, options.ws);
|
|
593
|
+
if (isArchivedGoal(loaded.node)) {
|
|
594
|
+
throw new errors_1.UsageError(`cannot claim work for archived goal ${loaded.node.qid}`);
|
|
595
|
+
}
|
|
467
596
|
const resolved = (0, qid_1.resolveQid)(loaded.index, options.workId, loaded.node.ws);
|
|
468
597
|
if (resolved.status !== "ok") {
|
|
469
598
|
throw new errors_1.NotFoundError((0, qid_1.formatResolveError)("work", options.workId, resolved, loaded.node.ws));
|
|
@@ -510,6 +639,9 @@ function runGoalClaimCommand(options) {
|
|
|
510
639
|
}
|
|
511
640
|
function runGoalStateMutationLocked(action, options) {
|
|
512
641
|
const loaded = loadGoal(options.root, options.id, options.ws);
|
|
642
|
+
if (action !== "archive" && isArchivedGoal(loaded.node)) {
|
|
643
|
+
throw new errors_1.UsageError(`cannot ${action} archived goal ${loaded.node.qid}`);
|
|
644
|
+
}
|
|
513
645
|
const now = options.now ?? new Date();
|
|
514
646
|
loaded.frontmatter.goal_state = GOAL_STATE_BY_ACTION[action];
|
|
515
647
|
loaded.frontmatter.status = ensureStatusAllowed(loaded.config, STATUS_BY_ACTION[action]);
|
|
@@ -546,3 +678,7 @@ function runGoalDoneCommand(options) {
|
|
|
546
678
|
const config = (0, config_1.loadConfig)(options.root);
|
|
547
679
|
return (0, lock_1.withMutationLock)(options.root, config.index.lock_timeout_ms, () => runGoalStateMutationLocked("done", options));
|
|
548
680
|
}
|
|
681
|
+
function runGoalArchiveCommand(options) {
|
|
682
|
+
const config = (0, config_1.loadConfig)(options.root);
|
|
683
|
+
return (0, lock_1.withMutationLock)(options.root, config.index.lock_timeout_ms, () => runGoalStateMutationLocked("archive", options));
|
|
684
|
+
}
|
package/dist/commands/new.js
CHANGED
|
@@ -228,11 +228,12 @@ function runNewCommandLocked(options) {
|
|
|
228
228
|
let status;
|
|
229
229
|
if (node_1.WORK_TYPES.has(type)) {
|
|
230
230
|
const allowed = new Set(config.work.status_enum.map((value) => value.toLowerCase()));
|
|
231
|
+
const allowedForType = type === "goal" ? new Set([...allowed, "archived"]) : allowed;
|
|
231
232
|
status = statusInput ?? (type === "goal" && allowed.has("progress")
|
|
232
233
|
? "progress"
|
|
233
234
|
: config.work.status_enum[0]?.toLowerCase());
|
|
234
|
-
if (!status || !
|
|
235
|
-
throw new errors_1.UsageError(`--status must be one of ${Array.from(
|
|
235
|
+
if (!status || !allowedForType.has(status)) {
|
|
236
|
+
throw new errors_1.UsageError(`--status must be one of ${Array.from(allowedForType).join(", ")}`);
|
|
236
237
|
}
|
|
237
238
|
}
|
|
238
239
|
else if (type === "dec") {
|
|
@@ -346,7 +347,7 @@ function runNewCommandLocked(options) {
|
|
|
346
347
|
skills: skills.length > 0 ? skills : undefined,
|
|
347
348
|
cases: cases.length > 0 ? cases : undefined,
|
|
348
349
|
supersedes: options.supersedes ? options.supersedes.toLowerCase() : undefined,
|
|
349
|
-
goal_state: type === "goal" ? (status === "done" ? "achieved" : status === "blocked" ? "blocked" : "active") : undefined,
|
|
350
|
+
goal_state: type === "goal" ? (status === "done" ? "achieved" : status === "blocked" ? "blocked" : status === "archived" ? "archived" : "active") : undefined,
|
|
350
351
|
goal_condition: type === "goal" ? title : undefined,
|
|
351
352
|
max_iterations: type === "goal" ? 25 : undefined,
|
|
352
353
|
blocked_after_attempts: type === "goal" ? 3 : undefined,
|
package/dist/commands/next.js
CHANGED
|
@@ -7,8 +7,8 @@ const errors_1 = require("../util/errors");
|
|
|
7
7
|
const qid_1 = require("../util/qid");
|
|
8
8
|
const sort_1 = require("../util/sort");
|
|
9
9
|
const node_card_1 = require("./node_card");
|
|
10
|
-
const NEXT_TYPES = new Set(["feat", "task", "bug", "test"]);
|
|
11
|
-
const NO_MATCH_MESSAGE = 'no matching work items found; consider `mdkg new task "..."` or `mdkg new
|
|
10
|
+
const NEXT_TYPES = new Set(["feat", "task", "bug", "test", "spike"]);
|
|
11
|
+
const NO_MATCH_MESSAGE = 'no matching work items found; consider `mdkg new task "..."`, `mdkg new test "..."`, or `mdkg new spike "..."`';
|
|
12
12
|
function normalizeWorkspace(value) {
|
|
13
13
|
if (!value || value === "all") {
|
|
14
14
|
return undefined;
|
package/dist/commands/task.js
CHANGED
|
@@ -22,7 +22,7 @@ const atomic_1 = require("../util/atomic");
|
|
|
22
22
|
const lock_1 = require("../util/lock");
|
|
23
23
|
const event_support_1 = require("./event_support");
|
|
24
24
|
const checkpoint_1 = require("./checkpoint");
|
|
25
|
-
const MUTABLE_TASK_TYPES = new Set(["feat", "task", "bug", "test"]);
|
|
25
|
+
const MUTABLE_TASK_TYPES = new Set(["feat", "task", "bug", "test", "spike"]);
|
|
26
26
|
const SKILL_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
27
27
|
function parseCsvList(raw) {
|
|
28
28
|
if (!raw) {
|
|
@@ -113,7 +113,7 @@ function loadMutableTaskNode(root, idOrQid, wsHint) {
|
|
|
113
113
|
throw new errors_1.UsageError(`cannot mutate read-only subgraph node ${node.qid}; update the source workspace for subgraph ${node.source.subgraph_alias}`);
|
|
114
114
|
}
|
|
115
115
|
if (!MUTABLE_TASK_TYPES.has(node.type)) {
|
|
116
|
-
throw new errors_1.UsageError(`mdkg task only supports feat, task, bug, and
|
|
116
|
+
throw new errors_1.UsageError(`mdkg task only supports feat, task, bug, test, and spike nodes; use markdown editing for ${node.type}:${node.id}`);
|
|
117
117
|
}
|
|
118
118
|
const filePath = path_1.default.resolve(root, node.path);
|
|
119
119
|
const content = fs_1.default.readFileSync(filePath, "utf8");
|
|
@@ -36,6 +36,17 @@ const RECOMMENDED_HEADINGS = {
|
|
|
36
36
|
"Links / Artifacts",
|
|
37
37
|
],
|
|
38
38
|
feat: ["Overview", "Acceptance Criteria", "Notes"],
|
|
39
|
+
spike: [
|
|
40
|
+
"Research Question",
|
|
41
|
+
"Context And Constraints",
|
|
42
|
+
"Search Plan",
|
|
43
|
+
"Findings",
|
|
44
|
+
"Options And Tradeoffs",
|
|
45
|
+
"Recommendation",
|
|
46
|
+
"Follow-Up Nodes To Create",
|
|
47
|
+
"Skill Candidates",
|
|
48
|
+
"Evidence And Sources",
|
|
49
|
+
],
|
|
39
50
|
epic: ["Goal", "Scope", "Milestones", "Out of Scope", "Risks", "Links / Artifacts"],
|
|
40
51
|
checkpoint: [
|
|
41
52
|
"Summary",
|
package/dist/graph/goal_scope.js
CHANGED
|
@@ -5,7 +5,7 @@ exports.goalScopeRefs = goalScopeRefs;
|
|
|
5
5
|
exports.collectGoalScope = collectGoalScope;
|
|
6
6
|
const qid_1 = require("../util/qid");
|
|
7
7
|
exports.GOAL_SCOPE_CONTAINER_TYPES = new Set(["epic", "feat"]);
|
|
8
|
-
exports.GOAL_SCOPE_ACTIONABLE_TYPES = new Set(["feat", "task", "bug", "test"]);
|
|
8
|
+
exports.GOAL_SCOPE_ACTIONABLE_TYPES = new Set(["feat", "task", "bug", "test", "spike"]);
|
|
9
9
|
exports.GOAL_SCOPE_ALLOWED_TYPES = new Set([
|
|
10
10
|
...exports.GOAL_SCOPE_CONTAINER_TYPES,
|
|
11
11
|
...exports.GOAL_SCOPE_ACTIONABLE_TYPES,
|
package/dist/graph/node.js
CHANGED
|
@@ -10,7 +10,7 @@ const id_1 = require("../util/id");
|
|
|
10
10
|
const refs_1 = require("../util/refs");
|
|
11
11
|
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
12
12
|
const DEC_ID_RE = /^dec-[0-9]+$/;
|
|
13
|
-
exports.WORK_TYPES = new Set(["goal", "epic", "feat", "task", "bug", "checkpoint", "test"]);
|
|
13
|
+
exports.WORK_TYPES = new Set(["goal", "epic", "feat", "task", "bug", "spike", "checkpoint", "test"]);
|
|
14
14
|
exports.DEC_TYPES = new Set(["dec"]);
|
|
15
15
|
exports.ALLOWED_TYPES = new Set([
|
|
16
16
|
"rule",
|
|
@@ -23,13 +23,14 @@ exports.ALLOWED_TYPES = new Set([
|
|
|
23
23
|
"feat",
|
|
24
24
|
"task",
|
|
25
25
|
"bug",
|
|
26
|
+
"spike",
|
|
26
27
|
"checkpoint",
|
|
27
28
|
"test",
|
|
28
29
|
"archive",
|
|
29
30
|
...agent_file_types_1.AGENT_FILE_TYPES,
|
|
30
31
|
]);
|
|
31
32
|
const DEC_STATUS = new Set(["proposed", "accepted", "rejected", "superseded"]);
|
|
32
|
-
const GOAL_STATE = new Set(["active", "paused", "achieved", "blocked", "budget_limited"]);
|
|
33
|
+
const GOAL_STATE = new Set(["active", "paused", "achieved", "blocked", "budget_limited", "archived"]);
|
|
33
34
|
const SKILL_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
34
35
|
const GOAL_ATTRIBUTE_KEYS = [
|
|
35
36
|
"goal_state",
|
|
@@ -279,13 +280,14 @@ function parseNode(content, filePath, options) {
|
|
|
279
280
|
const statusValue = optionalString(frontmatter, "status", filePath);
|
|
280
281
|
let status = undefined;
|
|
281
282
|
const workStatus = new Set(options.workStatusEnum.map((value) => value.toLowerCase()));
|
|
283
|
+
const allowedWorkStatus = type === "goal" ? new Set([...workStatus, "archived"]) : workStatus;
|
|
282
284
|
if (exports.WORK_TYPES.has(type)) {
|
|
283
285
|
if (!statusValue) {
|
|
284
286
|
throw formatError(filePath, "status is required for work items");
|
|
285
287
|
}
|
|
286
288
|
const normalized = requireLowercase(statusValue, "status", filePath);
|
|
287
|
-
if (!
|
|
288
|
-
throw formatError(filePath, `status must be one of ${Array.from(
|
|
289
|
+
if (!allowedWorkStatus.has(normalized)) {
|
|
290
|
+
throw formatError(filePath, `status must be one of ${Array.from(allowedWorkStatus).join(", ")}`);
|
|
289
291
|
}
|
|
290
292
|
status = normalized;
|
|
291
293
|
}
|
|
@@ -485,6 +485,26 @@ function validateGoalRefs(index, allowMissing, errors) {
|
|
|
485
485
|
}
|
|
486
486
|
}
|
|
487
487
|
}
|
|
488
|
+
function validateSingleActiveRootGoals(index, errors) {
|
|
489
|
+
const activeByWorkspace = {};
|
|
490
|
+
for (const [qid, node] of Object.entries(index.nodes)) {
|
|
491
|
+
if (node.type !== "goal" || node.source?.imported) {
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
if (node.status === "progress" && node.attributes.goal_state === "active") {
|
|
495
|
+
if (!activeByWorkspace[node.ws]) {
|
|
496
|
+
activeByWorkspace[node.ws] = [];
|
|
497
|
+
}
|
|
498
|
+
activeByWorkspace[node.ws].push(qid);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
for (const [workspace, qids] of Object.entries(activeByWorkspace)) {
|
|
502
|
+
if (qids.length <= 1) {
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
pushError(errors, `${workspace}: multiple active root goals found: ${qids.sort().join(", ")}; run mdkg goal activate <goal-id> to select exactly one`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
488
508
|
function detectPrevNextCycles(index, errors) {
|
|
489
509
|
const nodes = index.nodes;
|
|
490
510
|
const seen = new Set();
|
|
@@ -533,6 +553,7 @@ function collectGraphErrors(index, options = {}) {
|
|
|
533
553
|
validateAgentWorkflowFeedbackProposalRefs(index, allowMissing, knownSkillSlugs, externalWorkspaces, errors);
|
|
534
554
|
validateArchiveUriRefs(index, allowMissing, errors);
|
|
535
555
|
validateGoalRefs(index, allowMissing, errors);
|
|
556
|
+
validateSingleActiveRootGoals(index, errors);
|
|
536
557
|
detectPrevNextCycles(index, errors);
|
|
537
558
|
return errors;
|
|
538
559
|
}
|
package/dist/init/AGENT_START.md
CHANGED
|
@@ -24,7 +24,7 @@ Agent operating prompt:
|
|
|
24
24
|
- Use `mdkg show <id>` for direct inspection and `mdkg show <id> --meta` for card-only inspection.
|
|
25
25
|
- Use `mdkg search "..."` and `mdkg next` to discover current work.
|
|
26
26
|
- Use `mdkg new goal "..."` for long-running recursive objectives that need an explicit end condition, active node, required skills, required checks, and completion evidence.
|
|
27
|
-
- Use `mdkg goal
|
|
27
|
+
- Use `mdkg goal activate <goal-id>` to make one local root goal active, then `mdkg goal next` to surface one scoped feature, task, bug, test, or spike at a time; normal `mdkg next` remains for non-goal concrete work.
|
|
28
28
|
- Use `mdkg goal claim [goal-id] <work-id>` only after accepting the surfaced work item; `mdkg goal next` is read-only.
|
|
29
29
|
- Treat goal `required_checks` as report-only guidance from mdkg. Run commands yourself, then record evidence in the goal or active work item.
|
|
30
30
|
- Record skill improvement candidates during normal goal execution; edit `SKILL.md` only when the active node is explicit skill-maintenance work.
|
|
@@ -78,7 +78,7 @@ If the active task is known:
|
|
|
78
78
|
- `mdkg validate`
|
|
79
79
|
|
|
80
80
|
If an active goal is known:
|
|
81
|
-
- `mdkg goal
|
|
81
|
+
- `mdkg goal activate <goal-id>`
|
|
82
82
|
- `mdkg goal current`
|
|
83
83
|
- `mdkg goal next`
|
|
84
84
|
- `mdkg goal claim <work-id>`
|
|
@@ -92,6 +92,7 @@ Validation commands:
|
|
|
92
92
|
Node creation commands:
|
|
93
93
|
- `mdkg new <type> "<title>" [options] [--json]`
|
|
94
94
|
- `mdkg new goal "<title>" [options] [--json]`
|
|
95
|
+
- `mdkg new spike "<research question>" [options] [--json]`
|
|
95
96
|
|
|
96
97
|
Agent workflow file type creation:
|
|
97
98
|
- `mdkg new spec "<title>" [options] [--json]`
|
|
@@ -109,6 +110,9 @@ Agent workflow notes:
|
|
|
109
110
|
- `spec` and `work` scaffold as validation-clean standalone docs.
|
|
110
111
|
- `work_order`, `receipt`, `feedback`, `dispute`, and `proposal` need real refs before strict `mdkg validate` passes.
|
|
111
112
|
- `goal` nodes capture recursive objective state and required checks, but normal `mdkg next` does not select them.
|
|
113
|
+
- `spike` nodes are actionable research/planning work under `.mdkg/work/`; use `mdkg task start|update|done` for lifecycle state.
|
|
114
|
+
- Spikes record sources, findings, recommendations, follow-up node ideas, and skill candidates in Markdown body sections; they do not perform web search, execute research, create follow-up nodes, generate `SKILL.md`, or expose a `mdkg spike ...` namespace automatically.
|
|
115
|
+
- after fresh init, run `mdkg index` before treating `mdkg doctor --strict --json` as a clean health gate; init writes source scaffold files and index writes generated caches.
|
|
112
116
|
|
|
113
117
|
Workspace registry commands:
|
|
114
118
|
- `mdkg workspace ls [--json]`
|
|
@@ -125,6 +129,7 @@ Task mutation commands:
|
|
|
125
129
|
- `mdkg task start <id-or-qid> [--ws <alias>] [--run-id <id>] [--note "<text>"] [--json]`
|
|
126
130
|
- `mdkg task update <id-or-qid> [options] [--json]`
|
|
127
131
|
- `mdkg task done <id-or-qid> [--checkpoint "<title>"] [options] [--json]`
|
|
132
|
+
- task commands support task-like `feat`, `task`, `bug`, `test`, and `spike` nodes
|
|
128
133
|
|
|
129
134
|
Checkpoint commands:
|
|
130
135
|
- `mdkg checkpoint new <title> [--ws <alias>] [--json]`
|
|
@@ -157,7 +162,7 @@ Capability discovery:
|
|
|
157
162
|
- capability records are deterministic cache projections from Markdown
|
|
158
163
|
- records include source hash, headings, refs, and `indexed_at`
|
|
159
164
|
- SPEC and WORK capability records include read-only `linkage` arrays for related SPECs, work contracts, work orders, and receipts when those graph mirrors exist
|
|
160
|
-
- normal task, epic, feat, bug, test, and checkpoint nodes are intentionally excluded
|
|
165
|
+
- normal task, epic, feat, bug, test, spike, and checkpoint nodes are intentionally excluded
|
|
161
166
|
|
|
162
167
|
Spec capability records:
|
|
163
168
|
- `mdkg spec list [--json]`
|
|
@@ -237,14 +242,16 @@ Work semantic mirrors:
|
|
|
237
242
|
Goal nodes:
|
|
238
243
|
- `mdkg goal show <goal-id-or-qid> [--json]`
|
|
239
244
|
- `mdkg goal select <goal-id-or-qid> [--json]`
|
|
245
|
+
- `mdkg goal activate <goal-id-or-qid> [--json]`
|
|
240
246
|
- `mdkg goal current [--json]`
|
|
241
247
|
- `mdkg goal clear [--json]`
|
|
242
248
|
- `mdkg goal next [goal-id-or-qid] [--json]`
|
|
243
249
|
- `mdkg goal claim [goal-id-or-qid] <work-id-or-qid> [--json]`
|
|
244
250
|
- `mdkg goal evaluate <goal-id-or-qid> [--json]`
|
|
245
|
-
- `mdkg goal pause|resume|done <goal-id-or-qid> [--json]`
|
|
251
|
+
- `mdkg goal pause|resume|done|archive <goal-id-or-qid> [--json]`
|
|
246
252
|
- `mdkg goal show <goal-id-or-qid> [--ws <alias>] [--json]`
|
|
247
253
|
- `mdkg goal select <goal-id-or-qid> [--ws <alias>] [--json]`
|
|
254
|
+
- `mdkg goal activate <goal-id-or-qid> [--ws <alias>] [--json]`
|
|
248
255
|
- `mdkg goal current [--ws <alias>] [--json]`
|
|
249
256
|
- `mdkg goal next [goal-id-or-qid] [--ws <alias>] [--json]`
|
|
250
257
|
- `mdkg goal claim <work-id-or-qid> [--ws <alias>] [--json]`
|
|
@@ -253,7 +260,10 @@ Goal nodes:
|
|
|
253
260
|
- `mdkg goal pause <goal-id-or-qid> [--ws <alias>] [--json]`
|
|
254
261
|
- `mdkg goal resume <goal-id-or-qid> [--ws <alias>] [--json]`
|
|
255
262
|
- `mdkg goal done <goal-id-or-qid> [--ws <alias>] [--json]`
|
|
256
|
-
-
|
|
263
|
+
- `mdkg goal archive <goal-id-or-qid> [--ws <alias>] [--json]`
|
|
264
|
+
- goals orchestrate recursive progress through explicit `scope_refs`; tasks, bugs, tests, spikes, and features remain concrete executable units
|
|
265
|
+
- `goal activate` makes one local root goal active, pauses competing local active goals in the same workspace, and writes selected-goal state
|
|
266
|
+
- `goal archive` marks a superseded historical goal archived so it remains readable but not actionable
|
|
257
267
|
- `goal next` is read-only; use `goal claim` to set `active_node`
|
|
258
268
|
- `mdkg goal evaluate` is report-only and never runs commands from `required_checks`
|
|
259
269
|
- skill improvements discovered during normal goal execution should be recorded as candidates or proposals unless the active node is skill-maintenance
|