mdkg 0.3.6 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/CLI_COMMAND_MATRIX.md +56 -7
  3. package/README.md +33 -3
  4. package/dist/cli.js +132 -11
  5. package/dist/command-contract.json +431 -16
  6. package/dist/commands/bundle.js +2 -0
  7. package/dist/commands/checkpoint.js +139 -1
  8. package/dist/commands/db.js +8 -0
  9. package/dist/commands/format.js +116 -0
  10. package/dist/commands/goal.js +60 -0
  11. package/dist/commands/graph.js +148 -0
  12. package/dist/commands/handoff.js +295 -0
  13. package/dist/commands/mcp.js +9 -0
  14. package/dist/commands/pack.js +3 -1
  15. package/dist/commands/query_output.js +2 -0
  16. package/dist/commands/show.js +8 -0
  17. package/dist/commands/status.js +1 -0
  18. package/dist/commands/task.js +2 -0
  19. package/dist/commands/upgrade.js +61 -0
  20. package/dist/commands/validate.js +162 -3
  21. package/dist/commands/work.js +164 -0
  22. package/dist/core/project_db_queue_contract.js +101 -0
  23. package/dist/graph/edges.js +15 -0
  24. package/dist/graph/frontmatter.js +4 -1
  25. package/dist/graph/indexer.js +8 -0
  26. package/dist/graph/node.js +12 -1
  27. package/dist/graph/sqlite_index.js +2 -0
  28. package/dist/graph/subgraphs.js +2 -0
  29. package/dist/graph/validate_graph.js +5 -0
  30. package/dist/graph/visibility.js +6 -0
  31. package/dist/init/AGENT_START.md +4 -1
  32. package/dist/init/CLI_COMMAND_MATRIX.md +24 -3
  33. package/dist/init/README.md +17 -2
  34. package/dist/init/init-manifest.json +12 -12
  35. package/dist/init/templates/default/bug.md +2 -0
  36. package/dist/init/templates/default/chk.md +3 -0
  37. package/dist/init/templates/default/epic.md +2 -0
  38. package/dist/init/templates/default/feat.md +2 -0
  39. package/dist/init/templates/default/goal.md +3 -0
  40. package/dist/init/templates/default/spike.md +2 -0
  41. package/dist/init/templates/default/task.md +2 -0
  42. package/dist/init/templates/default/test.md +2 -0
  43. package/dist/pack/export_json.js +20 -8
  44. package/dist/pack/export_md.js +15 -4
  45. package/dist/pack/export_xml.js +9 -4
  46. package/dist/pack/metrics.js +12 -4
  47. package/dist/pack/pack.js +9 -1
  48. package/package.json +6 -2
@@ -3,13 +3,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.RECOMMENDED_HEADINGS = void 0;
6
7
  exports.collectValidateReceipt = collectValidateReceipt;
7
8
  exports.runValidateCommand = runValidateCommand;
8
9
  const fs_1 = __importDefault(require("fs"));
9
10
  const path_1 = __importDefault(require("path"));
11
+ const child_process_1 = require("child_process");
10
12
  const config_1 = require("../core/config");
11
13
  const template_schema_1 = require("../graph/template_schema");
12
14
  const node_1 = require("../graph/node");
15
+ const agent_file_types_1 = require("../graph/agent_file_types");
13
16
  const skills_indexer_1 = require("../graph/skills_indexer");
14
17
  const workspace_files_1 = require("../graph/workspace_files");
15
18
  const validate_graph_1 = require("../graph/validate_graph");
@@ -18,7 +21,7 @@ const visibility_1 = require("../graph/visibility");
18
21
  const sqlite_index_1 = require("../graph/sqlite_index");
19
22
  const errors_1 = require("../util/errors");
20
23
  const skill_mirror_1 = require("./skill_mirror");
