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.
- package/CHANGELOG.md +69 -0
- package/README.md +40 -15
- package/dist/cli.js +293 -13
- package/dist/commands/capability.js +13 -8
- package/dist/commands/db.js +185 -1
- package/dist/commands/format.js +1 -1
- package/dist/commands/spec.js +101 -0
- package/dist/commands/work.js +569 -20
- package/dist/core/project_db_migrations.js +24 -0
- package/dist/core/project_db_queue.js +186 -0
- package/dist/core/project_db_snapshot.js +28 -3
- package/dist/graph/agent_file_types.js +95 -7
- package/dist/graph/capabilities_indexer.js +89 -2
- package/dist/graph/frontmatter.js +6 -0
- package/dist/graph/node.js +8 -2
- package/dist/init/AGENT_START.md +15 -9
- package/dist/init/CLI_COMMAND_MATRIX.md +33 -5
- package/dist/init/README.md +36 -11
- package/dist/init/init-manifest.json +64 -9
- package/dist/init/skills/default/verify-close-and-checkpoint/SKILL.md +8 -7
- package/dist/init/templates/default/receipt.md +12 -1
- package/dist/init/templates/default/spec.md +8 -6
- package/dist/init/templates/default/work.md +5 -1
- package/dist/init/templates/default/work_order.md +11 -0
- package/dist/init/templates/skills/base.SKILL.md +66 -0
- package/dist/init/templates/specs/agent.SPEC.md +80 -0
- package/dist/init/templates/specs/api.SPEC.md +33 -0
- package/dist/init/templates/specs/base.SPEC.md +120 -0
- package/dist/init/templates/specs/capability.SPEC.md +45 -0
- package/dist/init/templates/specs/integration.SPEC.md +25 -0
- package/dist/init/templates/specs/model.SPEC.md +21 -0
- package/dist/init/templates/specs/project.SPEC.md +39 -0
- package/dist/init/templates/specs/runtime-agent.SPEC.md +49 -0
- package/dist/init/templates/specs/runtime-image.SPEC.md +21 -0
- package/dist/init/templates/specs/tool.SPEC.md +25 -0
- package/dist/util/argparse.js +8 -0
- 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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
validatePortableRefs(
|
|
378
|
-
|
|
379
|
-
validatePortableRefs(
|
|
380
|
-
|
|
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
|
|
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",
|