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.
@@ -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
- for (const [indexValue, value] of raw.entries()) {
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;
@@ -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
- warnings.push(`selected goal ${selected.qid} is not available; run \`mdkg goal select <goal-id>\``);
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 select <goal-id>\``);
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 select <goal-id>`");
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.updated = (0, date_1.formatDate)(now);
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
- warnings.push(`selected goal ${selected.qid} is not available; run \`mdkg goal select <goal-id>\``);
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
+ }
@@ -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 || !allowed.has(status)) {
235
- throw new errors_1.UsageError(`--status must be one of ${Array.from(allowed).join(", ")}`);
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,
@@ -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 test "..."`';
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;
@@ -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 test nodes; use markdown editing for ${node.type}:${node.id}`);
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",
@@ -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,
@@ -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 (!workStatus.has(normalized)) {
288
- throw formatError(filePath, `status must be one of ${Array.from(workStatus).join(", ")}`);
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
  }
@@ -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 select <goal-id>` when a goal is active, then `mdkg goal next` to surface one scoped feature, task, bug, or test at a time; normal `mdkg next` remains for non-goal concrete work.
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 select <goal-id>`
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
- - goals orchestrate recursive progress through explicit `scope_refs`; tasks, bugs, tests, and features remain concrete executable units
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