mdkg 0.1.10 → 0.3.0

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 (37) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +40 -15
  3. package/dist/cli.js +293 -13
  4. package/dist/commands/capability.js +13 -8
  5. package/dist/commands/db.js +185 -1
  6. package/dist/commands/format.js +1 -1
  7. package/dist/commands/spec.js +101 -0
  8. package/dist/commands/work.js +569 -20
  9. package/dist/core/project_db_migrations.js +24 -0
  10. package/dist/core/project_db_queue.js +186 -0
  11. package/dist/core/project_db_snapshot.js +28 -3
  12. package/dist/graph/agent_file_types.js +95 -7
  13. package/dist/graph/capabilities_indexer.js +89 -2
  14. package/dist/graph/frontmatter.js +6 -0
  15. package/dist/graph/node.js +8 -2
  16. package/dist/init/AGENT_START.md +15 -9
  17. package/dist/init/CLI_COMMAND_MATRIX.md +33 -5
  18. package/dist/init/README.md +36 -11
  19. package/dist/init/init-manifest.json +64 -9
  20. package/dist/init/skills/default/verify-close-and-checkpoint/SKILL.md +8 -7
  21. package/dist/init/templates/default/receipt.md +12 -1
  22. package/dist/init/templates/default/spec.md +8 -6
  23. package/dist/init/templates/default/work.md +5 -1
  24. package/dist/init/templates/default/work_order.md +11 -0
  25. package/dist/init/templates/skills/base.SKILL.md +66 -0
  26. package/dist/init/templates/specs/agent.SPEC.md +80 -0
  27. package/dist/init/templates/specs/api.SPEC.md +33 -0
  28. package/dist/init/templates/specs/base.SPEC.md +120 -0
  29. package/dist/init/templates/specs/capability.SPEC.md +45 -0
  30. package/dist/init/templates/specs/integration.SPEC.md +25 -0
  31. package/dist/init/templates/specs/model.SPEC.md +21 -0
  32. package/dist/init/templates/specs/project.SPEC.md +39 -0
  33. package/dist/init/templates/specs/runtime-agent.SPEC.md +49 -0
  34. package/dist/init/templates/specs/runtime-image.SPEC.md +21 -0
  35. package/dist/init/templates/specs/tool.SPEC.md +25 -0
  36. package/dist/util/argparse.js +8 -0
  37. package/package.json +5 -2
@@ -53,6 +53,24 @@ ON project_queue_message(queue_name, status, available_at_ms, created_at_ms, mes
53
53
  CREATE INDEX IF NOT EXISTS project_queue_message_lease_idx
54
54
  ON project_queue_message(queue_name, status, lease_deadline_ms, created_at_ms, message_id);
55
55
  `;
56
+ const QUEUE_CONTROL_MIGRATION_SQL = `
57
+ CREATE TABLE IF NOT EXISTS project_queue (
58
+ queue_name TEXT PRIMARY KEY,
59
+ status TEXT NOT NULL CHECK(status IN ('active', 'paused')),
60
+ paused_reason TEXT,
61
+ created_at_ms INTEGER NOT NULL,
62
+ updated_at_ms INTEGER NOT NULL
63
+ ) STRICT;
64
+
65
+ INSERT OR IGNORE INTO project_queue (queue_name, status, paused_reason, created_at_ms, updated_at_ms)
66
+ SELECT DISTINCT
67
+ queue_name,
68
+ 'active',
69
+ NULL,
70
+ CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER),
71
+ CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)
72
+ FROM project_queue_message;
73
+ `;
56
74
  const EVENTS_RECEIPTS_MIGRATION_SQL = `
57
75
  CREATE TABLE IF NOT EXISTS project_event (
58
76
  event_id TEXT PRIMARY KEY,
@@ -162,6 +180,12 @@ const BUILTIN_MIGRATIONS = [
162
180
  filename: "004_mdkg_project_db_writer_leases.sql",
163
181
  sql: WRITER_LEASES_MIGRATION_SQL.trim(),
164
182
  },
183
+ {
184
+ ordinal: 5,
185
+ key: "mdkg.project_db.queue_control.v1",
186
+ filename: "005_mdkg_project_db_queue_control.sql",
187
+ sql: QUEUE_CONTROL_MIGRATION_SQL.trim(),
188
+ },
165
189
  ];
