mdkg 0.1.4 → 0.1.6

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.
@@ -0,0 +1,548 @@
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.runGoalShowCommand = runGoalShowCommand;
7
+ exports.runGoalEvaluateCommand = runGoalEvaluateCommand;
8
+ exports.runGoalNextCommand = runGoalNextCommand;
9
+ exports.runGoalSelectCommand = runGoalSelectCommand;
10
+ exports.runGoalCurrentCommand = runGoalCurrentCommand;
11
+ exports.runGoalClearCommand = runGoalClearCommand;
12
+ exports.runGoalClaimCommand = runGoalClaimCommand;
13
+ exports.runGoalPauseCommand = runGoalPauseCommand;
14
+ exports.runGoalResumeCommand = runGoalResumeCommand;
15
+ exports.runGoalDoneCommand = runGoalDoneCommand;
16
+ const fs_1 = __importDefault(require("fs"));
17
+ const path_1 = __importDefault(require("path"));
18
+ const config_1 = require("../core/config");
19
+ const goal_scope_1 = require("../graph/goal_scope");
20
+ const index_cache_1 = require("../graph/index_cache");
21
+ const frontmatter_1 = require("../graph/frontmatter");
22
+ const indexer_1 = require("../graph/indexer");
23
+ const reindex_1 = require("../graph/reindex");
24
+ const atomic_1 = require("../util/atomic");
25
+ const date_1 = require("../util/date");
26
+ const errors_1 = require("../util/errors");
27
+ const lock_1 = require("../util/lock");
28
+ const qid_1 = require("../util/qid");
29
+ const sort_1 = require("../util/sort");
30
+ const event_support_1 = require("./event_support");
31
+ const node_card_1 = require("./node_card");
32
+ const CONCRETE_GOAL_NEXT_TYPES = new Set(["feat", "task", "bug", "test"]);
33
+ const SELECTED_GOAL_STATE_PATH = path_1.default.join(".mdkg", "state", "selected-goal.json");
34
+ const GOAL_STATE_BY_ACTION = {
35
+ pause: "paused",
36
+ resume: "active",
37
+ done: "achieved",
38
+ };
39
+ const STATUS_BY_ACTION = {
40
+ pause: "blocked",
41
+ resume: "progress",
42
+ done: "done",
43
+ };
44
+ function normalizeWorkspace(value) {
45
+ if (!value) {
46
+ return undefined;
47
+ }
48
+ const normalized = value.toLowerCase();
49
+ if (normalized === "all") {
50
+ throw new errors_1.UsageError("--ws all is not valid here");
51
+ }
52
+ return normalized;
53
+ }
54
+ function toStringList(value) {
55
+ if (Array.isArray(value)) {
56
+ return value.map((item) => String(item));
57
+ }
58
+ return [];
59
+ }
60
+ function selectedGoalPath(root) {
61
+ return path_1.default.join(root, SELECTED_GOAL_STATE_PATH);
62
+ }
63
+ function readSelectedGoalState(root, warnings) {
64
+ const filePath = selectedGoalPath(root);
65
+ if (!fs_1.default.existsSync(filePath)) {
66
+ return undefined;
67
+ }
68
+ try {
69
+ const parsed = JSON.parse(fs_1.default.readFileSync(filePath, "utf8"));
70
+ if (typeof parsed.qid === "string" &&
71
+ typeof parsed.id === "string" &&
72
+ typeof parsed.ws === "string" &&
73
+ typeof parsed.selected_at === "string") {
74
+ return {
75
+ qid: parsed.qid.toLowerCase(),
76
+ id: parsed.id.toLowerCase(),
77
+ ws: parsed.ws.toLowerCase(),
78
+ selected_at: parsed.selected_at,
79
+ };
80
+ }
81
+ warnings.push("selected goal state is malformed; run `mdkg goal select <goal-id>`");
82
+ return undefined;
83
+ }
84
+ catch {
85
+ warnings.push("selected goal state is unreadable; run `mdkg goal select <goal-id>`");
86
+ return undefined;
87
+ }
88
+ }
89
+ function writeSelectedGoalState(root, node, now) {
90
+ const state = {
91
+ qid: node.qid,
92
+ id: node.id,
93
+ ws: node.ws,
94
+ selected_at: now.toISOString(),
95
+ };
96
+ (0, atomic_1.atomicWriteFile)(selectedGoalPath(root), `${JSON.stringify(state, null, 2)}\n`);
97
+ }
98
+ function removeSelectedGoalState(root) {
99
+ const filePath = selectedGoalPath(root);
100
+ if (!fs_1.default.existsSync(filePath)) {
101
+ return false;
102
+ }
103
+ fs_1.default.rmSync(filePath, { force: true });
104
+ return true;
105
+ }
106
+ function optionalString(value) {
107
+ return typeof value === "string" ? value : undefined;
108
+ }
109
+ function resolveExplicitGoal(index, idOrQid, wsHint) {
110
+ const resolved = (0, qid_1.resolveQid)(index, idOrQid, wsHint);
111
+ if (resolved.status !== "ok") {
112
+ throw new errors_1.NotFoundError((0, qid_1.formatResolveError)("goal", idOrQid, resolved, wsHint));
113
+ }
114
+ const node = index.nodes[resolved.qid];
115
+ if (!node) {
116
+ throw new errors_1.NotFoundError(`goal not found: ${idOrQid}`);
117
+ }
118
+ return node;
119
+ }
120
+ function activeGoalCandidates(index, wsHint) {
121
+ return Object.values(index.nodes)
122
+ .filter((node) => node.type === "goal")
123
+ .filter((node) => !node.source?.imported)
124
+ .filter((node) => (wsHint ? node.ws === wsHint : true))
125
+ .filter((node) => node.status === "progress" && node.attributes.goal_state === "active")
126
+ .sort((a, b) => a.qid.localeCompare(b.qid));
127
+ }
128
+ function resolveGoalSelection(root, index, idOrQid, wsHint) {
129
+ const warnings = [];
130
+ if (idOrQid) {
131
+ return {
132
+ node: resolveExplicitGoal(index, idOrQid, wsHint),
133
+ source: "explicit",
134
+ warnings,
135
+ };
136
+ }
137
+ const selected = readSelectedGoalState(root, warnings);
138
+ if (selected) {
139
+ const node = index.nodes[selected.qid];
140
+ if (node && node.type === "goal" && !node.source?.imported) {
141
+ return { node, source: "selected", warnings };
142
+ }
143
+ warnings.push(`selected goal ${selected.qid} is not available; run \`mdkg goal select <goal-id>\``);
144
+ }
145
+ const active = activeGoalCandidates(index, wsHint);
146
+ if (active.length === 1) {
147
+ return { node: active[0], source: "unique_active", warnings };
148
+ }
149
+ 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>\``);
151
+ }
152
+ throw new errors_1.NotFoundError("no selected goal or unique active goal found; run `mdkg goal select <goal-id>`");
153
+ }
154
+ function loadGoal(root, idOrQid, wsHint) {
155
+ const config = (0, config_1.loadConfig)(root);
156
+ const { index } = (0, index_cache_1.loadIndex)({ root, config });
157
+ const ws = normalizeWorkspace(wsHint);
158
+ const selection = resolveGoalSelection(root, index, idOrQid, ws);
159
+ const node = selection.node;
160
+ if (node.source?.imported) {
161
+ throw new errors_1.UsageError(`cannot mutate read-only subgraph node ${node.qid}; update the source workspace for subgraph ${node.source.subgraph_alias}`);
162
+ }
163
+ if (node.type !== "goal") {
164
+ throw new errors_1.UsageError(`mdkg goal requires a goal node; got ${node.type}:${node.id}`);
165
+ }
166
+ const filePath = path_1.default.resolve(root, node.path);
167
+ const content = fs_1.default.readFileSync(filePath, "utf8");
168
+ const parsed = (0, frontmatter_1.parseFrontmatter)(content, filePath);
169
+ return {
170
+ config,
171
+ index,
172
+ node,
173
+ filePath,
174
+ frontmatter: { ...parsed.frontmatter },
175
+ body: parsed.body,
176
+ resolutionSource: selection.source,
177
+ warnings: selection.warnings,
178
+ };
179
+ }
180
+ function goalReceipt(root, loaded) {
181
+ const fm = loaded.frontmatter;
182
+ const rawPriority = fm.priority;
183
+ const priority = rawPriority === undefined ? undefined : Number.parseInt(String(rawPriority), 10);
184
+ return {
185
+ workspace: loaded.node.ws,
186
+ id: loaded.node.id,
187
+ qid: loaded.node.qid,
188
+ path: path_1.default.relative(root, loaded.filePath),
189
+ type: loaded.node.type,
190
+ title: loaded.node.title,
191
+ status: String(fm.status ?? ""),
192
+ ...(Number.isInteger(priority) ? { priority } : {}),
193
+ goal_state: String(fm.goal_state ?? ""),
194
+ goal_condition: String(fm.goal_condition ?? ""),
195
+ scope_refs: toStringList(fm.scope_refs),
196
+ active_node: optionalString(fm.active_node),
197
+ required_skills: toStringList(fm.required_skills),
198
+ required_checks: toStringList(fm.required_checks),
199
+ max_iterations: optionalString(fm.max_iterations),
200
+ blocked_after_attempts: optionalString(fm.blocked_after_attempts),
201
+ };
202
+ }
203
+ 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);
209
+ }
210
+ function maybeReindex(root, config) {
211
+ if (!config.index.auto_reindex) {
212
+ return;
213
+ }
214
+ (0, reindex_1.writeDerivedIndexes)(root, config, (0, indexer_1.buildIndex)(root, config, { tolerant: config.index.tolerant }));
215
+ }
216
+ function ensureStatusAllowed(config, status) {
217
+ const normalized = status.toLowerCase();
218
+ const allowed = new Set(config.work.status_enum.map((value) => value.toLowerCase()));
219
+ if (!allowed.has(normalized)) {
220
+ throw new errors_1.UsageError(`goal status ${normalized} is not allowed by work.status_enum`);
221
+ }
222
+ return normalized;
223
+ }
224
+ function completionEvidenceBody(body) {
225
+ const match = body.match(/(^|\n)# Completion Evidence\s*\n([\s\S]*?)(?=\n# |\s*$)/i);
226
+ return (match?.[2] ?? "").trim();
227
+ }
228
+ function hasCompletionEvidence(body) {
229
+ const evidence = completionEvidenceBody(body);
230
+ if (!evidence) {
231
+ return false;
232
+ }
233
+ return !/^(?:- )?(?:pending|none yet)\.?$/i.test(evidence.trim());
234
+ }
235
+ function isConcreteCandidate(node, statusRanks) {
236
+ if (node.source?.imported) {
237
+ return false;
238
+ }
239
+ if (!CONCRETE_GOAL_NEXT_TYPES.has(node.type) || !goal_scope_1.GOAL_SCOPE_ACTIONABLE_TYPES.has(node.type)) {
240
+ return false;
241
+ }
242
+ if (!node.status || !statusRanks.has(node.status)) {
243
+ return false;
244
+ }
245
+ return node.status !== "done";
246
+ }
247
+ function resolveCandidate(index, idOrQid, ws) {
248
+ const resolved = (0, qid_1.resolveQid)(index, idOrQid, ws);
249
+ if (resolved.status !== "ok") {
250
+ return undefined;
251
+ }
252
+ return index.nodes[resolved.qid];
253
+ }
254
+ function runGoalShowCommand(options) {
255
+ const loaded = loadGoal(options.root, options.id, options.ws);
256
+ const receipt = { action: "showed", goal: goalReceipt(options.root, loaded) };
257
+ if (options.json) {
258
+ console.log(JSON.stringify(receipt, null, 2));
259
+ return;
260
+ }
261
+ console.log(`goal: ${loaded.node.qid}`);
262
+ console.log(`state: ${loaded.frontmatter.goal_state}`);
263
+ console.log(`condition: ${loaded.frontmatter.goal_condition}`);
264
+ if (loaded.frontmatter.active_node) {
265
+ console.log(`active_node: ${loaded.frontmatter.active_node}`);
266
+ }
267
+ const checks = toStringList(loaded.frontmatter.required_checks);
268
+ console.log(`required_checks: ${checks.length === 0 ? "none" : checks.join(", ")}`);
269
+ }
270
+ function runGoalEvaluateCommand(options) {
271
+ const loaded = loadGoal(options.root, options.id, options.ws);
272
+ const checks = toStringList(loaded.frontmatter.required_checks).map((command) => ({
273
+ command,
274
+ status: "report_only",
275
+ }));
276
+ const receipt = {
277
+ action: "evaluated",
278
+ goal: goalReceipt(options.root, loaded),
279
+ report_only: true,
280
+ runs_scripts: false,
281
+ checks,
282
+ completion_evidence_present: hasCompletionEvidence(loaded.body),
283
+ };
284
+ if (options.json) {
285
+ console.log(JSON.stringify(receipt, null, 2));
286
+ return;
287
+ }
288
+ console.log(`goal evaluated: ${loaded.node.qid}`);
289
+ console.log("report_only: true");
290
+ console.log(`runs_scripts: false`);
291
+ console.log(`completion_evidence_present: ${hasCompletionEvidence(loaded.body) ? "yes" : "no"}`);
292
+ if (checks.length > 0) {
293
+ console.log("required_checks:");
294
+ for (const check of checks) {
295
+ console.log(`- ${check.command} (${check.status})`);
296
+ }
297
+ }
298
+ else {
299
+ console.log("required_checks: none");
300
+ }
301
+ }
302
+ function runGoalNextCommand(options) {
303
+ const loaded = loadGoal(options.root, options.id, options.ws);
304
+ const statusPreference = loaded.config.work.next.status_preference.map((status) => status.toLowerCase());
305
+ const statusRanks = new Set(statusPreference);
306
+ const warnings = [...loaded.warnings];
307
+ const activeNode = optionalString(loaded.frontmatter.active_node);
308
+ const scope = (0, goal_scope_1.collectGoalScope)(loaded.index, loaded.node);
309
+ for (const missing of scope.missingRefs) {
310
+ warnings.push(`scope_refs references missing node: ${missing}`);
311
+ }
312
+ for (const invalid of scope.invalidRefs) {
313
+ warnings.push(`scope contains non-actionable or unsupported node: ${invalid}`);
314
+ }
315
+ if (activeNode) {
316
+ const node = resolveCandidate(loaded.index, activeNode, loaded.node.ws);
317
+ if (node && scope.actionableQids.has(node.qid) && isConcreteCandidate(node, statusRanks)) {
318
+ if (options.json) {
319
+ console.log(JSON.stringify({
320
+ action: "selected",
321
+ goal: goalReceipt(options.root, loaded),
322
+ goal_source: loaded.resolutionSource,
323
+ node,
324
+ warnings,
325
+ }, null, 2));
326
+ return;
327
+ }
328
+ console.log((0, node_card_1.formatNodeCard)(node));
329
+ return;
330
+ }
331
+ warnings.push(`active_node is not an actionable local concrete item: ${activeNode}`);
332
+ }
333
+ const candidates = Array.from(scope.actionableQids)
334
+ .map((qid) => loaded.index.nodes[qid])
335
+ .filter((node) => Boolean(node))
336
+ .filter((node) => isConcreteCandidate(node, statusRanks));
337
+ const sorted = (0, sort_1.sortNodesForNext)(candidates, {
338
+ statusPreference,
339
+ priorityMax: loaded.config.work.priority_max,
340
+ });
341
+ const selected = sorted[0];
342
+ if (options.json) {
343
+ console.log(JSON.stringify({
344
+ action: "selected",
345
+ goal: goalReceipt(options.root, loaded),
346
+ goal_source: loaded.resolutionSource,
347
+ node: selected ?? null,
348
+ warnings,
349
+ }, null, 2));
350
+ return;
351
+ }
352
+ for (const warning of warnings) {
353
+ console.error(`warning: ${warning}`);
354
+ }
355
+ if (!selected) {
356
+ console.error("no actionable local work found for goal");
357
+ return;
358
+ }
359
+ console.log((0, node_card_1.formatNodeCard)(selected));
360
+ }
361
+ function runGoalSelectCommand(options) {
362
+ const config = (0, config_1.loadConfig)(options.root);
363
+ return (0, lock_1.withMutationLock)(options.root, config.index.lock_timeout_ms, () => {
364
+ const loaded = loadGoal(options.root, options.id, options.ws);
365
+ const now = options.now ?? new Date();
366
+ writeSelectedGoalState(options.root, loaded.node, now);
367
+ const receipt = {
368
+ action: "selected_goal",
369
+ goal: goalReceipt(options.root, loaded),
370
+ selection: {
371
+ path: SELECTED_GOAL_STATE_PATH,
372
+ selected_at: now.toISOString(),
373
+ },
374
+ };
375
+ if (options.json) {
376
+ console.log(JSON.stringify(receipt, null, 2));
377
+ return;
378
+ }
379
+ console.log(`selected goal: ${loaded.node.qid}`);
380
+ });
381
+ }
382
+ function runGoalCurrentCommand(options) {
383
+ const config = (0, config_1.loadConfig)(options.root);
384
+ const { index } = (0, index_cache_1.loadIndex)({ root: options.root, config });
385
+ const ws = normalizeWorkspace(options.ws);
386
+ const warnings = [];
387
+ let source = "none";
388
+ let node;
389
+ const selected = readSelectedGoalState(options.root, warnings);
390
+ if (selected) {
391
+ const selectedNode = index.nodes[selected.qid];
392
+ if (selectedNode && selectedNode.type === "goal" && !selectedNode.source?.imported) {
393
+ node = selectedNode;
394
+ source = "selected";
395
+ }
396
+ else {
397
+ warnings.push(`selected goal ${selected.qid} is not available; run \`mdkg goal select <goal-id>\``);
398
+ }
399
+ }
400
+ if (!node) {
401
+ const active = activeGoalCandidates(index, ws);
402
+ if (active.length === 1) {
403
+ node = active[0];
404
+ source = "unique_active";
405
+ }
406
+ else if (active.length > 1) {
407
+ source = "ambiguous";
408
+ warnings.push(`multiple active goals found: ${active.map((goal) => goal.qid).join(", ")}`);
409
+ }
410
+ }
411
+ const receipt = {
412
+ action: "current",
413
+ goal: node ? {
414
+ workspace: node.ws,
415
+ id: node.id,
416
+ qid: node.qid,
417
+ path: node.path,
418
+ type: node.type,
419
+ title: node.title,
420
+ status: node.status ?? "",
421
+ ...(node.priority !== undefined ? { priority: node.priority } : {}),
422
+ goal_state: String(node.attributes.goal_state ?? ""),
423
+ goal_condition: String(node.attributes.goal_condition ?? ""),
424
+ scope_refs: toStringList(node.attributes.scope_refs),
425
+ active_node: optionalString(node.attributes.active_node),
426
+ required_skills: toStringList(node.attributes.required_skills),
427
+ required_checks: toStringList(node.attributes.required_checks),
428
+ max_iterations: optionalString(node.attributes.max_iterations),
429
+ blocked_after_attempts: optionalString(node.attributes.blocked_after_attempts),
430
+ } : null,
431
+ source,
432
+ warnings,
433
+ };
434
+ if (options.json) {
435
+ console.log(JSON.stringify(receipt, null, 2));
436
+ return;
437
+ }
438
+ for (const warning of warnings) {
439
+ console.error(`warning: ${warning}`);
440
+ }
441
+ if (!node) {
442
+ console.error("no selected goal");
443
+ return;
444
+ }
445
+ console.log(`current goal: ${node.qid}`);
446
+ }
447
+ function runGoalClearCommand(options) {
448
+ const config = (0, config_1.loadConfig)(options.root);
449
+ return (0, lock_1.withMutationLock)(options.root, config.index.lock_timeout_ms, () => {
450
+ const cleared = removeSelectedGoalState(options.root);
451
+ const receipt = {
452
+ action: "cleared_goal",
453
+ path: SELECTED_GOAL_STATE_PATH,
454
+ cleared,
455
+ };
456
+ if (options.json) {
457
+ console.log(JSON.stringify(receipt, null, 2));
458
+ return;
459
+ }
460
+ console.log(cleared ? "selected goal cleared" : "no selected goal to clear");
461
+ });
462
+ }
463
+ function runGoalClaimCommand(options) {
464
+ const config = (0, config_1.loadConfig)(options.root);
465
+ return (0, lock_1.withMutationLock)(options.root, config.index.lock_timeout_ms, () => {
466
+ const loaded = loadGoal(options.root, options.id, options.ws);
467
+ const resolved = (0, qid_1.resolveQid)(loaded.index, options.workId, loaded.node.ws);
468
+ if (resolved.status !== "ok") {
469
+ throw new errors_1.NotFoundError((0, qid_1.formatResolveError)("work", options.workId, resolved, loaded.node.ws));
470
+ }
471
+ const node = loaded.index.nodes[resolved.qid];
472
+ if (!node) {
473
+ throw new errors_1.NotFoundError(`work not found: ${options.workId}`);
474
+ }
475
+ if (node.source?.imported) {
476
+ throw new errors_1.UsageError(`cannot mutate read-only subgraph node ${node.qid}; update the source workspace for subgraph ${node.source.subgraph_alias}`);
477
+ }
478
+ const statusPreference = loaded.config.work.next.status_preference.map((status) => status.toLowerCase());
479
+ const scope = (0, goal_scope_1.collectGoalScope)(loaded.index, loaded.node);
480
+ if (!scope.actionableQids.has(node.qid)) {
481
+ throw new errors_1.UsageError(`${node.qid} is not inside goal scope for ${loaded.node.qid}`);
482
+ }
483
+ if (!isConcreteCandidate(node, new Set(statusPreference))) {
484
+ throw new errors_1.UsageError(`${node.qid} is not an actionable goal work item`);
485
+ }
486
+ const now = options.now ?? new Date();
487
+ loaded.frontmatter.active_node = node.ws === loaded.node.ws ? node.id : node.qid;
488
+ writeGoalFile(loaded, now);
489
+ maybeReindex(options.root, loaded.config);
490
+ (0, event_support_1.appendAutomaticEvent)({
491
+ root: options.root,
492
+ ws: loaded.node.ws,
493
+ kind: "GOAL_CLAIM",
494
+ status: "ok",
495
+ refs: [loaded.node.id, node.id],
496
+ notes: `goal claim via mdkg goal claim`,
497
+ now,
498
+ });
499
+ const receipt = {
500
+ action: "claimed",
501
+ goal: goalReceipt(options.root, loaded),
502
+ node,
503
+ };
504
+ if (options.json) {
505
+ console.log(JSON.stringify(receipt, null, 2));
506
+ return;
507
+ }
508
+ console.log(`goal claim: ${loaded.node.qid} -> ${node.qid}`);
509
+ });
510
+ }
511
+ function runGoalStateMutationLocked(action, options) {
512
+ const loaded = loadGoal(options.root, options.id, options.ws);
513
+ const now = options.now ?? new Date();
514
+ loaded.frontmatter.goal_state = GOAL_STATE_BY_ACTION[action];
515
+ loaded.frontmatter.status = ensureStatusAllowed(loaded.config, STATUS_BY_ACTION[action]);
516
+ writeGoalFile(loaded, now);
517
+ maybeReindex(options.root, loaded.config);
518
+ (0, event_support_1.appendAutomaticEvent)({
519
+ root: options.root,
520
+ ws: loaded.node.ws,
521
+ kind: `GOAL_${action.toUpperCase()}`,
522
+ status: "ok",
523
+ refs: [loaded.node.id],
524
+ notes: `goal ${action} via mdkg goal ${action}`,
525
+ now,
526
+ });
527
+ const receipt = {
528
+ action,
529
+ goal: goalReceipt(options.root, loaded),
530
+ };
531
+ if (options.json) {
532
+ console.log(JSON.stringify(receipt, null, 2));
533
+ return;
534
+ }
535
+ console.log(`goal ${action}: ${loaded.node.qid}`);
536
+ }
537
+ function runGoalPauseCommand(options) {
538
+ const config = (0, config_1.loadConfig)(options.root);
539
+ return (0, lock_1.withMutationLock)(options.root, config.index.lock_timeout_ms, () => runGoalStateMutationLocked("pause", options));
540
+ }
541
+ function runGoalResumeCommand(options) {
542
+ const config = (0, config_1.loadConfig)(options.root);
543
+ return (0, lock_1.withMutationLock)(options.root, config.index.lock_timeout_ms, () => runGoalStateMutationLocked("resume", options));
544
+ }
545
+ function runGoalDoneCommand(options) {
546
+ const config = (0, config_1.loadConfig)(options.root);
547
+ return (0, lock_1.withMutationLock)(options.root, config.index.lock_timeout_ms, () => runGoalStateMutationLocked("done", options));
548
+ }
@@ -416,6 +416,7 @@ function runInitCommand(options) {
416
416
  ".mdkg/index/*.sqlite-wal",
417
417
  ".mdkg/index/*.sqlite-shm",
418
418
  ".mdkg/index/*.sqlite-journal",
419
+ ".mdkg/state/",
419
420
  ".mdkg/pack/",
420
421
  ".mdkg/archive/**/source/",
421
422
  ])) {
@@ -213,7 +213,9 @@ function runNewCommandLocked(options) {
213
213
  let status;
214
214
  if (node_1.WORK_TYPES.has(type)) {
215
215
  const allowed = new Set(config.work.status_enum.map((value) => value.toLowerCase()));
216
- status = statusInput ?? config.work.status_enum[0]?.toLowerCase();
216
+ status = statusInput ?? (type === "goal" && allowed.has("progress")
217
+ ? "progress"
218
+ : config.work.status_enum[0]?.toLowerCase());
217
219
  if (!status || !allowed.has(status)) {
218
220
  throw new errors_1.UsageError(`--status must be one of ${Array.from(allowed).join(", ")}`);
219
221
  }
@@ -329,6 +331,10 @@ function runNewCommandLocked(options) {
329
331
  skills: skills.length > 0 ? skills : undefined,
330
332
  cases: cases.length > 0 ? cases : undefined,
331
333
  supersedes: options.supersedes ? options.supersedes.toLowerCase() : undefined,
334
+ goal_state: type === "goal" ? (status === "done" ? "achieved" : status === "blocked" ? "blocked" : "active") : undefined,
335
+ goal_condition: type === "goal" ? title : undefined,
336
+ max_iterations: type === "goal" ? 25 : undefined,
337
+ blocked_after_attempts: type === "goal" ? 3 : undefined,
332
338
  created: today,
333
339
  updated: today,
334
340
  });
@@ -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(["task", "bug", "test"]);
25
+ const MUTABLE_TASK_TYPES = new Set(["feat", "task", "bug", "test"]);
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 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, and test 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");
@@ -18,7 +18,7 @@ const skill_mirror_1 = require("./skill_mirror");
18
18
  const DEFAULT_SEED_SUBDIR = path_1.default.resolve(__dirname, "..", "init");
19
19
  const PROTECTED_CORE_DOCS = new Set([".mdkg/core/SOUL.md", ".mdkg/core/HUMAN.md"]);
20
20
  const CREATE_ONLY_PRESERVED = new Set([".mdkg/core/core.md"]);
21
- const ARCHIVE_RAW_IGNORE_ENTRIES = [".mdkg/archive/**/source/"];
21
+ const LOCAL_STATE_IGNORE_ENTRIES = [".mdkg/state/", ".mdkg/archive/**/source/"];
22
22
  function seededInitEvent(nowIso) {
23
23
  const event = {
24
24
  ts: nowIso,
@@ -307,7 +307,7 @@ function ensureArchiveIgnorePolicy(root, dryRun, summary, changes) {
307
307
  const raw = fs_1.default.existsSync(ignorePath) ? fs_1.default.readFileSync(ignorePath, "utf8") : "";
308
308
  const lines = raw.split(/\r?\n/);
309
309
  const existing = new Set(lines.map((line) => line.trim()).filter(Boolean));
310
- const additions = ARCHIVE_RAW_IGNORE_ENTRIES.filter((entry) => !existing.has(entry));
310
+ const additions = LOCAL_STATE_IGNORE_ENTRIES.filter((entry) => !existing.has(entry));
311
311
  if (additions.length === 0) {
312
312
  summary.unchanged += 1;
313
313
  return;
@@ -316,7 +316,7 @@ function ensureArchiveIgnorePolicy(root, dryRun, summary, changes) {
316
316
  path: ".gitignore",
317
317
  category: "ignore_policy",
318
318
  action: fs_1.default.existsSync(ignorePath) ? "update" : "create",
319
- reason: "ignore raw local archive source copies while keeping sidecars and zip caches commit-eligible",
319
+ reason: "ignore local mdkg state and raw archive source copies while keeping authored graph records commit-eligible",
320
320
  });
321
321
  if (!dryRun) {
322
322
  const suffix = raw.length === 0 || raw.endsWith("\n") ? "" : "\n";
@@ -142,6 +142,38 @@ function buildWorkspaceMap(config) {
142
142
  }
143
143
  return workspaces;
144
144
  }
145
+ function addReverseEdge(reverse, edgeKey, target, source) {
146
+ if (!target) {
147
+ return;
148
+ }
149
+ reverse[edgeKey] = reverse[edgeKey] ?? {};
150
+ reverse[edgeKey][target] = reverse[edgeKey][target] ?? [];
151
+ reverse[edgeKey][target].push(source);
152
+ }
153
+ function buildReverseEdges(nodes) {
154
+ const reverse = {};
155
+ for (const [qid, node] of Object.entries(nodes)) {
156
+ addReverseEdge(reverse, "epic", node.edges.epic, qid);
157
+ addReverseEdge(reverse, "parent", node.edges.parent, qid);
158
+ addReverseEdge(reverse, "prev", node.edges.prev, qid);
159
+ addReverseEdge(reverse, "next", node.edges.next, qid);
160
+ for (const target of node.edges.relates) {
161
+ addReverseEdge(reverse, "relates", target, qid);
162
+ }
163
+ for (const target of node.edges.blocked_by) {
164
+ addReverseEdge(reverse, "blocked_by", target, qid);
165
+ }
166
+ for (const target of node.edges.blocks) {
167
+ addReverseEdge(reverse, "blocks", target, qid);
168
+ }
169
+ }
170
+ for (const targets of Object.values(reverse)) {
171
+ for (const sources of Object.values(targets)) {
172
+ sources.sort();
173
+ }
174
+ }
175
+ return reverse;
176
+ }
145
177
  function listDirectories(dirPath) {
146
178
  if (!fs_1.default.existsSync(dirPath)) {
147
179
  return [];
@@ -274,7 +306,7 @@ function runValidateCommand(options) {
274
306
  },
275
307
  workspaces: buildWorkspaceMap(config),
276
308
  nodes,
277
- reverse_edges: {},
309
+ reverse_edges: buildReverseEdges(nodes),
278
310
  };
279
311
  const subgraphProjection = (0, subgraphs_1.buildSubgraphsIndex)(options.root, config);
280
312
  for (const item of subgraphProjection.index.subgraphs) {
@@ -10,6 +10,14 @@ exports.DEFAULT_FRONTMATTER_KEY_ORDER = [
10
10
  "title",
11
11
  "status",
12
12
  "priority",
13
+ "goal_state",
14
+ "goal_condition",
15
+ "scope_refs",
16
+ "active_node",
17
+ "required_skills",
18
+ "required_checks",
19
+ "max_iterations",
20
+ "blocked_after_attempts",
13
21
  "epic",
14
22
  "parent",
15
23
  "prev",