21
- const RECOMMENDED_HEADINGS = {
24
+ exports.RECOMMENDED_HEADINGS = {
22
25
  task: [
23
26
  "Overview",
24
27
  "Acceptance Criteria",
@@ -103,6 +106,143 @@ function extractHeadings(body) {
103
106
  }
104
107
  return headings;
105
108
  }
109
+ const RAW_CONTENT_MARKERS = [
110
+ { id: "raw_prompt", pattern: /\bRAW_PROMPT_MARKER\b/i, description: "raw prompt marker" },
111
+ { id: "raw_payload", pattern: /\bRAW_PAYLOAD_MARKER\b/i, description: "raw payload marker" },
112
+ { id: "raw_secret", pattern: /\bRAW_SECRET_MARKER\b|BEGIN [A-Z ]*PRIVATE KEY|secret\s*=/i, description: "raw secret marker" },
113
+ ];
114
+ function shouldCheckRawContentWarnings(node) {
115
+ if ((0, agent_file_types_1.isAgentFileType)(node.type)) {
116
+ return true;
117
+ }
118
+ return node.type === "checkpoint" && typeof node.frontmatter.checkpoint_kind === "string";
119
+ }
120
+ function collectRawContentWarnings(qid, node) {
121
+ if (!shouldCheckRawContentWarnings(node)) {
122
+ return [];
123
+ }
124
+ const warnings = [];
125
+ for (const marker of RAW_CONTENT_MARKERS) {
126
+ if (marker.pattern.test(node.body)) {
127
+ warnings.push(`${qid}: raw-content.${marker.id} warning: ${marker.description} detected; use refs, hashes, summaries, or artifact links instead of raw secrets/prompts/payloads`);
128
+ }
129
+ }
130
+ return warnings;
131
+ }
132
+ function collectChangedPaths(root) {
133
+ const result = (0, child_process_1.spawnSync)("git", ["-C", root, "status", "--porcelain", "--", ".mdkg"], {
134
+ encoding: "utf8",
135
+ });
136
+ if (result.status !== 0) {
137
+ return new Set();
138
+ }
139
+ const changed = new Set();
140
+ for (const line of result.stdout.split(/\r?\n/)) {
141
+ if (!line.trim()) {
142
+ continue;
143
+ }
144
+ const rawPath = line.slice(3).trim();
145
+ const filePath = rawPath.includes(" -> ") ? rawPath.split(" -> ").pop() ?? rawPath : rawPath;
146
+ changed.add(filePath.replace(/\\/g, "/"));
147
+ }
148
+ return changed;
149
+ }
150
+ function qidFromWarning(message) {
151
+ const match = /^([a-z0-9_-]+:[^\s:]+):/.exec(message);
152
+ return match?.[1];
153
+ }
154
+ function warningPath(message, nodes) {
155
+ const qid = qidFromWarning(message);
156
+ if (qid && nodes[qid]) {
157
+ return nodes[qid].path;
158
+ }
159
+ for (const node of Object.values(nodes)) {
160
+ if (message.includes(node.path)) {
161
+ return node.path;
162
+ }
163
+ }
164
+ const match = /([.]mdkg\/[^\s:]+\.md|[.]mdkg\/[^\s:]+\/SKILLS?\.md)/.exec(message);
165
+ return match?.[1];
166
+ }
167
+ function warningDiagnostic(message, nodes) {
168
+ const qid = qidFromWarning(message);
169
+ const pathValue = warningPath(message, nodes);
170
+ const rawMatch = /raw-content\.([a-z_]+)/.exec(message);
171
+ if (rawMatch) {
172
+ return {
173
+ id: `raw-content.${rawMatch[1]}`,
174
+ category: "raw-content",
175
+ severity: "warning",
176
+ message,
177
+ qid,
178
+ path: pathValue,
179
+ ref: qid,
180
+ remediation: "Replace raw secrets, prompts, tokens, or payloads with refs, hashes, redacted summaries, or archive/artifact links.",
181
+ };
182
+ }
183
+ if (message.includes("missing recommended heading")) {
184
+ return {
185
+ id: "heading.missing",
186
+ category: "headings",
187
+ severity: "warning",
188
+ message,
189
+ qid,
190
+ path: pathValue,
191
+ ref: qid,
192
+ remediation: "Run mdkg format --headings --dry-run to review missing heading additions, then --apply if acceptable.",
193
+ };
194
+ }
195
+ if (message.includes("bundled template schema fallback")) {
196
+ return {
197
+ id: "template_schema.fallback",
198
+ category: "templates",
199
+ severity: "warning",
200
+ message,
201
+ path: pathValue,
202
+ remediation: "Run mdkg upgrade --apply to vendor missing built-in template schemas when the managed asset update is safe.",
203
+ };
204
+ }
205
+ if (message.includes("sqlite") || message.includes("index")) {
206
+ return {
207
+ id: "cache.index",
208
+ category: "cache",
209
+ severity: "warning",
210
+ message,
211
+ path: pathValue,
212
+ remediation: "Run mdkg index or mdkg db index rebuild when generated cache state should be refreshed.",
213
+ };
214
+ }
215
+ if (message.includes("skill") || message.includes("mirror")) {
216
+ return {
217
+ id: "skill_mirror.warning",
218
+ category: "skills",
219
+ severity: "warning",
220
+ message,
221
+ path: pathValue,
222
+ remediation: "Run mdkg skill sync after reviewing managed skill mirror drift.",
223
+ };
224
+ }
225
+ if (message.includes("subgraph")) {
226
+ return {
227
+ id: "subgraph.warning",
228
+ category: "subgraph",
229
+ severity: "warning",
230
+ message,
231
+ path: pathValue,
232
+ remediation: "Run mdkg subgraph verify or refresh the source bundle after reviewing child graph freshness.",
233
+ };
234
+ }
235
+ return {
236
+ id: "warning.generic",
237
+ category: "general",
238
+ severity: "warning",
239
+ message,
240
+ qid,
241
+ path: pathValue,
242
+ ref: qid,
243
+ remediation: "Review the warning and apply the focused mdkg command suggested by the message when appropriate.",
244
+ };
245
+ }
106
246
  function isCoreListFile(filePath) {
107
247
  return path_1.default.basename(filePath) === "core.md" && path_1.default.basename(path_1.default.dirname(filePath)) === "core";
108
248
  }
@@ -121,6 +261,8 @@ function normalizeEdges(edges, ws) {
121
261
  relates: edges.relates.map((value) => normalizeEdgeTarget(value, ws)),
122
262
  blocked_by: edges.blocked_by.map((value) => normalizeEdgeTarget(value, ws)),
123
263
  blocks: edges.blocks.map((value) => normalizeEdgeTarget(value, ws)),
264
+ context_refs: (edges.context_refs ?? []).map((value) => normalizeEdgeTarget(value, ws)),
265
+ evidence_refs: (edges.evidence_refs ?? []).map((value) => normalizeEdgeTarget(value, ws)),
124
266
  };
125
267
  }
126
268
  function buildIndexNode(root, ws, filePath, node) {
@@ -178,6 +320,12 @@ function buildReverseEdges(nodes) {
178
320
  for (const target of node.edges.blocks) {
179
321
  addReverseEdge(reverse, "blocks", target, qid);
180
322
  }
323
+ for (const target of node.edges.context_refs ?? []) {
324
+ addReverseEdge(reverse, "context_refs", target, qid);
325
+ }
326
+ for (const target of node.edges.evidence_refs ?? []) {
327
+ addReverseEdge(reverse, "evidence_refs", target, qid);
328
+ }
181
329
  }
182
330
  for (const targets of Object.values(reverse)) {
183
331
  for (const sources of Object.values(targets)) {
@@ -292,7 +440,7 @@ function collectValidateReceipt(options) {
292
440
  idsByWorkspace[alias].set(node.id, filePath);
293
441
  const qid = `${alias}:${node.id}`;
294
442
  nodes[qid] = buildIndexNode(options.root, alias, filePath, node);
295
- const recommended = RECOMMENDED_HEADINGS[node.type];
443
+ const recommended = exports.RECOMMENDED_HEADINGS[node.type];
296
444
  if (recommended) {
297
445
  const headings = extractHeadings(node.body);
298
446
  for (const heading of recommended) {
@@ -301,6 +449,7 @@ function collectValidateReceipt(options) {
301
449
  }
302
450
  }
303
451
  }
452
+ warnings.push(...collectRawContentWarnings(qid, node));
304
453
  }
305
454
  catch (err) {
306
455
  const message = err instanceof Error ? err.message : "unknown error";
@@ -372,7 +521,13 @@ function collectValidateReceipt(options) {
372
521
  }
373
522
  warnings.push(...(0, skill_mirror_1.auditSkillMirrors)(options.root, config));
374
523
  validateEventsJsonl(options.root, config, errors);
375
- const uniqueWarnings = Array.from(new Set(warnings));
524
+ const allUniqueWarnings = Array.from(new Set(warnings));
525
+ const allWarningDiagnostics = allUniqueWarnings.map((warning) => warningDiagnostic(warning, nodes));
526
+ const changedPaths = options.changedOnly ? collectChangedPaths(options.root) : new Set();
527
+ const filteredWarningDiagnostics = options.changedOnly
528
+ ? allWarningDiagnostics.filter((warning) => warning.path !== undefined && changedPaths.has(warning.path))
529
+ : allWarningDiagnostics;
530
+ const uniqueWarnings = filteredWarningDiagnostics.map((warning) => warning.message);
376
531
  const uniqueErrors = Array.from(new Set(errors));
377
532
  const reportLines = [
378
533
  ...uniqueWarnings.map((warning) => `warning: ${warning}`),
@@ -390,8 +545,12 @@ function collectValidateReceipt(options) {
390
545
  warning_count: uniqueWarnings.length,
391
546
  error_count: uniqueErrors.length,
392
547
  warnings: uniqueWarnings,
548
+ warning_diagnostics: filteredWarningDiagnostics,
393
549
  errors: uniqueErrors,
394
550
  ...(outPath ? { report_path: outPath } : {}),
551
+ ...(options.changedOnly
552
+ ? { warning_filter: { mode: "changed-only", changed_paths: Array.from(changedPaths).sort() } }
553
+ : {}),
395
554
  };
396
555
  return receipt;
397
556
  }
@@ -11,6 +11,7 @@ exports.runWorkTriggerCommand = runWorkTriggerCommand;
11
11
  exports.runWorkReceiptNewCommand = runWorkReceiptNewCommand;
12
12
  exports.runWorkReceiptUpdateCommand = runWorkReceiptUpdateCommand;
13
13
  exports.runWorkReceiptVerifyCommand = runWorkReceiptVerifyCommand;
14
+ exports.runWorkValidateCommand = runWorkValidateCommand;
14
15
  exports.runWorkArtifactAddCommand = runWorkArtifactAddCommand;
15
16
  const fs_1 = __importDefault(require("fs"));
16
17
  const path_1 = __importDefault(require("path"));
@@ -34,11 +35,15 @@ const archive_1 = require("./archive");
34
35
  const project_db_1 = require("../core/project_db");
35
36
  const project_db_migrations_1 = require("../core/project_db_migrations");
36
37
  const project_db_queue_1 = require("../core/project_db_queue");
38
+ const validate_1 = require("./validate");
39
+ const query_output_1 = require("./query_output");
40
+ const workspace_files_1 = require("../graph/workspace_files");
37
41
  const PRICING_MODELS = new Set(["free", "included", "quoted", "fixed", "metered", "subscription"]);
38
42
  const ORDER_STATUSES = new Set(["submitted", "accepted", "running", "completed", "cancelled", "failed"]);
39
43
  const RECEIPT_STATUSES = new Set(["recorded", "verified", "rejected", "superseded"]);
40
44
  const OUTCOMES = new Set(["success", "partial", "failure"]);
41
45
  const REDACTION_POLICIES = new Set(["refs_and_hashes_only", "redacted_summary", "external_private"]);
46
+ const WORKFLOW_VALIDATE_TYPES = new Set(agent_file_types_1.AGENT_FILE_TYPES);
42
47
  function parseCsvList(raw) {
43
48
  if (!raw) {
44
49
  return [];
@@ -210,6 +215,162 @@ function resolveReadableWorkNode(index, idOrQid, ws, type, label) {
210
215
  }
211
216
  return node;
212
217
  }
218
+ function normalizeWorkflowValidateType(value) {
219
+ if (value === undefined) {
220
+ return undefined;
221
+ }
222
+ const normalized = value.toLowerCase();
223
+ if (!WORKFLOW_VALIDATE_TYPES.has(normalized)) {
224
+ throw new errors_1.UsageError(`--type must be one of ${agent_file_types_1.AGENT_FILE_TYPES.join(", ")}`);
225
+ }
226
+ return normalized;
227
+ }
228
+ function isWorkflowNode(node) {
229
+ return WORKFLOW_VALIDATE_TYPES.has(node.type);
230
+ }
231
+ function resolveWorkflowTarget(index, idOrQid, ws) {
232
+ const resolved = (0, qid_1.resolveQid)(index, idOrQid, ws);
233
+ if (resolved.status !== "ok") {
234
+ throw new errors_1.NotFoundError((0, qid_1.formatResolveError)("workflow record", idOrQid, resolved, ws));
235
+ }
236
+ const node = index.nodes[resolved.qid];
237
+ if (!node || !isWorkflowNode(node)) {
238
+ throw new errors_1.NotFoundError(`workflow record not found: ${idOrQid}`);
239
+ }
240
+ return node;
241
+ }
242
+ function workflowDiagnosticCode(message) {
243
+ const rawMatch = /raw-content\.([a-z_]+)/.exec(message);
244
+ if (rawMatch) {
245
+ return `raw-content.${rawMatch[1]}`;
246
+ }
247
+ if (message.includes("missing recommended heading")) {
248
+ return "heading.missing";
249
+ }
250
+ if (message.includes("references missing") || message.includes("references missing node")) {
251
+ return "reference.missing";
252
+ }
253
+ if (message.includes("must be named")) {
254
+ return "schema.basename";
255
+ }
256
+ if (message.includes("must be") || message.includes("is required")) {
257
+ return "schema.invalid";
258
+ }
259
+ if (message.includes("visibility:")) {
260
+ return "visibility.policy";
261
+ }
262
+ return "validation.message";
263
+ }
264
+ function workflowDiagnosticQid(message, nodes) {
265
+ for (const node of nodes) {
266
+ if (message.includes(node.qid) || message.includes(node.path)) {
267
+ return node.qid;
268
+ }
269
+ }
270
+ return undefined;
271
+ }
272
+ function workflowCandidatePaths(options) {
273
+ const values = new Set();
274
+ if (options.target) {
275
+ values.add(options.target.path);
276
+ values.add(path_1.default.resolve(options.root, options.target.path));
277
+ return Array.from(values);
278
+ }
279
+ const basenames = options.type
280
+ ? new Set([agent_file_types_1.AGENT_FILE_BASENAMES[options.type]])
281
+ : new Set(Object.values(agent_file_types_1.AGENT_FILE_BASENAMES));
282
+ const filesByAlias = (0, workspace_files_1.listWorkspaceDocFilesByAlias)(options.root, options.config);
283
+ for (const [alias, files] of Object.entries(filesByAlias)) {
284
+ if (options.ws !== "root" && alias !== options.ws) {
285
+ continue;
286
+ }
287
+ for (const filePath of files) {
288
+ if (!basenames.has(path_1.default.basename(filePath))) {
289
+ continue;
290
+ }
291
+ values.add(filePath);
292
+ values.add(path_1.default.relative(options.root, filePath).split(path_1.default.sep).join("/"));
293
+ }
294
+ }
295
+ return Array.from(values);
296
+ }
297
+ function filterWorkflowMessages(messages, nodes, candidatePaths) {
298
+ if (nodes.length === 0 && candidatePaths.length === 0) {
299
+ return [];
300
+ }
301
+ return messages.filter((message) => nodes.some((node) => message.includes(node.qid) || message.includes(node.path)) ||
302
+ candidatePaths.some((filePath) => message.includes(filePath)));
303
+ }
304
+ function buildWorkValidateReceipt(options) {
305
+ const config = (0, config_1.loadConfig)(options.root);
306
+ const ws = normalizeWorkspace(options.ws);
307
+ const { index } = (0, index_cache_1.loadIndex)({ root: options.root, config, tolerant: true });
308
+ const type = normalizeWorkflowValidateType(options.type);
309
+ let targets;
310
+ let target;
311
+ if (options.id) {
312
+ target = resolveWorkflowTarget(index, options.id, ws);
313
+ if (type && target.type !== type) {
314
+ throw new errors_1.UsageError(`workflow record ${target.qid} is ${target.type}, not ${type}`);
315
+ }
316
+ targets = [target];
317
+ }
318
+ else {
319
+ targets = Object.values(index.nodes)
320
+ .filter((node) => isWorkflowNode(node) && (!type || node.type === type))
321
+ .sort((a, b) => a.qid.localeCompare(b.qid));
322
+ }
323
+ const candidatePaths = workflowCandidatePaths({ root: options.root, config, ws, type, target });
324
+ const validation = (0, validate_1.collectValidateReceipt)({ root: options.root });
325
+ const warnings = filterWorkflowMessages(validation.warnings, targets, candidatePaths);
326
+ const errors = filterWorkflowMessages(validation.errors, targets, candidatePaths);
327
+ const diagnostics = [
328
+ ...warnings.map((message) => ({
329
+ severity: "warning",
330
+ code: workflowDiagnosticCode(message),
331
+ message,
332
+ qid: workflowDiagnosticQid(message, targets),
333
+ })),
334
+ ...errors.map((message) => ({
335
+ severity: "error",
336
+ code: workflowDiagnosticCode(message),
337
+ message,
338
+ qid: workflowDiagnosticQid(message, targets),
339
+ })),
340
+ ];
341
+ return {
342
+ action: "work.validate",
343
+ ok: errors.length === 0,
344
+ type: type ?? "all",
345
+ ...(target ? { target: (0, query_output_1.toNodeSummaryJson)(target) } : {}),
346
+ checked_count: target ? 1 : candidatePaths.filter((value) => !path_1.default.isAbsolute(value)).length,
347
+ nodes: targets.map((node) => (0, query_output_1.toNodeSummaryJson)(node)),
348
+ warning_count: warnings.length,
349
+ error_count: errors.length,
350
+ warnings,
351
+ errors,
352
+ diagnostics,
353
+ };
354
+ }
355
+ function printWorkValidateReceipt(receipt, json) {
356
+ if (json) {
357
+ (0, query_output_1.writeJson)(receipt);
358
+ if (!receipt.ok) {
359
+ throw new errors_1.ValidationError(`workflow validation failed with ${receipt.error_count} error(s)`);
360
+ }
361
+ return;
362
+ }
363
+ for (const warning of receipt.warnings) {
364
+ console.error(`warning: ${warning}`);
365
+ }
366
+ if (!receipt.ok) {
367
+ for (const error of receipt.errors) {
368
+ console.error(error);
369
+ }
370
+ throw new errors_1.ValidationError(`workflow validation failed with ${receipt.error_count} error(s)`);
371
+ }
372
+ console.log(`workflow validation ok: ${receipt.checked_count} file(s)`);
373
+ }
213
374
  function resolveTriggerWorkNode(index, ws, refRaw) {
214
375
  const ref = normalizePortableIdRef(refRaw, "<work-or-capability-ref>");
215
376
  const resolved = (0, qid_1.resolveQid)(index, ref, ws);
@@ -943,6 +1104,9 @@ function runWorkReceiptUpdateCommand(options) {
943
1104
  function runWorkReceiptVerifyCommand(options) {
944
1105
  return runWorkReceiptVerifyCommandLocked(options);
945
1106
  }
1107
+ function runWorkValidateCommand(options) {
1108
+ return printWorkValidateReceipt(buildWorkValidateReceipt(options), options.json);
1109
+ }
946
1110
  function runWorkArtifactAddCommand(options) {
947
1111
  return withWorkLock(options.root, () => runWorkArtifactAddCommandLocked(options));
948
1112
  }
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.projectDbQueueAdapterContract = projectDbQueueAdapterContract;
4
+ function projectDbQueueAdapterContract() {
5
+ return {
6
+ schema_version: 1,
7
+ contract_id: "mdkg.project_db.queue.adapter.v1",
8
+ stability: "public",
9
+ boundary: {
10
+ role: "durable local delivery state for mdkg project DB integrations",
11
+ canonical_history: "queue rows are not canonical event history or durable runtime transcripts",
12
+ raw_payload_policy: "store compact refs and redacted payloads; do not store raw secrets, prompts, provider payloads, or bulky runtime artifacts",
13
+ internal_surfaces: ["event", "receipt", "reducer", "writer_lease", "materializer"],
14
+ },
15
+ runtime: {
16
+ database: "node:sqlite",
17
+ transactions: "short BEGIN IMMEDIATE transactions for writes and claims",
18
+ external_dependencies: [],
19
+ },
20
+ commands: [
21
+ "mdkg db queue create <queue>",
22
+ "mdkg db queue pause <queue>",
23
+ "mdkg db queue resume <queue>",
24
+ "mdkg db queue enqueue <queue> <message-id>",
25
+ "mdkg db queue claim <queue>",
26
+ "mdkg db queue ack <queue> <message-id>",
27
+ "mdkg db queue fail <queue> <message-id>",
28
+ "mdkg db queue dead-letter <queue> <message-id>",
29
+ "mdkg db queue release-expired [queue]",
30
+ "mdkg db queue stats [queue]",
31
+ "mdkg db queue list <queue>",
32
+ "mdkg db queue show <queue> <message-id>",
33
+ "mdkg db queue contract",
34
+ ],
35
+ queue_control: {
36
+ table: "project_queue",
37
+ states: ["active", "paused"],
38
+ paused_behavior: {
39
+ rejects: ["enqueue", "claim"],
40
+ allows: ["ack", "fail", "dead-letter", "release-expired", "stats", "list", "show"],
41
+ },
42
+ },
43
+ message: {
44
+ table: "project_queue_message",
45
+ states: ["ready", "leased", "acked", "dead_letter"],
46
+ fields: [
47
+ "queue_name",
48
+ "message_id",
49
+ "dedupe_key",
50
+ "payload_json",
51
+ "payload_hash",
52
+ "status",
53
+ "available_at_ms",
54
+ "attempt_count",
55
+ "max_attempts",
56
+ "lease_owner",
57
+ "lease_deadline_ms",
58
+ "created_at_ms",
59
+ "updated_at_ms",
60
+ "last_error",
61
+ ],
62
+ terminal_states: ["acked", "dead_letter"],
63
+ },
64
+ payload_hash: {
65
+ algorithm: "sha256",
66
+ encoding: "sha256:<64 lowercase hex chars>",
67
+ canonicalization: "payload JSON is serialized deterministically with object keys sorted before hashing and storage",
68
+ },
69
+ dedupe: {
70
+ key: "queue_name + dedupe_key",
71
+ scope: "only non-null dedupe keys participate in the partial unique index",
72
+ duplicate_behavior: "enqueue with an existing dedupe key returns the existing message without replacing payload_json or payload_hash",
73
+ },
74
+ claim: {
75
+ selection: "oldest ready or expired leased message ordered by available_at_ms, created_at_ms, then message_id",
76
+ lease: "claim sets status=leased, lease_owner, and lease_deadline_ms; ack/fail/dead-letter must use the same lease owner",
77
+ transactional: true,
78
+ },
79
+ settlement: {
80
+ ack: "leased message becomes acked and clears lease owner/deadline",
81
+ fail: "leased message increments attempt_count; if attempts remain it becomes ready at now + retry_after_ms, otherwise it becomes dead_letter",
82
+ dead_letter: "leased message becomes dead_letter immediately and records last_error",
83
+ release_expired: "expired leased messages become ready and clear lease owner/deadline without changing attempt_count",
84
+ },
85
+ stats: {
86
+ counters: ["total", "ready", "leased", "acked", "dead_letter", "ready_available", "leased_expired"],
87
+ snapshot_summary: ["total", "ready", "leased", "acked", "dead_letter", "paused_ready", "active_ready"],
88
+ },
89
+ snapshot_policy: {
90
+ drain: "default seal policy; requires no ready or leased delivery work",
91
+ paused: "allows ready messages only when their queues are paused; leased messages are never allowed",
92
+ },
93
+ adapter_guidance: [
94
+ "create queues explicitly before enqueueing integration work",
95
+ "use dedupe keys for idempotent delivery",
96
+ "treat message payloads as refs and redacted envelopes, not canonical runtime state",
97
+ "settle or pause queues before committing sealed project DB state",
98
+ "use stats/list/show for operator review and avoid direct SQL coupling",
99
+ ],
100
+ };
101
+ }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.extractEdges = extractEdges;
4
4
  const id_1 = require("../util/id");
5
+ const refs_1 = require("../util/refs");
5
6
  function formatError(filePath, key, message) {
6
7
  return new Error(`${filePath}: ${key} ${message}`);
7
8
  }
@@ -18,6 +19,12 @@ function normalizeIdRef(value, filePath, key, options) {
18
19
  }
19
20
  return normalized;
20
21
  }
22
+ function normalizeSemanticRef(value, filePath, key) {
23
+ if (!(0, refs_1.validatePortableOrUriRef)(value)) {
24
+ throw formatError(filePath, key, `invalid semantic reference: ${value}`);
25
+ }
26
+ return value.includes("://") ? value : value.toLowerCase();
27
+ }
21
28
  function readString(fm, key) {
22
29
  const value = fm[key];
23
30
  if (value === undefined) {
@@ -46,10 +53,18 @@ function extractEdges(frontmatter, filePath, options = {}) {
46
53
  const relates = readStringList(frontmatter, "relates") ?? [];
47
54
  const blocked_by = readStringList(frontmatter, "blocked_by") ?? [];
48
55
  const blocks = readStringList(frontmatter, "blocks") ?? [];
56
+ const context_refs = options.includeSemanticRefs
57
+ ? readStringList(frontmatter, "context_refs") ?? []
58
+ : [];
59
+ const evidence_refs = options.includeSemanticRefs
60
+ ? readStringList(frontmatter, "evidence_refs") ?? []
61
+ : [];
49
62
  const edges = {
50
63
  relates: relates.map((value) => normalizeIdRef(value, filePath, "relates", options)),
51
64
  blocked_by: blocked_by.map((value) => normalizeIdRef(value, filePath, "blocked_by", options)),
52
65
  blocks: blocks.map((value) => normalizeIdRef(value, filePath, "blocks", options)),
66
+ context_refs: context_refs.map((value) => normalizeSemanticRef(value, filePath, "context_refs")),
67
+ evidence_refs: evidence_refs.map((value) => normalizeSemanticRef(value, filePath, "evidence_refs")),
53
68
  };
54
69
  if (epic) {
55
70
  edges.epic = normalizeIdRef(epic, filePath, "epic", options);
@@ -8,12 +8,14 @@ exports.DEFAULT_FRONTMATTER_KEY_ORDER = [
8
8
  "id",
9
9
  "type",
10
10
  "title",
11
+ "checkpoint_kind",
11
12
  "status",
12
13
  "priority",
13
14
  "goal_state",
14
15
  "goal_condition",
15
16
  "scope_refs",
16
17
  "active_node",
18
+ "last_active_node",
17
19
  "required_skills",
18
20
  "required_checks",
19
21
  "max_iterations",
@@ -60,7 +62,6 @@ exports.DEFAULT_FRONTMATTER_KEY_ORDER = [
60
62
  "severity",
61
63
  "proposal_status",
62
64
  "proposal_kind",
63
- "evidence_refs",
64
65
  "archive_kind",
65
66
  "source_path",
66
67
  "stored_path",
@@ -89,6 +90,8 @@ exports.DEFAULT_FRONTMATTER_KEY_ORDER = [
89
90
  "blocked_by",
90
91
  "blocks",
91
92
  "refs",
93
+ "context_refs",
94
+ "evidence_refs",
92
95
  "aliases",
93
96
  "skills",
94
97
  "cases",
@@ -25,6 +25,8 @@ function normalizeEdges(edges, ws) {
25
25
  relates: edges.relates.map((value) => normalizeEdgeTarget(value, ws)),
26
26
  blocked_by: edges.blocked_by.map((value) => normalizeEdgeTarget(value, ws)),
27
27
  blocks: edges.blocks.map((value) => normalizeEdgeTarget(value, ws)),
28
+ context_refs: (edges.context_refs ?? []).map((value) => normalizeEdgeTarget(value, ws)),
29
+ evidence_refs: (edges.evidence_refs ?? []).map((value) => normalizeEdgeTarget(value, ws)),
28
30
  };
29
31
  }
30
32
  function addReverseEdge(reverse, edgeKey, target, source) {
@@ -118,6 +120,12 @@ function buildIndex(root, config, options = {}) {
118
120
  for (const target of edges.blocks) {
119
121
  addReverseEdge(reverse_edges, "blocks", target, qid);
120
122
  }
123
+ for (const target of edges.context_refs ?? []) {
124
+ addReverseEdge(reverse_edges, "context_refs", target, qid);
125
+ }
126
+ for (const target of edges.evidence_refs ?? []) {
127
+ addReverseEdge(reverse_edges, "evidence_refs", target, qid);
128
+ }
121
129
  }
122
130
  for (const edgeKey of Object.keys(reverse_edges)) {
123
131
  for (const target of Object.keys(reverse_edges[edgeKey])) {
@@ -37,6 +37,7 @@ const GOAL_ATTRIBUTE_KEYS = [
37
37
  "goal_condition",
38
38
  "scope_refs",
39
39
  "active_node",
40
+ "last_active_node",
40
41
  "required_skills",
41
42
  "required_checks",
42
43
  "max_iterations",
@@ -180,6 +181,13 @@ function validateGoalFrontmatter(type, frontmatter, filePath) {
180
181
  throw formatError(filePath, "active_node must be a local id or qid");
181
182
  }
182
183
  }
184
+ const lastActiveNode = optionalString(frontmatter, "last_active_node", filePath);
185
+ if (lastActiveNode !== undefined) {
186
+ const normalized = requireLowercase(lastActiveNode, "last_active_node", filePath);
187
+ if (!(0, id_1.isPortableIdRef)(normalized)) {
188
+ throw formatError(filePath, "last_active_node must be a local id or qid");
189
+ }
190
+ }
183
191
  const scopeRefs = optionalList(frontmatter, "scope_refs", filePath);
184
192
  for (const [index, value] of scopeRefs.entries()) {
185
193
  if (typeof value !== "string" || value.trim().length === 0) {
@@ -334,7 +342,10 @@ function parseNode(content, filePath, options) {
334
342
  throw formatError(filePath, "supersedes must be a dec-# id");
335
343
  }
336
344
  }
337
- const edges = (0, edges_1.extractEdges)(frontmatter, filePath, { allowPortableRefs: true });
345
+ const edges = (0, edges_1.extractEdges)(frontmatter, filePath, {
346
+ allowPortableRefs: true,
347
+ includeSemanticRefs: exports.WORK_TYPES.has(type),
348
+ });
338
349
  const attributes = {
339
350
  ...extractGoalAttributes(type, frontmatter),
340
351
  ...(0, agent_file_types_1.extractAgentAttributes)(type, frontmatter),
@@ -205,6 +205,8 @@ function writeSqliteIndex(options) {
205
205
  ["relates", node.edges.relates],
206
206
  ["blocked_by", node.edges.blocked_by],
207
207
  ["blocks", node.edges.blocks],
208
+ ["context_refs", node.edges.context_refs ?? []],
209
+ ["evidence_refs", node.edges.evidence_refs ?? []],
208
210
  ]) {
209
211
  for (const target of values) {
210
212
  insertEdge.run(node.qid, kind, target);
@@ -278,6 +278,8 @@ function projectOneSubgraph(root, alias, subgraph) {
278
278
  relates: remapTargets(node.edges.relates, qidMap),
279
279
  blocked_by: remapTargets(node.edges.blocked_by, qidMap),
280
280
  blocks: remapTargets(node.edges.blocks, qidMap),
281
+ context_refs: remapTargets(node.edges.context_refs ?? [], qidMap),
282
+ evidence_refs: remapTargets(node.edges.evidence_refs ?? [], qidMap),
281
283
  },
282
284
  source: {
283
285
  imported: true,