mdkg 0.4.2 → 0.5.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 +71 -15
- package/CLI_COMMAND_MATRIX.md +59 -2
- package/README.md +20 -2
- package/dist/cli.js +124 -2
- package/dist/command-contract.json +756 -2
- package/dist/commands/archive.js +31 -14
- package/dist/commands/bundle.js +180 -44
- package/dist/commands/capability.js +1 -0
- package/dist/commands/checkpoint.js +6 -5
- package/dist/commands/event_support.js +16 -7
- package/dist/commands/fix.js +11 -8
- package/dist/commands/git.js +41 -10
- package/dist/commands/goal.js +23 -23
- package/dist/commands/graph.js +64 -10
- package/dist/commands/handoff.js +4 -2
- package/dist/commands/init.js +28 -23
- package/dist/commands/loop.js +1668 -0
- package/dist/commands/loop_descriptors.js +219 -0
- package/dist/commands/mcp.js +101 -11
- package/dist/commands/new.js +49 -3
- package/dist/commands/pack.js +14 -7
- package/dist/commands/search.js +10 -1
- package/dist/commands/skill.js +12 -9
- package/dist/commands/skill_mirror.js +39 -31
- package/dist/commands/subgraph.js +59 -26
- package/dist/commands/task.js +77 -4
- package/dist/commands/upgrade.js +13 -15
- package/dist/commands/validate.js +20 -7
- package/dist/commands/work.js +44 -26
- package/dist/commands/workspace.js +10 -4
- package/dist/core/config.js +45 -17
- package/dist/core/filesystem_authority.js +281 -0
- package/dist/core/project_db_events.js +4 -0
- package/dist/core/project_db_materializer.js +2 -0
- package/dist/core/project_db_migrations.js +238 -181
- package/dist/core/project_db_snapshot.js +64 -14
- package/dist/core/workspace_path.js +7 -0
- package/dist/graph/archive_integrity.js +1 -1
- package/dist/graph/capabilities_index_cache.js +4 -1
- package/dist/graph/frontmatter.js +38 -0
- package/dist/graph/goal_scope.js +4 -1
- package/dist/graph/index_cache.js +37 -7
- package/dist/graph/loop_bindings.js +39 -0
- package/dist/graph/node.js +209 -1
- package/dist/graph/node_body.js +52 -6
- package/dist/graph/skills_index_cache.js +41 -6
- package/dist/graph/skills_indexer.js +5 -2
- package/dist/graph/sqlite_index.js +118 -105
- package/dist/graph/subgraphs.js +142 -5
- package/dist/graph/validate_graph.js +109 -13
- package/dist/graph/workspace_files.js +39 -9
- package/dist/init/AGENT_START.md +16 -0
- package/dist/init/CLI_COMMAND_MATRIX.md +14 -0
- package/dist/init/config.json +7 -1
- package/dist/init/init-manifest.json +49 -4
- package/dist/init/skills/default/pursue-mdkg-loop/SKILL.md +150 -0
- package/dist/init/templates/default/loop.md +160 -0
- package/dist/init/templates/loops/backend-api-cli-bloat-audit.loop.md +117 -0
- package/dist/init/templates/loops/design-frontend-ux-audit.loop.md +117 -0
- package/dist/init/templates/loops/duplicate-code-and-linting-audit.loop.md +114 -0
- package/dist/init/templates/loops/security-audit.loop.md +140 -0
- package/dist/init/templates/loops/tech-stack-best-practices-audit.loop.md +116 -0
- package/dist/init/templates/loops/test-ci-skill-infrastructure-audit.loop.md +116 -0
- package/dist/init/templates/loops/user-story-audit-and-recommendations.loop.md +116 -0
- package/dist/pack/order.js +3 -1
- package/dist/pack/pack.js +139 -6
- package/dist/templates/loader.js +9 -3
- package/dist/util/lock.js +10 -9
- package/dist/util/zip.js +163 -9
- package/package.json +8 -4
package/dist/graph/subgraphs.js
CHANGED
|
@@ -22,6 +22,131 @@ const MANIFEST_ENTRY = "manifest.json";
|
|
|
22
22
|
const GLOBAL_INDEX_ENTRY = ".mdkg/index/global.json";
|
|
23
23
|
const SKILLS_INDEX_ENTRY = ".mdkg/index/skills.json";
|
|
24
24
|
const CAPABILITIES_INDEX_ENTRY = ".mdkg/index/capabilities.json";
|
|
25
|
+
const REQUIRED_INDEX_ENTRIES = [GLOBAL_INDEX_ENTRY, SKILLS_INDEX_ENTRY, CAPABILITIES_INDEX_ENTRY];
|
|
26
|
+
const SHA256_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
27
|
+
function isRecord(value) {
|
|
28
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
29
|
+
}
|
|
30
|
+
function requireString(value, field) {
|
|
31
|
+
if (typeof value !== "string" || value.length === 0)
|
|
32
|
+
throw new Error(`bundle manifest invalid: ${field} must be a non-empty string`);
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
function requireHash(value, field) {
|
|
36
|
+
const result = requireString(value, field);
|
|
37
|
+
if (!SHA256_PATTERN.test(result))
|
|
38
|
+
throw new Error(`bundle manifest invalid: ${field} must be a sha256 digest`);
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
function requireSafePath(value, field) {
|
|
42
|
+
const result = requireString(value, field).replace(/\\/g, "/");
|
|
43
|
+
if (path_1.default.posix.isAbsolute(result) || result.split("/").some((part) => part === "" || part === "." || part === "..")) {
|
|
44
|
+
throw new Error(`bundle manifest invalid: ${field} must be a safe relative path`);
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
function manifestFilesHash(files) {
|
|
49
|
+
const data = `${JSON.stringify(files.map((file) => ({
|
|
50
|
+
path: file.path,
|
|
51
|
+
kind: file.kind,
|
|
52
|
+
workspace: file.workspace,
|
|
53
|
+
visibility: file.visibility,
|
|
54
|
+
size: file.size,
|
|
55
|
+
sha256: file.sha256,
|
|
56
|
+
})), null, 2)}\n`;
|
|
57
|
+
return sha256Buffer(Buffer.from(data, "utf8"));
|
|
58
|
+
}
|
|
59
|
+
function validateForeignManifest(value) {
|
|
60
|
+
if (!isRecord(value))
|
|
61
|
+
throw new Error("bundle manifest invalid: root must be an object");
|
|
62
|
+
if (value.manifest_version !== 1 || value.tool !== "mdkg")
|
|
63
|
+
throw new Error("bundle manifest invalid: unsupported version or tool");
|
|
64
|
+
const mdkgVersion = requireString(value.mdkg_version, "mdkg_version");
|
|
65
|
+
if (value.profile !== "private" && value.profile !== "public")
|
|
66
|
+
throw new Error("bundle manifest invalid: profile must be private or public");
|
|
67
|
+
if (!Array.isArray(value.selected_workspaces) || value.selected_workspaces.length === 0) {
|
|
68
|
+
throw new Error("bundle manifest invalid: selected_workspaces must be a non-empty array");
|
|
69
|
+
}
|
|
70
|
+
const selectedWorkspaces = value.selected_workspaces.map((item, index) => requireString(item, `selected_workspaces[${index}]`));
|
|
71
|
+
if (new Set(selectedWorkspaces).size !== selectedWorkspaces.length)
|
|
72
|
+
throw new Error("bundle manifest invalid: duplicate selected workspace");
|
|
73
|
+
if (!isRecord(value.source))
|
|
74
|
+
throw new Error("bundle manifest invalid: source must be an object");
|
|
75
|
+
const repo = requireString(value.source.repo, "source.repo");
|
|
76
|
+
const gitHead = value.source.git_head;
|
|
77
|
+
if (gitHead !== null && (typeof gitHead !== "string" || !/^[a-f0-9]{40}$/.test(gitHead))) {
|
|
78
|
+
throw new Error("bundle manifest invalid: source.git_head must be null or a Git SHA");
|
|
79
|
+
}
|
|
80
|
+
if (typeof value.source.dirty !== "boolean")
|
|
81
|
+
throw new Error("bundle manifest invalid: source.dirty must be boolean");
|
|
82
|
+
if (!Number.isSafeInteger(value.file_count) || Number(value.file_count) < 0)
|
|
83
|
+
throw new Error("bundle manifest invalid: file_count");
|
|
84
|
+
if (!isRecord(value.index_hashes) || !Array.isArray(value.files))
|
|
85
|
+
throw new Error("bundle manifest invalid: indexes and files are required");
|
|
86
|
+
const indexHashes = {};
|
|
87
|
+
for (const [entryPath, hash] of Object.entries(value.index_hashes))
|
|
88
|
+
indexHashes[requireSafePath(entryPath, "index hash path")] = requireHash(hash, `index_hashes.${entryPath}`);
|
|
89
|
+
const seen = new Set();
|
|
90
|
+
const files = value.files.map((item, index) => {
|
|
91
|
+
if (!isRecord(item))
|
|
92
|
+
throw new Error(`bundle manifest invalid: files[${index}] must be an object`);
|
|
93
|
+
const filePath = requireSafePath(item.path, `files[${index}].path`);
|
|
94
|
+
if (seen.has(filePath))
|
|
95
|
+
throw new Error(`bundle manifest invalid: duplicate file ${filePath}`);
|
|
96
|
+
seen.add(filePath);
|
|
97
|
+
if (item.kind !== "authored" && item.kind !== "archive_cache" && item.kind !== "generated_index")
|
|
98
|
+
throw new Error(`bundle manifest invalid: files[${index}].kind`);
|
|
99
|
+
if (!Number.isSafeInteger(item.size) || Number(item.size) < 0)
|
|
100
|
+
throw new Error(`bundle manifest invalid: files[${index}].size`);
|
|
101
|
+
const workspace = item.workspace === undefined ? undefined : requireString(item.workspace, `files[${index}].workspace`);
|
|
102
|
+
if (item.visibility !== undefined && item.visibility !== "private" && item.visibility !== "internal" && item.visibility !== "public")
|
|
103
|
+
throw new Error(`bundle manifest invalid: files[${index}].visibility`);
|
|
104
|
+
return { path: filePath, kind: item.kind, size: Number(item.size), sha256: requireHash(item.sha256, `files[${index}].sha256`), ...(workspace ? { workspace } : {}), ...(item.visibility ? { visibility: item.visibility } : {}) };
|
|
105
|
+
});
|
|
106
|
+
if (Number(value.file_count) !== files.length)
|
|
107
|
+
throw new Error("bundle manifest invalid: file_count mismatch");
|
|
108
|
+
for (const required of REQUIRED_INDEX_ENTRIES) {
|
|
109
|
+
const row = files.find((file) => file.path === required);
|
|
110
|
+
if (!row || row.kind !== "generated_index" || row.sha256 !== indexHashes[required])
|
|
111
|
+
throw new Error(`bundle manifest invalid: required index contract mismatch for ${required}`);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
manifest_version: 1,
|
|
115
|
+
tool: "mdkg",
|
|
116
|
+
mdkg_version: mdkgVersion,
|
|
117
|
+
profile: value.profile,
|
|
118
|
+
selected_workspaces: selectedWorkspaces,
|
|
119
|
+
source: { repo, git_head: gitHead, dirty: value.source.dirty },
|
|
120
|
+
source_tree_hash: requireHash(value.source_tree_hash, "source_tree_hash"),
|
|
121
|
+
bundle_hash: requireHash(value.bundle_hash, "bundle_hash"),
|
|
122
|
+
file_count: files.length,
|
|
123
|
+
index_hashes: indexHashes,
|
|
124
|
+
files,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function validateIndexShape(value) {
|
|
128
|
+
if (!isRecord(value) || !isRecord(value.meta) || !isRecord(value.nodes) || !isRecord(value.reverse_edges) || !isRecord(value.workspaces)) {
|
|
129
|
+
throw new Error("generated global index has an invalid shape");
|
|
130
|
+
}
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
function publicProjectionErrors(index, manifest) {
|
|
134
|
+
if (manifest.profile !== "public")
|
|
135
|
+
return [];
|
|
136
|
+
const selected = new Set(manifest.selected_workspaces);
|
|
137
|
+
const rows = new Map(manifest.files.map((file) => [file.path, file]));
|
|
138
|
+
const errors = [];
|
|
139
|
+
for (const alias of Object.keys(index.workspaces)) {
|
|
140
|
+
if (!selected.has(alias))
|
|
141
|
+
errors.push(`public bundle contains unselected workspace ${alias}`);
|
|
142
|
+
}
|
|
143
|
+
for (const node of Object.values(index.nodes)) {
|
|
144
|
+
const row = rows.get(toPosixPath(node.path));
|
|
145
|
+
if (!selected.has(node.ws) || !row || row.visibility !== "public")
|
|
146
|
+
errors.push(`public bundle contains non-public node ${node.qid}`);
|
|
147
|
+
}
|
|
148
|
+
return errors;
|
|
149
|
+
}
|
|
25
150
|
function toPosixPath(value) {
|
|
26
151
|
return value.split(path_1.default.sep).join("/");
|
|
27
152
|
}
|
|
@@ -36,7 +161,7 @@ function readJsonEntry(entries, entryPath) {
|
|
|
36
161
|
return JSON.parse(data.toString("utf8"));
|
|
37
162
|
}
|
|
38
163
|
function readBundleEntries(bundlePath) {
|
|
39
|
-
return new Map((0, zip_1.
|
|
164
|
+
return new Map((0, zip_1.readZipFileEntries)(bundlePath).map((entry) => [entry.name, entry.data]));
|
|
40
165
|
}
|
|
41
166
|
function resolveBundlePath(root, source) {
|
|
42
167
|
return path_1.default.resolve(root, source.path);
|
|
@@ -126,6 +251,15 @@ function entryHashErrors(entries, manifest) {
|
|
|
126
251
|
errors.push(`size mismatch for ${file.path}`);
|
|
127
252
|
}
|
|
128
253
|
}
|
|
254
|
+
for (const required of REQUIRED_INDEX_ENTRIES) {
|
|
255
|
+
const data = entries.get(required);
|
|
256
|
+
if (data && sha256Buffer(data) !== manifest.index_hashes[required])
|
|
257
|
+
errors.push(`generated index hash mismatch: ${required}`);
|
|
258
|
+
}
|
|
259
|
+
if (manifestFilesHash(manifest.files.filter((file) => file.kind !== "generated_index")) !== manifest.source_tree_hash)
|
|
260
|
+
errors.push("source_tree_hash mismatch");
|
|
261
|
+
if (manifestFilesHash(manifest.files) !== manifest.bundle_hash)
|
|
262
|
+
errors.push("bundle_hash mismatch");
|
|
129
263
|
return errors;
|
|
130
264
|
}
|
|
131
265
|
function projectOneSource(root, subgraph, source) {
|
|
@@ -159,11 +293,14 @@ function projectOneSource(root, subgraph, source) {
|
|
|
159
293
|
throw new Error(`bundle not found: ${source.path}`);
|
|
160
294
|
}
|
|
161
295
|
entries = readBundleEntries(bundlePath);
|
|
162
|
-
manifest = readJsonEntry(entries, MANIFEST_ENTRY);
|
|
163
|
-
index = readJsonEntry(entries, GLOBAL_INDEX_ENTRY);
|
|
164
|
-
readJsonEntry(entries, SKILLS_INDEX_ENTRY)
|
|
165
|
-
|
|
296
|
+
manifest = validateForeignManifest(readJsonEntry(entries, MANIFEST_ENTRY));
|
|
297
|
+
index = validateIndexShape(readJsonEntry(entries, GLOBAL_INDEX_ENTRY));
|
|
298
|
+
if (!isRecord(readJsonEntry(entries, SKILLS_INDEX_ENTRY)))
|
|
299
|
+
throw new Error("generated skills index has an invalid shape");
|
|
300
|
+
if (!isRecord(readJsonEntry(entries, CAPABILITIES_INDEX_ENTRY)))
|
|
301
|
+
throw new Error("generated capabilities index has an invalid shape");
|
|
166
302
|
errors.push(...entryHashErrors(entries, manifest));
|
|
303
|
+
errors.push(...publicProjectionErrors(index, manifest));
|
|
167
304
|
if (manifest.profile !== source.expected_profile) {
|
|
168
305
|
errors.push(`expected ${source.expected_profile} bundle but found ${manifest.profile}`);
|
|
169
306
|
}
|
|
@@ -6,6 +6,7 @@ const agent_file_types_1 = require("./agent_file_types");
|
|
|
6
6
|
const refs_1 = require("../util/refs");
|
|
7
7
|
const qid_1 = require("../util/qid");
|
|
8
8
|
const goal_scope_1 = require("./goal_scope");
|
|
9
|
+
const loop_bindings_1 = require("./loop_bindings");
|
|
9
10
|
function pushError(errors, message) {
|
|
10
11
|
if (errors) {
|
|
11
12
|
errors.push(message);
|
|
@@ -337,12 +338,11 @@ function validateAgentWorkflowDisputeRefs(index, allowMissing, externalWorkspace
|
|
|
337
338
|
}
|
|
338
339
|
}
|
|
339
340
|
function buildNodeIdsByWorkspace(index) {
|
|
340
|
-
const nodeIdsByWorkspace =
|
|
341
|
+
const nodeIdsByWorkspace = new Map();
|
|
341
342
|
for (const node of Object.values(index.nodes)) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
nodeIdsByWorkspace[node.ws].add(node.id);
|
|
343
|
+
const ids = nodeIdsByWorkspace.get(node.ws) ?? new Set();
|
|
344
|
+
ids.add(node.id);
|
|
345
|
+
nodeIdsByWorkspace.set(node.ws, ids);
|
|
346
346
|
}
|
|
347
347
|
return nodeIdsByWorkspace;
|
|
348
348
|
}
|
|
@@ -351,10 +351,10 @@ function validateAgentWorkflowNodeIdRef(qid, ws, field, value, nodeIdsByWorkspac
|
|
|
351
351
|
if (workspace && value.includes(":") && externalWorkspaces?.has(workspace)) {
|
|
352
352
|
return;
|
|
353
353
|
}
|
|
354
|
-
if (value.includes(":") && nodeIdsByWorkspace
|
|
354
|
+
if (value.includes(":") && nodeIdsByWorkspace.get(workspace)?.has(value.split(":").slice(1).join(":"))) {
|
|
355
355
|
return;
|
|
356
356
|
}
|
|
357
|
-
if (nodeIdsByWorkspace
|
|
357
|
+
if (nodeIdsByWorkspace.get(ws)?.has(value)) {
|
|
358
358
|
return;
|
|
359
359
|
}
|
|
360
360
|
if (allowSkillRef &&
|
|
@@ -393,15 +393,14 @@ function validateAgentWorkflowFeedbackProposalRefs(index, allowMissing, knownSki
|
|
|
393
393
|
}
|
|
394
394
|
}
|
|
395
395
|
function buildArchiveIdsByWorkspace(index) {
|
|
396
|
-
const archiveIdsByWorkspace =
|
|
396
|
+
const archiveIdsByWorkspace = new Map();
|
|
397
397
|
for (const node of Object.values(index.nodes)) {
|
|
398
398
|
if (node.type !== "archive") {
|
|
399
399
|
continue;
|
|
400
400
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
archiveIdsByWorkspace[node.ws].add(node.id);
|
|
401
|
+
const ids = archiveIdsByWorkspace.get(node.ws) ?? new Set();
|
|
402
|
+
ids.add(node.id);
|
|
403
|
+
archiveIdsByWorkspace.set(node.ws, ids);
|
|
405
404
|
}
|
|
406
405
|
return archiveIdsByWorkspace;
|
|
407
406
|
}
|
|
@@ -414,7 +413,7 @@ function validateArchiveUriValue(qid, ws, field, value, archiveIdsByWorkspace, a
|
|
|
414
413
|
pushError(errors, `${qid}: ${field} has malformed archive ref ${value}`);
|
|
415
414
|
return;
|
|
416
415
|
}
|
|
417
|
-
if (archiveIdsByWorkspace
|
|
416
|
+
if (archiveIdsByWorkspace.get(ws)?.has(archiveId)) {
|
|
418
417
|
return;
|
|
419
418
|
}
|
|
420
419
|
if (allowMissing) {
|
|
@@ -496,6 +495,102 @@ function validateGoalRefs(index, allowMissing, errors) {
|
|
|
496
495
|
}
|
|
497
496
|
}
|
|
498
497
|
}
|
|
498
|
+
function loopAttributeList(node, key) {
|
|
499
|
+
const value = node.attributes[key];
|
|
500
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
501
|
+
}
|
|
502
|
+
function resolveLoopEvidenceNode(index, ws, ref) {
|
|
503
|
+
const resolved = (0, qid_1.resolveQid)(index, ref, ws);
|
|
504
|
+
return resolved.status === "ok" ? index.nodes[resolved.qid] : undefined;
|
|
505
|
+
}
|
|
506
|
+
function isAcceptedLoopDecision(node) {
|
|
507
|
+
return node.type === "dec" && node.status === "accepted";
|
|
508
|
+
}
|
|
509
|
+
function isVerifiedLoopApproval(node) {
|
|
510
|
+
if (node.type === "dec") {
|
|
511
|
+
return node.status === "accepted";
|
|
512
|
+
}
|
|
513
|
+
if (node.type === "checkpoint") {
|
|
514
|
+
return node.status === "done";
|
|
515
|
+
}
|
|
516
|
+
return node.type === "receipt" && node.attributes.receipt_status === "verified";
|
|
517
|
+
}
|
|
518
|
+
function validateLoopTypedRefs(index, allowMissing, errors) {
|
|
519
|
+
for (const [qid, node] of Object.entries(index.nodes)) {
|
|
520
|
+
if (node.type !== "loop") {
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
for (const ref of loopAttributeList(node, "decision_refs")) {
|
|
524
|
+
if ((0, refs_1.isUriRef)(ref)) {
|
|
525
|
+
pushError(errors, `${qid}: decision_refs requires a local accepted dec ref, got ${ref}`);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
const target = resolveLoopEvidenceNode(index, node.ws, ref);
|
|
529
|
+
if (!target) {
|
|
530
|
+
if (!allowMissing) {
|
|
531
|
+
pushError(errors, `${qid}: decision_refs references missing node ${ref}`);
|
|
532
|
+
}
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (!isAcceptedLoopDecision(target)) {
|
|
536
|
+
pushError(errors, `${qid}: decision_refs ${ref} must target an accepted dec, got ${target.type}:${target.status ?? "none"}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
for (const ref of loopAttributeList(node, "approval_refs")) {
|
|
540
|
+
if ((0, refs_1.isUriRef)(ref)) {
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const target = resolveLoopEvidenceNode(index, node.ws, ref);
|
|
544
|
+
if (!target) {
|
|
545
|
+
if (!allowMissing) {
|
|
546
|
+
pushError(errors, `${qid}: approval_refs references missing node ${ref}`);
|
|
547
|
+
}
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
if (!isVerifiedLoopApproval(target)) {
|
|
551
|
+
pushError(errors, `${qid}: approval_refs ${ref} must target an accepted dec, done checkpoint, or verified receipt, got ${target.type}:${target.status ?? target.attributes.receipt_status ?? "none"}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
const checks = [
|
|
555
|
+
{ key: "question_answer_refs", kind: "decision" },
|
|
556
|
+
{ key: "action_approval_refs", kind: "approval" },
|
|
557
|
+
{ key: "evidence_lane_refs", kind: "evidence" },
|
|
558
|
+
{ key: "lane_waiver_decision_refs", kind: "decision" },
|
|
559
|
+
{ key: "lane_waiver_approval_refs", kind: "approval" },
|
|
560
|
+
];
|
|
561
|
+
for (const check of checks) {
|
|
562
|
+
const bindings = (0, loop_bindings_1.parseLoopRefBindings)(loopAttributeList(node, check.key));
|
|
563
|
+
for (const binding of bindings) {
|
|
564
|
+
if ((0, refs_1.isUriRef)(binding.ref)) {
|
|
565
|
+
if (check.kind === "decision") {
|
|
566
|
+
pushError(errors, `${qid}: ${check.key} ${binding.identity} requires a local accepted dec ref, got ${binding.ref}`);
|
|
567
|
+
}
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
const target = resolveLoopEvidenceNode(index, node.ws, binding.ref);
|
|
571
|
+
if (!target) {
|
|
572
|
+
if (!allowMissing) {
|
|
573
|
+
pushError(errors, `${qid}: ${check.key} ${binding.identity} references missing node ${binding.ref}`);
|
|
574
|
+
}
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if (check.kind === "decision" && !isAcceptedLoopDecision(target)) {
|
|
578
|
+
pushError(errors, `${qid}: ${check.key} ${binding.identity} must target an accepted dec`);
|
|
579
|
+
}
|
|
580
|
+
if (check.kind === "approval" && !isVerifiedLoopApproval(target)) {
|
|
581
|
+
pushError(errors, `${qid}: ${check.key} ${binding.identity} must target an accepted dec, done checkpoint, or verified receipt`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
const waiverDecisionIdentities = new Set((0, loop_bindings_1.parseLoopRefBindings)(loopAttributeList(node, "lane_waiver_decision_refs")).map((binding) => binding.identity));
|
|
586
|
+
const waiverApprovalIdentities = new Set((0, loop_bindings_1.parseLoopRefBindings)(loopAttributeList(node, "lane_waiver_approval_refs")).map((binding) => binding.identity));
|
|
587
|
+
for (const identity of new Set([...waiverDecisionIdentities, ...waiverApprovalIdentities])) {
|
|
588
|
+
if (!waiverDecisionIdentities.has(identity) || !waiverApprovalIdentities.has(identity)) {
|
|
589
|
+
pushError(errors, `${qid}: lane waiver ${identity} requires both decision and approval bindings`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
499
594
|
function validateSingleActiveRootGoals(index, errors) {
|
|
500
595
|
const activeByWorkspace = {};
|
|
501
596
|
for (const [qid, node] of Object.entries(index.nodes)) {
|
|
@@ -563,6 +658,7 @@ function collectGraphErrors(index, options = {}) {
|
|
|
563
658
|
validateAgentWorkflowDisputeRefs(index, allowMissing, externalWorkspaces, errors);
|
|
564
659
|
validateAgentWorkflowFeedbackProposalRefs(index, allowMissing, knownSkillSlugs, externalWorkspaces, errors);
|
|
565
660
|
validateArchiveUriRefs(index, allowMissing, errors);
|
|
661
|
+
validateLoopTypedRefs(index, allowMissing, errors);
|
|
566
662
|
validateGoalRefs(index, allowMissing, errors);
|
|
567
663
|
validateSingleActiveRootGoals(index, errors);
|
|
568
664
|
detectPrevNextCycles(index, errors);
|
|
@@ -8,28 +8,51 @@ exports.listWorkspaceDocFiles = listWorkspaceDocFiles;
|
|
|
8
8
|
exports.listWorkspaceDocFilesByAlias = listWorkspaceDocFilesByAlias;
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
10
|
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const filesystem_authority_1 = require("../core/filesystem_authority");
|
|
12
|
+
const workspace_path_1 = require("../core/workspace_path");
|
|
11
13
|
const DOC_FOLDERS = ["core", "design", "work"];
|
|
12
|
-
function
|
|
14
|
+
function accountMarkdownFile(filePath, budget) {
|
|
15
|
+
const size = fs_1.default.statSync(filePath).size;
|
|
16
|
+
if (size > budget.limits.max_file_bytes) {
|
|
17
|
+
throw new Error(`graph file exceeds index.limits.max_file_bytes (${budget.limits.max_file_bytes}): ${filePath}`);
|
|
18
|
+
}
|
|
19
|
+
if (budget.files + 1 > budget.limits.max_files) {
|
|
20
|
+
throw new Error(`graph file count exceeds index.limits.max_files (${budget.limits.max_files})`);
|
|
21
|
+
}
|
|
22
|
+
if (budget.bytes + size > budget.limits.max_total_bytes) {
|
|
23
|
+
throw new Error(`graph bytes exceed index.limits.max_total_bytes (${budget.limits.max_total_bytes})`);
|
|
24
|
+
}
|
|
25
|
+
budget.files += 1;
|
|
26
|
+
budget.bytes += size;
|
|
27
|
+
}
|
|
28
|
+
function listMarkdownFiles(dir, budget, depth = 0) {
|
|
13
29
|
if (!fs_1.default.existsSync(dir)) {
|
|
14
30
|
return [];
|
|
15
31
|
}
|
|
32
|
+
if (depth > budget.limits.max_depth) {
|
|
33
|
+
throw new Error(`graph directory depth exceeds index.limits.max_depth (${budget.limits.max_depth}): ${dir}`);
|
|
34
|
+
}
|
|
16
35
|
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
17
36
|
const files = [];
|
|
18
37
|
for (const entry of entries) {
|
|
19
38
|
const fullPath = path_1.default.join(dir, entry.name);
|
|
20
39
|
if (entry.isDirectory()) {
|
|
21
|
-
files.push(...listMarkdownFiles(fullPath));
|
|
40
|
+
files.push(...listMarkdownFiles(fullPath, budget, depth + 1));
|
|
22
41
|
}
|
|
23
42
|
else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
43
|
+
accountMarkdownFile(fullPath, budget);
|
|
24
44
|
files.push(fullPath);
|
|
25
45
|
}
|
|
26
46
|
}
|
|
27
47
|
return files;
|
|
28
48
|
}
|
|
29
|
-
function listArchiveSidecarFiles(dir) {
|
|
49
|
+
function listArchiveSidecarFiles(dir, budget, depth = 0) {
|
|
30
50
|
if (!fs_1.default.existsSync(dir)) {
|
|
31
51
|
return [];
|
|
32
52
|
}
|
|
53
|
+
if (depth > budget.limits.max_depth) {
|
|
54
|
+
throw new Error(`graph directory depth exceeds index.limits.max_depth (${budget.limits.max_depth}): ${dir}`);
|
|
55
|
+
}
|
|
33
56
|
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
34
57
|
const files = [];
|
|
35
58
|
for (const entry of entries) {
|
|
@@ -38,9 +61,10 @@ function listArchiveSidecarFiles(dir) {
|
|
|
38
61
|
}
|
|
39
62
|
const fullPath = path_1.default.join(dir, entry.name);
|
|
40
63
|
if (entry.isDirectory()) {
|
|
41
|
-
files.push(...listArchiveSidecarFiles(fullPath));
|
|
64
|
+
files.push(...listArchiveSidecarFiles(fullPath, budget, depth + 1));
|
|
42
65
|
}
|
|
43
66
|
else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
67
|
+
accountMarkdownFile(fullPath, budget);
|
|
44
68
|
files.push(fullPath);
|
|
45
69
|
}
|
|
46
70
|
}
|
|
@@ -54,31 +78,37 @@ function getWorkspaceDocRoots(root, config) {
|
|
|
54
78
|
if (!entry.enabled) {
|
|
55
79
|
continue;
|
|
56
80
|
}
|
|
57
|
-
const wsRoot =
|
|
81
|
+
const wsRoot = (0, filesystem_authority_1.withContainedPathSink)({
|
|
82
|
+
root,
|
|
83
|
+
relativePath: (0, workspace_path_1.workspaceDocumentRelativePath)(entry.path, entry.mdkg_dir),
|
|
84
|
+
operation: "read",
|
|
85
|
+
}, ({ absolutePath }) => absolutePath);
|
|
58
86
|
roots.push({ alias, root: wsRoot });
|
|
59
87
|
}
|
|
60
88
|
return roots;
|
|
61
89
|
}
|
|
62
90
|
function listWorkspaceDocFiles(root, config) {
|
|
63
91
|
const files = [];
|
|
92
|
+
const budget = { files: 0, bytes: 0, limits: config.index.limits };
|
|
64
93
|
for (const { root: wsRoot } of getWorkspaceDocRoots(root, config)) {
|
|
65
94
|
for (const folder of DOC_FOLDERS) {
|
|
66
95
|
const folderPath = path_1.default.join(wsRoot, folder);
|
|
67
|
-
files.push(...listMarkdownFiles(folderPath));
|
|
96
|
+
files.push(...listMarkdownFiles(folderPath, budget));
|
|
68
97
|
}
|
|
69
|
-
files.push(...listArchiveSidecarFiles(path_1.default.join(wsRoot, "archive")));
|
|
98
|
+
files.push(...listArchiveSidecarFiles(path_1.default.join(wsRoot, "archive"), budget));
|
|
70
99
|
}
|
|
71
100
|
return files;
|
|
72
101
|
}
|
|
73
102
|
function listWorkspaceDocFilesByAlias(root, config) {
|
|
74
103
|
const result = {};
|
|
104
|
+
const budget = { files: 0, bytes: 0, limits: config.index.limits };
|
|
75
105
|
for (const { alias, root: wsRoot } of getWorkspaceDocRoots(root, config)) {
|
|
76
106
|
const files = [];
|
|
77
107
|
for (const folder of DOC_FOLDERS) {
|
|
78
108
|
const folderPath = path_1.default.join(wsRoot, folder);
|
|
79
|
-
files.push(...listMarkdownFiles(folderPath));
|
|
109
|
+
files.push(...listMarkdownFiles(folderPath, budget));
|
|
80
110
|
}
|
|
81
|
-
files.push(...listArchiveSidecarFiles(path_1.default.join(wsRoot, "archive")));
|
|
111
|
+
files.push(...listArchiveSidecarFiles(path_1.default.join(wsRoot, "archive"), budget));
|
|
82
112
|
files.sort();
|
|
83
113
|
result[alias] = files;
|
|
84
114
|
}
|
package/dist/init/AGENT_START.md
CHANGED
|
@@ -28,6 +28,11 @@ Agent operating prompt:
|
|
|
28
28
|
- Use `mdkg goal activate <goal-id>` to make one local root goal active, then `mdkg goal next` to surface one scoped feature, task, bug, test, or spike at a time; normal `mdkg next` remains for non-goal concrete work.
|
|
29
29
|
- Use `mdkg goal claim [goal-id] <work-id>` only after accepting the surfaced work item; `mdkg goal next` is read-only.
|
|
30
30
|
- Treat goal `required_checks` as report-only guidance from mdkg. Run commands yourself, then record evidence in the goal or active work item.
|
|
31
|
+
- Use `mdkg loop show <loop>`, `mdkg loop plan <loop>`, and
|
|
32
|
+
`mdkg pack <loop>` for first-class loop nodes. Use
|
|
33
|
+
`mdkg skill show pursue-mdkg-loop` before executing a loop so the agent works
|
|
34
|
+
every authorized linked lane, records blocker recovery, and closes only when
|
|
35
|
+
the loop definition of done is satisfied or explicitly waived.
|
|
31
36
|
- Record skill improvement candidates during normal goal execution; edit `SKILL.md` only when the active node is explicit skill-maintenance work.
|
|
32
37
|
- Use `mdkg skill list`, `mdkg skill search`, and `mdkg skill show <slug>` for skill discovery.
|
|
33
38
|
- Use `mdkg capability list/search/show` for deterministic skills, `MANIFEST.md` / legacy `SPEC.md`, `WORK.md`, core-doc, and design-doc capability discovery.
|
|
@@ -97,6 +102,16 @@ If an active goal is known:
|
|
|
97
102
|
- `mdkg goal evaluate <goal-id>`
|
|
98
103
|
- repeat until the goal condition is achieved, blocked, paused, or budget-limited
|
|
99
104
|
|
|
105
|
+
If an active loop is known:
|
|
106
|
+
- `mdkg loop show <loop-id> --json`
|
|
107
|
+
- `mdkg skill show pursue-mdkg-loop`
|
|
108
|
+
- `mdkg loop plan <loop-id> --json`
|
|
109
|
+
- `mdkg pack <loop-id> --pack-profile concise --dry-run --stats`
|
|
110
|
+
- answer or record pre-run questions and approval requirements
|
|
111
|
+
- work every authorized linked lane before marking the loop done or blocked
|
|
112
|
+
- keep the loop open while required lanes are incomplete and not explicitly
|
|
113
|
+
waived
|
|
114
|
+
|
|
100
115
|
If no task is known:
|
|
101
116
|
- `mdkg search "..."`
|
|
102
117
|
- `mdkg show <id>`
|
|
@@ -113,6 +128,7 @@ Skill discovery:
|
|
|
113
128
|
- `mdkg skill list --tags stage:execute --json`
|
|
114
129
|
- `mdkg skill list --tags stage:review --json`
|
|
115
130
|
- `mdkg skill show select-work-and-ground-context`
|
|
131
|
+
- `mdkg skill show pursue-mdkg-loop`
|
|
116
132
|
|
|
117
133
|
Capability discovery:
|
|
118
134
|
- `mdkg capability list --kind skill --json`
|
|
@@ -29,6 +29,7 @@ Primary commands:
|
|
|
29
29
|
- `mdkg subgraph`
|
|
30
30
|
- `mdkg work`
|
|
31
31
|
- `mdkg goal`
|
|
32
|
+
- `mdkg loop`
|
|
32
33
|
- `mdkg task`
|
|
33
34
|
- `mdkg validate`
|
|
34
35
|
- `mdkg status [--json]`
|
|
@@ -130,6 +131,7 @@ Validation commands:
|
|
|
130
131
|
Node creation commands:
|
|
131
132
|
- `mdkg new <type> "<title>" [options] [--json]`
|
|
132
133
|
- `mdkg new goal "<title>" [options] [--json]`
|
|
134
|
+
- `mdkg new loop "<title>" [options] [--json]`
|
|
133
135
|
- `mdkg new spike "<research question>" [options] [--json]`
|
|
134
136
|
|
|
135
137
|
Agent workflow file type creation:
|
|
@@ -153,10 +155,22 @@ Agent workflow notes:
|
|
|
153
155
|
- `manifest` and `work` scaffold as validation-clean standalone docs.
|
|
154
156
|
- `work_order`, `receipt`, `feedback`, `dispute`, and `proposal` need real refs before strict `mdkg validate` passes.
|
|
155
157
|
- `goal` nodes capture recursive objective state and required checks, but normal `mdkg next` does not select them.
|
|
158
|
+
- `loop` nodes capture reusable process state, fork lineage, child refs, run/evidence refs, blocker-continuation policy, and a high-bar definition of done; runtime execution remains outside mdkg.
|
|
156
159
|
- `spike` nodes are actionable research/planning work under `.mdkg/work/`; use `mdkg task start|update|done` for lifecycle state.
|
|
157
160
|
- Spikes record sources, findings, recommendations, follow-up node ideas, and skill candidates in Markdown body sections; they do not perform web search, execute research, create follow-up nodes, generate `SKILL.md`, or expose a `mdkg spike ...` namespace automatically.
|
|
158
161
|
- after fresh init, run `mdkg index` before treating `mdkg doctor --strict --json` as a clean health gate; init writes source scaffold files and index writes generated caches.
|
|
159
162
|
|
|
163
|
+
Loop commands:
|
|
164
|
+
- `mdkg loop list [--json]`
|
|
165
|
+
- `mdkg loop show <loop-or-template> [--json]`
|
|
166
|
+
- `mdkg loop fork <template> --scope <scope> [--materialization default_children|planning_only|manual] [--planning-only] [--no-children] [--dry-run] [--json]`
|
|
167
|
+
- `mdkg loop plan <loop> [--json]`
|
|
168
|
+
- `mdkg loop next <loop> [--json]`
|
|
169
|
+
- `mdkg loop runs <loop> [--json]`
|
|
170
|
+
- `loop` is one first-class node type; templates, scoped forks, and run-bearing loops are represented through metadata and links
|
|
171
|
+
- seed templates live under `.mdkg/templates/loops/*.loop.md`
|
|
172
|
+
- mdkg defines durable declarative process state and graph context; runtimes execute agents, tools, sandboxes, traces, and model routing
|
|
173
|
+
|
|
160
174
|
Workspace registry commands:
|
|
161
175
|
- `mdkg workspace ls [--json]`
|
|
162
176
|
- `mdkg workspace add <alias> <path> [--mdkg-dir <dir>] [--visibility <level>] [--json]`
|
package/dist/init/config.json
CHANGED
|
@@ -12,7 +12,13 @@
|
|
|
12
12
|
"global_index_path": ".mdkg/index/global.json",
|
|
13
13
|
"sqlite_path": ".mdkg/index/mdkg.sqlite",
|
|
14
14
|
"sqlite_commit_warning_bytes": 52428800,
|
|
15
|
-
"lock_timeout_ms": 10000
|
|
15
|
+
"lock_timeout_ms": 10000,
|
|
16
|
+
"limits": {
|
|
17
|
+
"max_files": 100000,
|
|
18
|
+
"max_file_bytes": 8388608,
|
|
19
|
+
"max_total_bytes": 536870912,
|
|
20
|
+
"max_depth": 64
|
|
21
|
+
}
|
|
16
22
|
},
|
|
17
23
|
"capabilities": {
|
|
18
24
|
"cache_path": ".mdkg/index/capabilities.json"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"tool": "mdkg",
|
|
4
|
-
"mdkg_version": "0.
|
|
4
|
+
"mdkg_version": "0.5.0",
|
|
5
5
|
"files": [
|
|
6
6
|
{
|
|
7
7
|
"path": ".mdkg/config.json",
|
|
8
8
|
"category": "config",
|
|
9
|
-
"sha256": "
|
|
9
|
+
"sha256": "3d0ed0f8d65a33fe54a3855b3314a6762ad6dd1eaf6efcdce3e551a493433dc3"
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
12
|
"path": ".mdkg/core/COLLABORATION.md",
|
|
@@ -83,6 +83,11 @@
|
|
|
83
83
|
"category": "default_skill",
|
|
84
84
|
"sha256": "0545b011232d6924d84d80ad4b33153e591249d440c55481f125b37923c298d5"
|
|
85
85
|
},
|
|
86
|
+
{
|
|
87
|
+
"path": ".mdkg/skills/pursue-mdkg-loop/SKILL.md",
|
|
88
|
+
"category": "default_skill",
|
|
89
|
+
"sha256": "65dae2a6e79f2f7e4265b7057287db0308ac6ac59e6ebe9a6b6cd50b5d90176b"
|
|
90
|
+
},
|
|
86
91
|
{
|
|
87
92
|
"path": ".mdkg/skills/select-work-and-ground-context/SKILL.md",
|
|
88
93
|
"category": "default_skill",
|
|
@@ -143,6 +148,11 @@
|
|
|
143
148
|
"category": "template",
|
|
144
149
|
"sha256": "8f984580aefd02b34639fa7f5d2a834662656bbf2e12f14a285f5ae31aff74ce"
|
|
145
150
|
},
|
|
151
|
+
{
|
|
152
|
+
"path": ".mdkg/templates/default/loop.md",
|
|
153
|
+
"category": "template",
|
|
154
|
+
"sha256": "6e8f280c3375b9ec18e09fdb263665b2e3da45ca09b2547378fdd4c9bc015d89"
|
|
155
|
+
},
|
|
146
156
|
{
|
|
147
157
|
"path": ".mdkg/templates/default/manifest.md",
|
|
148
158
|
"category": "template",
|
|
@@ -203,6 +213,41 @@
|
|
|
203
213
|
"category": "template",
|
|
204
214
|
"sha256": "1ab3c4c79ecdaff2b76007bfbb9844bf581da592f70ac2a2475594b9c279d7cd"
|
|
205
215
|
},
|
|
216
|
+
{
|
|
217
|
+
"path": ".mdkg/templates/loops/backend-api-cli-bloat-audit.loop.md",
|
|
218
|
+
"category": "template",
|
|
219
|
+
"sha256": "e61083901ae2dcd315eba81ebf9aa90b9cbe7fb3ba923dd6834feb5502fd03b3"
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"path": ".mdkg/templates/loops/design-frontend-ux-audit.loop.md",
|
|
223
|
+
"category": "template",
|
|
224
|
+
"sha256": "2da3ad70d71e52545006883452b5fc2673761ecd0875a00746a5468c25eee241"
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"path": ".mdkg/templates/loops/duplicate-code-and-linting-audit.loop.md",
|
|
228
|
+
"category": "template",
|
|
229
|
+
"sha256": "36cbe0957883b5d75ebae6705f24eeb77f616202553c428386eeba9821aeaccc"
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"path": ".mdkg/templates/loops/security-audit.loop.md",
|
|
233
|
+
"category": "template",
|
|
234
|
+
"sha256": "2c26211f628cd62474595191fdc106e051a839bf7232a2336852a1b44e1ca9ba"
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"path": ".mdkg/templates/loops/tech-stack-best-practices-audit.loop.md",
|
|
238
|
+
"category": "template",
|
|
239
|
+
"sha256": "866c6aae55fa1e1eba9b77c0c92b678c779d96eb39cf293cf51c7a244ea39e54"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"path": ".mdkg/templates/loops/test-ci-skill-infrastructure-audit.loop.md",
|
|
243
|
+
"category": "template",
|
|
244
|
+
"sha256": "84d12faf4c33d808eae229f13f169f5b1aedd199e9ef5ef3f1b4b055f0fb3c39"
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
"path": ".mdkg/templates/loops/user-story-audit-and-recommendations.loop.md",
|
|
248
|
+
"category": "template",
|
|
249
|
+
"sha256": "2bc4b328ccdfd5c0959b003956a5b8bff1ba83032514ed23025fc8f3587c323f"
|
|
250
|
+
},
|
|
206
251
|
{
|
|
207
252
|
"path": ".mdkg/templates/skills/base.SKILL.md",
|
|
208
253
|
"category": "template",
|
|
@@ -311,7 +356,7 @@
|
|
|
311
356
|
{
|
|
312
357
|
"path": "AGENT_START.md",
|
|
313
358
|
"category": "startup_doc",
|
|
314
|
-
"sha256": "
|
|
359
|
+
"sha256": "60d5133337d383b0a6a13e108d26a6e034afe59f21a27318cf6f8f0cc3c3c5a3"
|
|
315
360
|
},
|
|
316
361
|
{
|
|
317
362
|
"path": "AGENTS.md",
|
|
@@ -326,7 +371,7 @@
|
|
|
326
371
|
{
|
|
327
372
|
"path": "CLI_COMMAND_MATRIX.md",
|
|
328
373
|
"category": "startup_doc",
|
|
329
|
-
"sha256": "
|
|
374
|
+
"sha256": "b055b6e1a36f633ccb7eb9954f3ba4ec93d57b0829fcfcbdbc1713885d6bc357"
|
|
330
375
|
},
|
|
331
376
|
{
|
|
332
377
|
"path": "llms.txt",
|