166
190
  function loadDatabaseCtor() {
167
191
  try {
@@ -10,6 +10,14 @@ exports.failProjectQueueMessage = failProjectQueueMessage;
10
10
  exports.deadLetterProjectQueueMessage = deadLetterProjectQueueMessage;
11
11
  exports.releaseExpiredProjectQueueLeases = releaseExpiredProjectQueueLeases;
12
12
  exports.readProjectQueueStats = readProjectQueueStats;
13
+ exports.createProjectQueue = createProjectQueue;
14
+ exports.pauseProjectQueue = pauseProjectQueue;
15
+ exports.resumeProjectQueue = resumeProjectQueue;
16
+ exports.readProjectQueue = readProjectQueue;
17
+ exports.listProjectQueues = listProjectQueues;
18
+ exports.readProjectQueueMessage = readProjectQueueMessage;
19
+ exports.listProjectQueueMessages = listProjectQueueMessages;
20
+ exports.readProjectQueueSnapshotSummary = readProjectQueueSnapshotSummary;
13
21
  const crypto_1 = __importDefault(require("crypto"));
14
22
  function loadDatabaseCtor() {
15
23
  try {
@@ -91,12 +99,44 @@ function toMessage(row) {
91
99
  last_error: row.last_error === null || row.last_error === undefined ? null : String(row.last_error),
92
100
  };
93
101
  }
102
+ function toQueue(row) {
103
+ return {
104
+ queue_name: String(row.queue_name),
105
+ status: String(row.status),
106
+ paused_reason: row.paused_reason === null || row.paused_reason === undefined ? null : String(row.paused_reason),
107
+ created_at_ms: Number(row.created_at_ms),
108
+ updated_at_ms: Number(row.updated_at_ms),
109
+ };
110
+ }
94
111
  function getMessage(db, queueName, messageId) {
95
112
  const row = db
96
113
  .prepare("SELECT * FROM project_queue_message WHERE queue_name = ? AND message_id = ?")
97
114
  .get(queueName, messageId);
98
115
  return row ? toMessage(row) : null;
99
116
  }
117
+ function getQueue(db, queueName) {
118
+ const row = db.prepare("SELECT * FROM project_queue WHERE queue_name = ?").get(queueName);
119
+ return row ? toQueue(row) : null;
120
+ }
121
+ function ensureQueue(db, queueName, currentNow) {
122
+ const existing = getQueue(db, queueName);
123
+ if (existing) {
124
+ return existing;
125
+ }
126
+ db
127
+ .prepare("INSERT INTO project_queue (queue_name, status, paused_reason, created_at_ms, updated_at_ms) VALUES (?, 'active', NULL, ?, ?)")
128
+ .run(queueName, currentNow, currentNow);
129
+ const queue = getQueue(db, queueName);
130
+ if (!queue) {
131
+ throw new Error(`queue could not be loaded after create: ${queueName}`);
132
+ }
133
+ return queue;
134
+ }
135
+ function requireQueueActive(queue, action) {
136
+ if (queue.status === "paused") {
137
+ throw new Error(`queue ${queue.queue_name} is paused; cannot ${action}`);
138
+ }
139
+ }
100
140
  function withDb(databasePath, fn) {
101
141
  const DatabaseSync = loadDatabaseCtor();
102
142
  const db = new DatabaseSync(databasePath);
@@ -139,6 +179,7 @@ function enqueueProjectQueueMessage(databasePath, input) {
139
179
  const payloadJson = stableJson(input.payload);
140
180
  const hash = payloadHash(payloadJson);
141
181
  return withDb(databasePath, (db) => withImmediateTransaction(db, () => {
182
+ requireQueueActive(ensureQueue(db, input.queue_name, currentNow), "enqueue");
142
183
  if (input.dedupe_key) {
143
184
  const duplicate = db
144
185
  .prepare("SELECT * FROM project_queue_message WHERE queue_name = ? AND dedupe_key = ?")
@@ -167,6 +208,7 @@ function claimProjectQueueMessage(databasePath, input) {
167
208
  assertPositiveInteger(input.lease_ms, "lease_ms");
168
209
  const currentNow = nowMs(input.now_ms);
169
210
  return withDb(databasePath, (db) => withImmediateTransaction(db, () => {
211
+ requireQueueActive(ensureQueue(db, input.queue_name, currentNow), "claim");
170
212
  const row = db
171
213
  .prepare([
172
214
  "SELECT * FROM project_queue_message",
@@ -315,3 +357,147 @@ function readProjectQueueStats(databasePath, input = {}) {
315
357
  };
316
358
  });
317
359
  }
360
+ function createProjectQueue(databasePath, input) {
361
+ assertNonEmpty(input.queue_name, "queue_name");
362
+ const currentNow = nowMs(input.now_ms);
363
+ if (input.reason !== undefined && input.reason !== null) {
364
+ assertNonEmpty(input.reason, "reason");
365
+ }
366
+ return withDb(databasePath, (db) => withImmediateTransaction(db, () => {
367
+ const existing = getQueue(db, input.queue_name);
368
+ if (existing) {
369
+ return { created: false, queue: existing };
370
+ }
371
+ const status = input.paused ? "paused" : "active";
372
+ db
373
+ .prepare("INSERT INTO project_queue (queue_name, status, paused_reason, created_at_ms, updated_at_ms) VALUES (?, ?, ?, ?, ?)")
374
+ .run(input.queue_name, status, input.paused ? input.reason ?? null : null, currentNow, currentNow);
375
+ const queue = getQueue(db, input.queue_name);
376
+ if (!queue) {
377
+ throw new Error(`queue could not be loaded after create: ${input.queue_name}`);
378
+ }
379
+ return { created: true, queue };
380
+ }));
381
+ }
382
+ function pauseProjectQueue(databasePath, input) {
383
+ assertNonEmpty(input.queue_name, "queue_name");
384
+ if (input.reason !== undefined && input.reason !== null) {
385
+ assertNonEmpty(input.reason, "reason");
386
+ }
387
+ const currentNow = nowMs(input.now_ms);
388
+ return withDb(databasePath, (db) => withImmediateTransaction(db, () => {
389
+ ensureQueue(db, input.queue_name, currentNow);
390
+ db
391
+ .prepare("UPDATE project_queue SET status = 'paused', paused_reason = ?, updated_at_ms = ? WHERE queue_name = ?")
392
+ .run(input.reason ?? null, currentNow, input.queue_name);
393
+ const queue = getQueue(db, input.queue_name);
394
+ if (!queue) {
395
+ throw new Error(`queue could not be loaded after pause: ${input.queue_name}`);
396
+ }
397
+ return queue;
398
+ }));
399
+ }
400
+ function resumeProjectQueue(databasePath, input) {
401
+ assertNonEmpty(input.queue_name, "queue_name");
402
+ const currentNow = nowMs(input.now_ms);
403
+ return withDb(databasePath, (db) => withImmediateTransaction(db, () => {
404
+ ensureQueue(db, input.queue_name, currentNow);
405
+ db
406
+ .prepare("UPDATE project_queue SET status = 'active', paused_reason = NULL, updated_at_ms = ? WHERE queue_name = ?")
407
+ .run(currentNow, input.queue_name);
408
+ const queue = getQueue(db, input.queue_name);
409
+ if (!queue) {
410
+ throw new Error(`queue could not be loaded after resume: ${input.queue_name}`);
411
+ }
412
+ return queue;
413
+ }));
414
+ }
415
+ function readProjectQueue(databasePath, queueName) {
416
+ assertNonEmpty(queueName, "queue_name");
417
+ return withDb(databasePath, (db) => getQueue(db, queueName));
418
+ }
419
+ function listProjectQueues(databasePath) {
420
+ return withDb(databasePath, (db) => db.prepare("SELECT * FROM project_queue ORDER BY queue_name ASC").all().map(toQueue));
421
+ }
422
+ function readProjectQueueMessage(databasePath, queueName, messageId) {
423
+ assertNonEmpty(queueName, "queue_name");
424
+ assertNonEmpty(messageId, "message_id");
425
+ return withDb(databasePath, (db) => getMessage(db, queueName, messageId));
426
+ }
427
+ function listProjectQueueMessages(databasePath, input) {
428
+ assertNonEmpty(input.queue_name, "queue_name");
429
+ const limit = input.limit ?? 50;
430
+ assertPositiveInteger(limit, "limit");
431
+ if (input.status && input.status !== "all" && !["ready", "leased", "acked", "dead_letter"].includes(input.status)) {
432
+ throw new Error("status must be ready, leased, acked, dead_letter, or all");
433
+ }
434
+ return withDb(databasePath, (db) => {
435
+ const sql = input.status && input.status !== "all"
436
+ ? [
437
+ "SELECT * FROM project_queue_message",
438
+ "WHERE queue_name = ? AND status = ?",
439
+ "ORDER BY available_at_ms ASC, created_at_ms ASC, message_id ASC",
440
+ "LIMIT ?",
441
+ ].join(" ")
442
+ : [
443
+ "SELECT * FROM project_queue_message",
444
+ "WHERE queue_name = ?",
445
+ "ORDER BY available_at_ms ASC, created_at_ms ASC, message_id ASC",
446
+ "LIMIT ?",
447
+ ].join(" ");
448
+ const params = input.status && input.status !== "all" ? [input.queue_name, input.status, limit] : [input.queue_name, limit];
449
+ return db.prepare(sql).all(...params).map(toMessage);
450
+ });
451
+ }
452
+ function readProjectQueueSnapshotSummary(databasePath) {
453
+ return withDb(databasePath, (db) => {
454
+ const rows = db
455
+ .prepare([
456
+ "SELECT q.queue_name, q.status, q.paused_reason,",
457
+ "COUNT(m.message_id) AS total,",
458
+ "SUM(CASE WHEN m.status = 'ready' THEN 1 ELSE 0 END) AS ready,",
459
+ "SUM(CASE WHEN m.status = 'leased' THEN 1 ELSE 0 END) AS leased,",
460
+ "SUM(CASE WHEN m.status = 'acked' THEN 1 ELSE 0 END) AS acked,",
461
+ "SUM(CASE WHEN m.status = 'dead_letter' THEN 1 ELSE 0 END) AS dead_letter",
462
+ "FROM project_queue q",
463
+ "LEFT JOIN project_queue_message m ON m.queue_name = q.queue_name",
464
+ "GROUP BY q.queue_name, q.status, q.paused_reason",
465
+ "ORDER BY q.queue_name ASC",
466
+ ].join(" "))
467
+ .all();
468
+ const queues = rows.map((row) => ({
469
+ queue_name: String(row.queue_name),
470
+ status: String(row.status),
471
+ paused_reason: row.paused_reason === null || row.paused_reason === undefined ? null : String(row.paused_reason),
472
+ total: Number(row.total ?? 0),
473
+ ready: Number(row.ready ?? 0),
474
+ leased: Number(row.leased ?? 0),
475
+ acked: Number(row.acked ?? 0),
476
+ dead_letter: Number(row.dead_letter ?? 0),
477
+ }));
478
+ const summary = {
479
+ queues,
480
+ total: 0,
481
+ ready: 0,
482
+ leased: 0,
483
+ acked: 0,
484
+ dead_letter: 0,
485
+ paused_ready: 0,
486
+ active_ready: 0,
487
+ };
488
+ for (const queue of queues) {
489
+ summary.total += queue.total;
490
+ summary.ready += queue.ready;
491
+ summary.leased += queue.leased;
492
+ summary.acked += queue.acked;
493
+ summary.dead_letter += queue.dead_letter;
494
+ if (queue.status === "paused") {
495
+ summary.paused_ready += queue.ready;
496
+ }
497
+ else {
498
+ summary.active_ready += queue.ready;
499
+ }
500
+ }
501
+ return summary;
502
+ });
503
+ }
@@ -18,6 +18,7 @@ const version_1 = require("./version");
18
18
  const atomic_1 = require("../util/atomic");
19
19
  const errors_1 = require("../util/errors");
20
20
  const project_db_migrations_1 = require("./project_db_migrations");
21
+ const project_db_queue_1 = require("./project_db_queue");
21
22
  function loadDatabaseCtor() {
22
23
  try {
23
24
  const loaded = require("node:sqlite");
@@ -170,7 +171,7 @@ function readManifest(filePath) {
170
171
  }
171
172
  return manifest;
172
173
  }
173
- function buildManifest(root, config, snapshotFile, runtimeHash) {
174
+ function buildManifest(root, config, snapshotFile, runtimeHash, queuePolicy, queueSummary) {
174
175
  const layout = (0, project_db_1.resolveConfiguredProjectDbLayout)(root, config.db);
175
176
  const metadata = collectSnapshotMetadata(snapshotFile, config.db.migration_table);
176
177
  return {
@@ -188,17 +189,39 @@ function buildManifest(root, config, snapshotFile, runtimeHash) {
188
189
  byte_size: fs_1.default.statSync(snapshotFile).size,
189
190
  table_counts: metadata.table_counts,
190
191
  migrations: metadata.migrations,
192
+ queue_policy: queuePolicy,
193
+ queue_summary: queueSummary,
191
194
  };
192
195
  }
196
+ function assertQueueSnapshotPolicy(policy, summary) {
197
+ if (policy === "drain") {
198
+ if (summary.ready > 0 || summary.leased > 0) {
199
+ throw new errors_1.ValidationError(`db snapshot seal requires drained queues; found ready=${summary.ready}, leased=${summary.leased}`);
200
+ }
201
+ return;
202
+ }
203
+ if (policy === "paused") {
204
+ if (summary.leased > 0) {
205
+ throw new errors_1.ValidationError(`db snapshot seal --queue-policy paused requires no leased messages; found leased=${summary.leased}`);
206
+ }
207
+ if (summary.active_ready > 0) {
208
+ throw new errors_1.ValidationError(`db snapshot seal --queue-policy paused requires ready messages to be in paused queues; found active_ready=${summary.active_ready}`);
209
+ }
210
+ return;
211
+ }
212
+ throw new errors_1.ValidationError(`unsupported queue snapshot policy: ${policy}`);
213
+ }
193
214
  function warningListFromVerify(root, config) {
194
215
  return (0, project_db_migrations_1.verifyProjectDb)(root, config).warnings;
195
216
  }
196
- function sealProjectDbSnapshot(root, config) {
217
+ function sealProjectDbSnapshot(root, config, queuePolicy = "drain") {
197
218
  const verification = (0, project_db_migrations_1.verifyProjectDb)(root, config);
198
219
  if (!verification.ok) {
199
220
  throw new errors_1.ValidationError(`db snapshot seal requires a valid project DB; run mdkg db verify`);
200
221
  }
201
222
  const layout = (0, project_db_1.resolveConfiguredProjectDbLayout)(root, config.db);
223
+ const queueSummary = (0, project_db_queue_1.readProjectQueueSnapshotSummary)(layout.runtimeFile);
224
+ assertQueueSnapshotPolicy(queuePolicy, queueSummary);
202
225
  const oldHash = fs_1.default.existsSync(layout.stateFile) ? sha256File(layout.stateFile) : null;
203
226
  fs_1.default.mkdirSync(layout.stateDir, { recursive: true });
204
227
  const tempSnapshot = path_1.default.join(layout.stateDir, `.project.sqlite.${process.pid}-${Date.now()}.tmp`);
@@ -228,7 +251,7 @@ function sealProjectDbSnapshot(root, config) {
228
251
  }
229
252
  try {
230
253
  const runtimeHash = sha256File(layout.runtimeFile);
231
- const manifest = buildManifest(root, config, tempSnapshot, runtimeHash);
254
+ const manifest = buildManifest(root, config, tempSnapshot, runtimeHash, queuePolicy, queueSummary);
232
255
  (0, atomic_1.atomicWriteFile)(tempManifest, `${JSON.stringify(manifest, null, 2)}\n`);
233
256
  fs_1.default.renameSync(tempSnapshot, layout.stateFile);
234
257
  fs_1.default.renameSync(tempManifest, layout.stateManifest);
@@ -243,6 +266,8 @@ function sealProjectDbSnapshot(root, config) {
243
266
  byte_size: manifest.byte_size,
244
267
  table_counts: manifest.table_counts,
245
268
  migrations: manifest.migrations,
269
+ queue_policy: queuePolicy,
270
+ queue_summary: queueSummary,
246
271
  warnings: warningListFromVerify(root, config),
247
272
  };
248
273
  }
@@ -32,6 +32,7 @@ exports.AGENT_FILE_BASENAMES = {
32
32
  exports.AGENT_ATTRIBUTE_KEY_ORDER = {
33
33
  spec: [
34
34
  "version",
35
+ "spec_kind",
35
36
  "role",
36
37
  "runtime_mode",
37
38
  "work_contracts",
@@ -68,7 +69,10 @@ exports.AGENT_ATTRIBUTE_KEY_ORDER = {
68
69
  "requester",
69
70
  "order_status",
70
71
  "request_ref",
72
+ "trigger_ref",
73
+ "payload_hash",
71
74
  "input_refs",
75
+ "queue_refs",
72
76
  "requested_outputs",
73
77
  "constraint_refs",
74
78
  "artifact_policy",
@@ -79,8 +83,10 @@ exports.AGENT_ATTRIBUTE_KEY_ORDER = {
79
83
  "receipt_status",
80
84
  "outcome",
81
85
  "cost_ref",
86
+ "redaction_policy",
82
87
  "proof_refs",
83
88
  "attestation_refs",
89
+ "evidence_hashes",
84
90
  "input_hashes",
85
91
  "output_hashes",
86
92
  ],
@@ -128,6 +134,27 @@ const UPDATE_POLICY_VALUES = new Set([
128
134
  "automatic",
129
135
  "disabled",
130
136
  ]);
137
+ const SPEC_KIND_VALUES = new Set([
138
+ "cli_tool",
139
+ "api",
140
+ "agent",
141
+ "runtime_agent",
142
+ "capability",
143
+ "tool",
144
+ "model",
145
+ "runtime_image",
146
+ "integration",
147
+ "project_service",
148
+ ]);
149
+ const DOCUMENTATION_ONLY_SPEC_KIND_ROUTES = {
150
+ gap_register: "use an EDD, task, checkpoint, or goal for gap registers",
151
+ checkpoint: "use a checkpoint node for checkpoint evidence",
152
+ roadmap: "use an epic, goal, EDD, or PRD for roadmaps",
153
+ audit: "use a task, checkpoint, or EDD for audits",
154
+ go_no_go: "use a DEC, task, or checkpoint for go/no-go notes",
155
+ planning_note: "use an EDD, task, or checkpoint for planning notes",
156
+ launch_checklist: "use a task, test, checkpoint, or DEC for launch checklists",
157
+ };
131
158
  const PRICING_MODEL_VALUES = new Set([
132
159
  "free",
133
160
  "included",
@@ -146,6 +173,11 @@ const ORDER_STATUS_VALUES = new Set([
146
173
  ]);
147
174
  const RECEIPT_STATUS_VALUES = new Set(["recorded", "verified", "rejected", "superseded"]);
148
175
  const OUTCOME_VALUES = new Set(["success", "partial", "failure"]);
176
+ const REDACTION_POLICY_VALUES = new Set([
177
+ "refs_and_hashes_only",
178
+ "redacted_summary",
179
+ "external_private",
180
+ ]);
149
181
  const ARTIFACT_POLICY_VALUES = new Set([
150
182
  "commit_sidecar_and_zip",
151
183
  "external_only",
@@ -265,6 +297,16 @@ function requireEnum(value, key, allowed, filePath) {
265
297
  throw formatError(filePath, `${key} must be one of ${Array.from(allowed).join(", ")}`);
266
298
  }
267
299
  }
300
+ function validateSpecKind(value, filePath) {
301
+ if (SPEC_KIND_VALUES.has(value)) {
302
+ return;
303
+ }
304
+ const route = DOCUMENTATION_ONLY_SPEC_KIND_ROUTES[value];
305
+ if (route) {
306
+ throw formatError(filePath, `spec_kind ${value} is documentation-only; ${route}. SPEC.md must define a reusable invocable capability surface.`);
307
+ }
308
+ throw formatError(filePath, `spec_kind must be one of ${Array.from(SPEC_KIND_VALUES).join(", ")}; documentation-only records belong in normal mdkg nodes such as task, test, epic, goal, checkpoint, EDD, PRD, DEC, bug, feedback, dispute, or proposal.`);
309
+ }
268
310
  function requireLowerToken(value, key, filePath) {
269
311
  if (!LOWER_TOKEN_RE.test(value)) {
270
312
  throw formatError(filePath, `${key} must be lowercase snake/kebab style`);
@@ -290,6 +332,14 @@ function validatePortableRefs(values, key, filePath) {
290
332
  }
291
333
  }
292
334
  }
335
+ function validateWorkInvocationAnchor(requiredCapabilities, refsByKey, filePath) {
336
+ const hasRequiredCapability = requiredCapabilities.length > 0;
337
+ const hasDependencyRef = Object.values(refsByKey).some((values) => values.length > 0);
338
+ if (hasRequiredCapability || hasDependencyRef) {
339
+ return;
340
+ }
341
+ throw formatError(filePath, "WORK.md must include at least one required_capabilities entry or dependency ref in skill_refs, tool_refs, model_refs, wasm_component_refs, runtime_image_refs, or subagent_refs");
342
+ }
293
343
  function validatePortableOrUriRefs(values, key, filePath) {
294
344
  for (const [index, value] of values.entries()) {
295
345
  if (!(0, refs_1.validatePortableOrUriRef)(value)) {
@@ -315,6 +365,11 @@ function validateHashRefs(values, key, filePath) {
315
365
  }
316
366
  }
317
367
  }
368
+ function validateHashRef(value, key, filePath) {
369
+ if (!(0, refs_1.isSha256Ref)(value)) {
370
+ throw formatError(filePath, `${key} must be sha256:<64 lowercase hex chars>`);
371
+ }
372
+ }
318
373
  function validateRelativeMarkdownPaths(values, key, basename, filePath) {
319
374
  for (const [index, value] of values.entries()) {
320
375
  if (path_1.default.isAbsolute(value) || value.split(/[\\/]/).includes("..")) {
@@ -344,6 +399,10 @@ function validateAgentFrontmatter(type, frontmatter, filePath) {
344
399
  requireSemver(version, "version", filePath);
345
400
  switch (type) {
346
401
  case "spec": {
402
+ const specKind = optionalString(frontmatter, "spec_kind", filePath);
403
+ if (specKind) {
404
+ validateSpecKind(specKind, filePath);
405
+ }
347
406
  const role = expectString(frontmatter, "role", filePath);
348
407
  requireEnum(role, "role", ROLE_VALUES, filePath);
349
408
  const runtimeMode = expectString(frontmatter, "runtime_mode", filePath);
@@ -371,13 +430,28 @@ function validateAgentFrontmatter(type, frontmatter, filePath) {
371
430
  requireLowerToken(kind, "kind", filePath);
372
431
  const pricingModel = expectString(frontmatter, "pricing_model", filePath);
373
432
  requireEnum(pricingModel, "pricing_model", PRICING_MODEL_VALUES, filePath);
374
- validateCapabilities(expectList(frontmatter, "required_capabilities", filePath), "required_capabilities", filePath);
375
- validatePortableRefs(optionalList(frontmatter, "skill_refs", filePath), "skill_refs", filePath);
376
- validatePortableRefs(optionalList(frontmatter, "tool_refs", filePath), "tool_refs", filePath);
377
- validatePortableRefs(optionalList(frontmatter, "model_refs", filePath), "model_refs", filePath);
378
- validatePortableRefs(optionalList(frontmatter, "wasm_component_refs", filePath), "wasm_component_refs", filePath);
379
- validatePortableRefs(optionalList(frontmatter, "runtime_image_refs", filePath), "runtime_image_refs", filePath);
380
- validatePortableRefs(optionalList(frontmatter, "subagent_refs", filePath), "subagent_refs", filePath);
433
+ const requiredCapabilities = expectList(frontmatter, "required_capabilities", filePath);
434
+ validateCapabilities(requiredCapabilities, "required_capabilities", filePath);
435
+ const skillRefs = optionalList(frontmatter, "skill_refs", filePath);
436
+ validatePortableRefs(skillRefs, "skill_refs", filePath);
437
+ const toolRefs = optionalList(frontmatter, "tool_refs", filePath);
438
+ validatePortableRefs(toolRefs, "tool_refs", filePath);
439
+ const modelRefs = optionalList(frontmatter, "model_refs", filePath);
440
+ validatePortableRefs(modelRefs, "model_refs", filePath);
441
+ const wasmComponentRefs = optionalList(frontmatter, "wasm_component_refs", filePath);
442
+ validatePortableRefs(wasmComponentRefs, "wasm_component_refs", filePath);
443
+ const runtimeImageRefs = optionalList(frontmatter, "runtime_image_refs", filePath);
444
+ validatePortableRefs(runtimeImageRefs, "runtime_image_refs", filePath);
445
+ const subagentRefs = optionalList(frontmatter, "subagent_refs", filePath);
446
+ validatePortableRefs(subagentRefs, "subagent_refs", filePath);
447
+ validateWorkInvocationAnchor(requiredCapabilities, {
448
+ skill_refs: skillRefs,
449
+ tool_refs: toolRefs,
450
+ model_refs: modelRefs,
451
+ wasm_component_refs: wasmComponentRefs,
452
+ runtime_image_refs: runtimeImageRefs,
453
+ subagent_refs: subagentRefs,
454
+ }, filePath);
381
455
  validateFieldDescriptors(expectList(frontmatter, "inputs", filePath), "inputs", filePath);
382
456
  validateFieldDescriptors(expectList(frontmatter, "outputs", filePath), "outputs", filePath);
383
457
  expectBoolean(frontmatter, "receipt_required", filePath);
@@ -396,7 +470,16 @@ function validateAgentFrontmatter(type, frontmatter, filePath) {
396
470
  if (requestRef) {
397
471
  validatePortableOrUriScalar(requestRef, "request_ref", filePath);
398
472
  }
473
+ const triggerRef = optionalRefString(frontmatter, "trigger_ref", filePath);
474
+ if (triggerRef) {
475
+ validatePortableOrUriScalar(triggerRef, "trigger_ref", filePath);
476
+ }
477
+ const payloadHash = optionalString(frontmatter, "payload_hash", filePath);
478
+ if (payloadHash) {
479
+ validateHashRef(payloadHash, "payload_hash", filePath);
480
+ }
399
481
  validatePortableOrUriRefs(optionalList(frontmatter, "input_refs", filePath), "input_refs", filePath);
482
+ validatePortableOrUriRefs(optionalList(frontmatter, "queue_refs", filePath), "queue_refs", filePath);
400
483
  validateOptionalFieldDescriptors(optionalList(frontmatter, "requested_outputs", filePath), "requested_outputs", filePath);
401
484
  validatePortableOrUriRefs(optionalList(frontmatter, "constraint_refs", filePath), "constraint_refs", filePath);
402
485
  const artifactPolicy = optionalString(frontmatter, "artifact_policy", filePath);
@@ -416,8 +499,13 @@ function validateAgentFrontmatter(type, frontmatter, filePath) {
416
499
  if (costRef) {
417
500
  validatePortableOrUriScalar(costRef, "cost_ref", filePath);
418
501
  }
502
+ const redactionPolicy = optionalString(frontmatter, "redaction_policy", filePath);
503
+ if (redactionPolicy) {
504
+ requireEnum(redactionPolicy, "redaction_policy", REDACTION_POLICY_VALUES, filePath);
505
+ }
419
506
  validatePortableOrUriRefs(optionalList(frontmatter, "proof_refs", filePath), "proof_refs", filePath);
420
507
  validatePortableOrUriRefs(optionalList(frontmatter, "attestation_refs", filePath), "attestation_refs", filePath);
508
+ validateHashRefs(optionalList(frontmatter, "evidence_hashes", filePath), "evidence_hashes", filePath);
421
509
  validateHashRefs(optionalList(frontmatter, "input_hashes", filePath), "input_hashes", filePath);
422
510
  validateHashRefs(optionalList(frontmatter, "output_hashes", filePath), "output_hashes", filePath);
423
511
  break;
@@ -74,7 +74,92 @@ function pickAttributes(attributes, keys) {
74
74
  }
75
75
  return picked;
76
76
  }
77
- function nodeCapabilityRecord(root, config, node, kind, indexedAt) {
77
+ function toStringList(value) {
78
+ if (!Array.isArray(value)) {
79
+ return [];
80
+ }
81
+ return value.filter((item) => typeof item === "string");
82
+ }
83
+ function nodeRefSet(node) {
84
+ return new Set([node.id, node.qid, `${node.ws}:${node.id}`]);
85
+ }
86
+ function sortedNodes(nodes) {
87
+ return [...nodes].sort((a, b) => a.qid.localeCompare(b.qid));
88
+ }
89
+ function resolveSpecWorkContracts(index, specNode) {
90
+ const candidates = new Map();
91
+ const specDir = path_1.default.posix.dirname(specNode.path);
92
+ for (const contractPath of toStringList(specNode.attributes.work_contracts)) {
93
+ const normalizedPath = path_1.default.posix.normalize(path_1.default.posix.join(specDir, contractPath));
94
+ for (const node of Object.values(index.nodes)) {
95
+ if (node.type === "work" && node.ws === specNode.ws && node.path === normalizedPath) {
96
+ candidates.set(node.qid, node);
97
+ }
98
+ }
99
+ }
100
+ for (const qid of specNode.edges.relates) {
101
+ const node = index.nodes[qid];
102
+ if (node?.type === "work") {
103
+ candidates.set(node.qid, node);
104
+ }
105
+ }
106
+ for (const qid of index.reverse_edges.relates?.[specNode.qid] ?? []) {
107
+ const node = index.nodes[qid];
108
+ if (node?.type === "work") {
109
+ candidates.set(node.qid, node);
110
+ }
111
+ }
112
+ return sortedNodes(candidates.values());
113
+ }
114
+ function resolveWorkSpecs(index, workNode) {
115
+ const candidates = new Map();
116
+ const workRefs = nodeRefSet(workNode);
117
+ for (const node of Object.values(index.nodes)) {
118
+ if (node.type !== "spec" || node.ws !== workNode.ws) {
119
+ continue;
120
+ }
121
+ const agentId = typeof workNode.attributes.agent_id === "string" ? workNode.attributes.agent_id : undefined;
122
+ if (agentId && nodeRefSet(node).has(agentId)) {
123
+ candidates.set(node.qid, node);
124
+ }
125
+ if (resolveSpecWorkContracts(index, node).some((contract) => contract.qid === workNode.qid)) {
126
+ candidates.set(node.qid, node);
127
+ }
128
+ if (node.edges.relates.some((qid) => workRefs.has(qid))) {
129
+ candidates.set(node.qid, node);
130
+ }
131
+ }
132
+ return sortedNodes(candidates.values());
133
+ }
134
+ function resolveWorkOrders(index, workNode) {
135
+ const workRefs = nodeRefSet(workNode);
136
+ return sortedNodes(Object.values(index.nodes).filter((node) => node.type === "work_order" && workRefs.has(String(node.attributes.work_id ?? ""))));
137
+ }
138
+ function resolveReceiptsForOrders(index, orderNodes) {
139
+ const orderRefs = new Set();
140
+ for (const order of orderNodes) {
141
+ for (const ref of nodeRefSet(order)) {
142
+ orderRefs.add(ref);
143
+ }
144
+ }
145
+ return sortedNodes(Object.values(index.nodes).filter((node) => node.type === "receipt" && orderRefs.has(String(node.attributes.work_order_id ?? ""))));
146
+ }
147
+ function buildCapabilityLinkage(index, node, kind) {
148
+ if (kind !== "spec" && kind !== "work") {
149
+ return undefined;
150
+ }
151
+ const workContracts = kind === "spec" ? resolveSpecWorkContracts(index, node) : [node];
152
+ const specNodes = kind === "work" ? resolveWorkSpecs(index, node) : [];
153
+ const workOrders = workContracts.flatMap((workNode) => resolveWorkOrders(index, workNode));
154
+ const receipts = resolveReceiptsForOrders(index, workOrders);
155
+ return {
156
+ spec_qids: specNodes.map((specNode) => specNode.qid),
157
+ work_contract_qids: workContracts.map((workNode) => workNode.qid),
158
+ work_order_qids: workOrders.map((orderNode) => orderNode.qid),
159
+ receipt_qids: receipts.map((receiptNode) => receiptNode.qid),
160
+ };
161
+ }
162
+ function nodeCapabilityRecord(root, config, index, node, kind, indexedAt) {
78
163
  const absolutePath = path_1.default.resolve(root, node.path);
79
164
  const content = fs_1.default.readFileSync(absolutePath, "utf8");
80
165
  const record = {
@@ -99,6 +184,7 @@ function nodeCapabilityRecord(root, config, node, kind, indexedAt) {
99
184
  if (kind === "spec") {
100
185
  record.spec = pickAttributes(node.attributes, [
101
186
  "version",
187
+ "spec_kind",
102
188
  "role",
103
189
  "runtime_mode",
104
190
  "work_contracts",
@@ -131,6 +217,7 @@ function nodeCapabilityRecord(root, config, node, kind, indexedAt) {
131
217
  "receipt_required",
132
218
  ]);
133
219
  }
220
+ record.linkage = buildCapabilityLinkage(index, node, kind);
134
221
  return record;
135
222
  }
136
223
  function skillCapabilityRecord(root, config, skill, indexedAt) {
@@ -210,7 +297,7 @@ function buildCapabilitiesIndex(root, config, nodeIndex) {
210
297
  if (!kind) {
211
298
  continue;
212
299
  }
213
- records.push(nodeCapabilityRecord(root, config, node, kind, generatedAt));
300
+ records.push(nodeCapabilityRecord(root, config, index, node, kind, generatedAt));
214
301
  }
215
302
  records.push(...buildWorkspaceSkillCapabilities(root, config, generatedAt));
216
303
  const sortedRecords = sortRecords(records);
@@ -24,6 +24,7 @@ exports.DEFAULT_FRONTMATTER_KEY_ORDER = [
24
24
  "next",
25
25
  "supersedes",
26
26
  "version",
27
+ "spec_kind",
27
28
  "role",
28
29
  "runtime_mode",
29
30
  "work_contracts",
@@ -42,10 +43,14 @@ exports.DEFAULT_FRONTMATTER_KEY_ORDER = [
42
43
  "requester",
43
44
  "order_status",
44
45
  "request_ref",
46
+ "trigger_ref",
47
+ "payload_hash",
48
+ "queue_refs",
45
49
  "work_order_id",
46
50
  "receipt_status",
47
51
  "outcome",
48
52
  "cost_ref",
53
+ "redaction_policy",
49
54
  "target_id",
50
55
  "sentiment",
51
56
  "feedback_status",
@@ -73,6 +78,7 @@ exports.DEFAULT_FRONTMATTER_KEY_ORDER = [
73
78
  "artifact_policy",
74
79
  "proof_refs",
75
80
  "attestation_refs",
81
+ "evidence_hashes",
76
82
  "input_hashes",
77
83
  "output_hashes",
78
84
  "tags",