mdkg 0.4.1 → 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 +103 -15
- package/CLI_COMMAND_MATRIX.md +109 -2
- package/README.md +40 -2
- package/dist/cli.js +262 -2
- package/dist/command-contract.json +1902 -524
- 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 +624 -0
- 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/argparse.js +5 -0
- package/dist/util/lock.js +10 -9
- package/dist/util/zip.js +163 -9
- package/package.json +8 -4
|
@@ -0,0 +1,1668 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runLoopForkCommand = runLoopForkCommand;
|
|
7
|
+
exports.runLoopListCommand = runLoopListCommand;
|
|
8
|
+
exports.runLoopShowCommand = runLoopShowCommand;
|
|
9
|
+
exports.runLoopPlanCommand = runLoopPlanCommand;
|
|
10
|
+
exports.runLoopNextCommand = runLoopNextCommand;
|
|
11
|
+
exports.runLoopRunsCommand = runLoopRunsCommand;
|
|
12
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
13
|
+
const fs_1 = __importDefault(require("fs"));
|
|
14
|
+
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const config_1 = require("../core/config");
|
|
16
|
+
const filesystem_authority_1 = require("../core/filesystem_authority");
|
|
17
|
+
const frontmatter_1 = require("../graph/frontmatter");
|
|
18
|
+
const indexer_1 = require("../graph/indexer");
|
|
19
|
+
const index_cache_1 = require("../graph/index_cache");
|
|
20
|
+
const loop_bindings_1 = require("../graph/loop_bindings");
|
|
21
|
+
const node_body_1 = require("../graph/node_body");
|
|
22
|
+
const reindex_1 = require("../graph/reindex");
|
|
23
|
+
const sqlite_index_1 = require("../graph/sqlite_index");
|
|
24
|
+
const date_1 = require("../util/date");
|
|
25
|
+
const refs_1 = require("../util/refs");
|
|
26
|
+
const qid_1 = require("../util/qid");
|
|
27
|
+
const lock_1 = require("../util/lock");
|
|
28
|
+
const errors_1 = require("../util/errors");
|
|
29
|
+
const node_card_1 = require("./node_card");
|
|
30
|
+
const query_output_1 = require("./query_output");
|
|
31
|
+
const event_support_1 = require("./event_support");
|
|
32
|
+
const LOOP_MATERIALIZATION_MODES = new Set(["default_children", "planning_only", "manual"]);
|
|
33
|
+
const CHILD_NODE_SPECS = [
|
|
34
|
+
{ type: "spike", prefix: "spike", titleSuffix: "grounding spike" },
|
|
35
|
+
{ type: "task", prefix: "task", titleSuffix: "execution plan" },
|
|
36
|
+
{ type: "test", prefix: "test", titleSuffix: "validation contract" },
|
|
37
|
+
];
|
|
38
|
+
function blockerContinuationGuidance(policyValue) {
|
|
39
|
+
const policy = typeof policyValue === "string" && policyValue.trim().length > 0
|
|
40
|
+
? policyValue
|
|
41
|
+
: "spike_proposal_recommendation_continue";
|
|
42
|
+
return {
|
|
43
|
+
policy,
|
|
44
|
+
branch_blocker_steps: [
|
|
45
|
+
"identify the blocked branch and affected goals/subgoals",
|
|
46
|
+
"record blocker evidence on affected nodes",
|
|
47
|
+
"create or request a source-grounded spike when uncertainty is material",
|
|
48
|
+
"request web grounding in the spike when current external facts are required",
|
|
49
|
+
"create or request a proposal for non-trivial blockers",
|
|
50
|
+
"require at least three viable proposal options",
|
|
51
|
+
"record one recommended path and rationale",
|
|
52
|
+
"mark whether the blocked branch is deferred, waiting on approval, waiting on external state, or ready for the recommendation",
|
|
53
|
+
"continue other useful scoped work when safety and ownership rules allow",
|
|
54
|
+
],
|
|
55
|
+
spike: {
|
|
56
|
+
required_when: "material uncertainty needs source-grounded or web-grounded investigation",
|
|
57
|
+
grounding: ["source", "web_when_current_external_facts_are_required"],
|
|
58
|
+
},
|
|
59
|
+
proposal: {
|
|
60
|
+
required_when: "the blocker is non-trivial or has multiple plausible paths",
|
|
61
|
+
minimum_viable_options: 3,
|
|
62
|
+
requires_recommended_path: true,
|
|
63
|
+
},
|
|
64
|
+
blocker_evidence: {
|
|
65
|
+
required_on_affected_nodes: true,
|
|
66
|
+
target_nodes: ["goal", "subgoal", "task", "loop branch"],
|
|
67
|
+
},
|
|
68
|
+
continue_strategy: "scan remaining loop scope for useful work that advances the definition of done",
|
|
69
|
+
whole_loop_blocked_threshold: "repeated or global blockers that prevent meaningful progress across remaining scope",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function blockerContinuationMarkdown(policyValue) {
|
|
73
|
+
const guidance = blockerContinuationGuidance(policyValue);
|
|
74
|
+
return [
|
|
75
|
+
"# Blocker Continuation Guidance",
|
|
76
|
+
"",
|
|
77
|
+
`Policy: ${String(guidance.policy)}`,
|
|
78
|
+
"",
|
|
79
|
+
"- Record blocker evidence on affected goals, subgoals, tasks, or loop branches.",
|
|
80
|
+
"- Create or request a source-grounded spike when uncertainty is material.",
|
|
81
|
+
"- Ask the executing agent or harness to use web grounding when current external facts are required.",
|
|
82
|
+
"- Create or request a proposal for non-trivial blockers with at least three viable paths.",
|
|
83
|
+
"- Record one recommended path and the rationale for choosing it.",
|
|
84
|
+
"- Continue other useful scoped work when safety and ownership rules allow.",
|
|
85
|
+
"- Reserve whole-loop blocked state for repeated or global blockers that prevent meaningful progress across the remaining scope.",
|
|
86
|
+
].join("\n");
|
|
87
|
+
}
|
|
88
|
+
function normalizeWorkspace(config, value) {
|
|
89
|
+
const ws = (value ?? "root").toLowerCase();
|
|
90
|
+
if (ws === "all") {
|
|
91
|
+
throw new errors_1.UsageError("--ws all is not valid for loop mutation commands");
|
|
92
|
+
}
|
|
93
|
+
if (!config.workspaces[ws]) {
|
|
94
|
+
throw new errors_1.NotFoundError(`workspace not found: ${ws}`);
|
|
95
|
+
}
|
|
96
|
+
return ws;
|
|
97
|
+
}
|
|
98
|
+
function toPosix(value) {
|
|
99
|
+
return value.split(path_1.default.sep).join("/");
|
|
100
|
+
}
|
|
101
|
+
function slugifyTitle(title) {
|
|
102
|
+
const slug = title
|
|
103
|
+
.trim()
|
|
104
|
+
.toLowerCase()
|
|
105
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
106
|
+
.replace(/^-+|-+$/g, "")
|
|
107
|
+
.replace(/-+/g, "-");
|
|
108
|
+
if (!slug) {
|
|
109
|
+
return "loop";
|
|
110
|
+
}
|
|
111
|
+
const maxLen = 80;
|
|
112
|
+
return slug.length > maxLen ? slug.slice(0, maxLen).replace(/-+$/g, "") : slug;
|
|
113
|
+
}
|
|
114
|
+
function maxIdForPrefix(index, ws, prefix) {
|
|
115
|
+
let max = 0;
|
|
116
|
+
const pattern = new RegExp(`^${prefix}-(\\d+)$`);
|
|
117
|
+
for (const node of Object.values(index)) {
|
|
118
|
+
if (node.ws !== ws) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const match = pattern.exec(node.id);
|
|
122
|
+
if (!match) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const parsed = Number.parseInt(match[1] ?? "", 10);
|
|
126
|
+
if (Number.isInteger(parsed) && parsed > max) {
|
|
127
|
+
max = parsed;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return max;
|
|
131
|
+
}
|
|
132
|
+
function idNumber(id) {
|
|
133
|
+
const match = /-(\d+)$/.exec(id);
|
|
134
|
+
return match ? Number.parseInt(match[1] ?? "0", 10) : 0;
|
|
135
|
+
}
|
|
136
|
+
function createIdAllocator(root, config, index, ws, reserve) {
|
|
137
|
+
const maxByPrefix = new Map();
|
|
138
|
+
return (prefix) => {
|
|
139
|
+
const currentMax = maxByPrefix.get(prefix) ?? maxIdForPrefix(index.nodes, ws, prefix);
|
|
140
|
+
const reserved = reserve && (0, sqlite_index_1.isSqliteBackend)(config)
|
|
141
|
+
? (0, sqlite_index_1.reserveSqliteNumericId)({ root, config, ws, prefix, currentMax })
|
|
142
|
+
: undefined;
|
|
143
|
+
const id = reserved ?? `${prefix}-${currentMax + 1}`;
|
|
144
|
+
maxByPrefix.set(prefix, Math.max(currentMax, idNumber(id)));
|
|
145
|
+
return id;
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function frontmatterString(frontmatter, key, fallback) {
|
|
149
|
+
const value = frontmatter[key];
|
|
150
|
+
return typeof value === "string" && value.trim().length > 0 ? value : fallback;
|
|
151
|
+
}
|
|
152
|
+
function frontmatterList(frontmatter, key) {
|
|
153
|
+
const value = frontmatter[key];
|
|
154
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
155
|
+
}
|
|
156
|
+
function attributeString(node, key) {
|
|
157
|
+
const value = node.attributes[key];
|
|
158
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
159
|
+
}
|
|
160
|
+
function attributeStringList(node, key) {
|
|
161
|
+
return frontmatterList(node.attributes, key);
|
|
162
|
+
}
|
|
163
|
+
function uniqueLowercase(values) {
|
|
164
|
+
return [...new Set(values.map((value) => value.trim().toLowerCase()).filter(Boolean))];
|
|
165
|
+
}
|
|
166
|
+
function relativeNodePath(root, absolutePath) {
|
|
167
|
+
return toPosix(path_1.default.relative(root, absolutePath));
|
|
168
|
+
}
|
|
169
|
+
function renderNodeFile(frontmatter, body) {
|
|
170
|
+
const normalizedBody = body.trimStart();
|
|
171
|
+
const content = ["---", ...(0, frontmatter_1.formatFrontmatter)(frontmatter), "---", normalizedBody].join("\n");
|
|
172
|
+
return content.endsWith("\n") ? content : `${content}\n`;
|
|
173
|
+
}
|
|
174
|
+
function sha256Content(content) {
|
|
175
|
+
return `sha256:${crypto_1.default.createHash("sha256").update(content).digest("hex")}`;
|
|
176
|
+
}
|
|
177
|
+
function seedSlugFromInput(input) {
|
|
178
|
+
return input
|
|
179
|
+
.trim()
|
|
180
|
+
.toLowerCase()
|
|
181
|
+
.replace(/\\/g, "/")
|
|
182
|
+
.split("/")
|
|
183
|
+
.pop()
|
|
184
|
+
.replace(/\.loop\.md$/, "")
|
|
185
|
+
.replace(/\.md$/, "");
|
|
186
|
+
}
|
|
187
|
+
function seedTemplatePath(root, config, slug) {
|
|
188
|
+
return path_1.default.resolve(root, config.templates.root_path, "loops", `${slug}.loop.md`);
|
|
189
|
+
}
|
|
190
|
+
function loadSeedTemplates(root, config) {
|
|
191
|
+
const dir = path_1.default.resolve(root, config.templates.root_path, "loops");
|
|
192
|
+
const relativeDir = relativeNodePath(root, dir);
|
|
193
|
+
if (!(0, filesystem_authority_1.containedPathExists)({ root, relativePath: relativeDir })) {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
return (0, filesystem_authority_1.withContainedTreeSink)({ root, relativePath: relativeDir, operation: "read" }, ({ absolutePath }) => fs_1.default.readdirSync(absolutePath))
|
|
197
|
+
.filter((entry) => entry.endsWith(".loop.md"))
|
|
198
|
+
.sort()
|
|
199
|
+
.map((entry) => {
|
|
200
|
+
const filePath = path_1.default.join(dir, entry);
|
|
201
|
+
const content = (0, filesystem_authority_1.readContainedFile)({ root, relativePath: relativeNodePath(root, filePath) });
|
|
202
|
+
const { frontmatter, body } = (0, frontmatter_1.parseFrontmatter)(content, filePath);
|
|
203
|
+
if (frontmatter.type !== "loop") {
|
|
204
|
+
throw new errors_1.UsageError(`seed loop template must use type: loop: ${path_1.default.relative(root, filePath)}`);
|
|
205
|
+
}
|
|
206
|
+
const slug = entry.replace(/\.loop\.md$/, "");
|
|
207
|
+
return {
|
|
208
|
+
kind: "seed",
|
|
209
|
+
ref: `template://loops/${slug}`,
|
|
210
|
+
title: frontmatterString(frontmatter, "title", slug),
|
|
211
|
+
path: relativeNodePath(root, filePath),
|
|
212
|
+
hash: sha256Content(content),
|
|
213
|
+
frontmatter,
|
|
214
|
+
body,
|
|
215
|
+
id: typeof frontmatter.id === "string" ? frontmatter.id : undefined,
|
|
216
|
+
slug,
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
function resolveLoopTemplate(root, config, index, raw, ws) {
|
|
221
|
+
const resolved = (0, qid_1.resolveQid)(index, raw, ws);
|
|
222
|
+
if (resolved.status === "ok") {
|
|
223
|
+
const node = index.nodes[resolved.qid];
|
|
224
|
+
if (!node) {
|
|
225
|
+
throw new errors_1.NotFoundError(`loop template not found: ${raw}`);
|
|
226
|
+
}
|
|
227
|
+
if (node.type !== "loop") {
|
|
228
|
+
throw new errors_1.UsageError(`template must resolve to a loop node, got ${node.type}: ${node.qid}`);
|
|
229
|
+
}
|
|
230
|
+
const filePath = path_1.default.resolve(root, node.path);
|
|
231
|
+
const content = (0, filesystem_authority_1.readContainedFile)({ root, relativePath: node.path });
|
|
232
|
+
const { frontmatter, body } = (0, frontmatter_1.parseFrontmatter)(content, filePath);
|
|
233
|
+
return {
|
|
234
|
+
kind: "node",
|
|
235
|
+
ref: node.qid,
|
|
236
|
+
title: node.title,
|
|
237
|
+
path: node.path,
|
|
238
|
+
hash: sha256Content(content),
|
|
239
|
+
frontmatter,
|
|
240
|
+
body,
|
|
241
|
+
id: node.id,
|
|
242
|
+
qid: node.qid,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
const slug = seedSlugFromInput(raw);
|
|
246
|
+
const filePath = seedTemplatePath(root, config, slug);
|
|
247
|
+
const relativeTemplatePath = relativeNodePath(root, filePath);
|
|
248
|
+
if ((0, filesystem_authority_1.containedPathExists)({ root, relativePath: relativeTemplatePath })) {
|
|
249
|
+
const content = (0, filesystem_authority_1.readContainedFile)({ root, relativePath: relativeTemplatePath });
|
|
250
|
+
const { frontmatter, body } = (0, frontmatter_1.parseFrontmatter)(content, filePath);
|
|
251
|
+
if (frontmatter.type !== "loop") {
|
|
252
|
+
throw new errors_1.UsageError(`seed loop template must use type: loop: ${path_1.default.relative(root, filePath)}`);
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
kind: "seed",
|
|
256
|
+
ref: `template://loops/${slug}`,
|
|
257
|
+
title: frontmatterString(frontmatter, "title", slug),
|
|
258
|
+
path: relativeNodePath(root, filePath),
|
|
259
|
+
hash: sha256Content(content),
|
|
260
|
+
frontmatter,
|
|
261
|
+
body,
|
|
262
|
+
id: typeof frontmatter.id === "string" ? frontmatter.id : undefined,
|
|
263
|
+
slug,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
const available = loadSeedTemplates(root, config).map((template) => template.slug).filter(Boolean);
|
|
267
|
+
if (available.length > 0) {
|
|
268
|
+
throw new errors_1.NotFoundError(`${(0, qid_1.formatResolveError)("loop template", raw, resolved, ws)}; seed templates: ${available.join(", ")}`);
|
|
269
|
+
}
|
|
270
|
+
throw new errors_1.NotFoundError((0, qid_1.formatResolveError)("loop template", raw, resolved, ws));
|
|
271
|
+
}
|
|
272
|
+
function artifactValue(node, key) {
|
|
273
|
+
const prefix = `${key}=`;
|
|
274
|
+
return node.artifacts.find((artifact) => artifact.startsWith(prefix))?.slice(prefix.length);
|
|
275
|
+
}
|
|
276
|
+
function resolveTemplateForProvenance(root, config, index, templateRef, ws) {
|
|
277
|
+
if (templateRef.startsWith("template://loops/")) {
|
|
278
|
+
return loadSeedTemplates(root, config).find((template) => template.ref === templateRef);
|
|
279
|
+
}
|
|
280
|
+
if (templateRef.includes("://")) {
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
const resolved = (0, qid_1.resolveQid)(index, templateRef, ws);
|
|
284
|
+
if (resolved.status !== "ok") {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
const node = index.nodes[resolved.qid];
|
|
288
|
+
if (!node || node.type !== "loop") {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
const filePath = path_1.default.resolve(root, node.path);
|
|
292
|
+
if (!fs_1.default.existsSync(filePath)) {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
const content = fs_1.default.readFileSync(filePath, "utf8");
|
|
296
|
+
const { frontmatter, body } = (0, frontmatter_1.parseFrontmatter)(content, filePath);
|
|
297
|
+
return {
|
|
298
|
+
kind: "node",
|
|
299
|
+
ref: node.qid,
|
|
300
|
+
title: node.title,
|
|
301
|
+
path: node.path,
|
|
302
|
+
hash: sha256Content(content),
|
|
303
|
+
frontmatter,
|
|
304
|
+
body,
|
|
305
|
+
id: node.id,
|
|
306
|
+
qid: node.qid,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function loopTemplateProvenance(root, config, index, node) {
|
|
310
|
+
const templateRefs = attributeStringList(node, "template_refs");
|
|
311
|
+
const templateRef = templateRefs.length === 1 ? templateRefs[0] : undefined;
|
|
312
|
+
const storedPath = artifactValue(node, "template_path");
|
|
313
|
+
const storedHash = artifactValue(node, "template_hash");
|
|
314
|
+
const base = {
|
|
315
|
+
template_ref: templateRef,
|
|
316
|
+
stored_path: storedPath,
|
|
317
|
+
stored_hash: storedHash,
|
|
318
|
+
path_changed: false,
|
|
319
|
+
};
|
|
320
|
+
if (!templateRef || !storedHash || templateRefs.length !== 1) {
|
|
321
|
+
return {
|
|
322
|
+
...base,
|
|
323
|
+
state: "unknown",
|
|
324
|
+
warning: templateRefs.length > 1
|
|
325
|
+
? "template provenance is unknown because the loop declares multiple template refs"
|
|
326
|
+
: "template provenance is unknown because the fork lacks one template ref and stored content hash",
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
const current = resolveTemplateForProvenance(root, config, index, templateRef, node.ws);
|
|
330
|
+
if (!current) {
|
|
331
|
+
if (templateRef.includes("://") && !templateRef.startsWith("template://loops/")) {
|
|
332
|
+
return {
|
|
333
|
+
...base,
|
|
334
|
+
state: "unknown",
|
|
335
|
+
warning: `template provenance cannot be checked for unsupported template ref ${templateRef}`,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
...base,
|
|
340
|
+
state: "missing_template",
|
|
341
|
+
warning: `template ${templateRef} is missing; the fork was not changed`,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
const pathChanged = Boolean(storedPath && storedPath !== current.path);
|
|
345
|
+
if (storedHash !== current.hash) {
|
|
346
|
+
return {
|
|
347
|
+
...base,
|
|
348
|
+
state: "stale",
|
|
349
|
+
current_path: current.path,
|
|
350
|
+
current_hash: current.hash,
|
|
351
|
+
path_changed: pathChanged,
|
|
352
|
+
warning: `template ${templateRef} content changed since this fork; re-fork or promote explicitly`,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
...base,
|
|
357
|
+
state: "current",
|
|
358
|
+
current_path: current.path,
|
|
359
|
+
current_hash: current.hash,
|
|
360
|
+
path_changed: pathChanged,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function resolveScope(index, scope, ws) {
|
|
364
|
+
const input = scope.trim();
|
|
365
|
+
if (!input) {
|
|
366
|
+
throw new errors_1.UsageError("--scope cannot be empty");
|
|
367
|
+
}
|
|
368
|
+
const resolved = (0, qid_1.resolveQid)(index, input, ws);
|
|
369
|
+
if (resolved.status === "ok") {
|
|
370
|
+
return {
|
|
371
|
+
input,
|
|
372
|
+
refs: [resolved.qid],
|
|
373
|
+
context_refs: [resolved.qid],
|
|
374
|
+
relates: [resolved.qid],
|
|
375
|
+
warnings: [],
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
const normalized = input.includes("://") ? input : input.toLowerCase();
|
|
379
|
+
if ((0, refs_1.validatePortableOrUriRef)(normalized)) {
|
|
380
|
+
const contextRefs = normalized.includes("://") ? [normalized] : [];
|
|
381
|
+
return {
|
|
382
|
+
input,
|
|
383
|
+
refs: [normalized],
|
|
384
|
+
context_refs: contextRefs,
|
|
385
|
+
relates: [],
|
|
386
|
+
warnings: [`scope ref ${normalized} was recorded but not linked because it is not resolved in the current graph`],
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
input,
|
|
391
|
+
refs: [],
|
|
392
|
+
context_refs: [],
|
|
393
|
+
relates: [],
|
|
394
|
+
warnings: ["scope was recorded as description only because it is not a portable id, qid, or URI ref"],
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function statusDefault(config) {
|
|
398
|
+
if (config.work.status_enum.includes("todo")) {
|
|
399
|
+
return "todo";
|
|
400
|
+
}
|
|
401
|
+
return config.work.status_enum[0] ?? "todo";
|
|
402
|
+
}
|
|
403
|
+
function priorityDefault(config, template) {
|
|
404
|
+
const raw = frontmatterString(template.frontmatter, "priority", String(config.work.priority_max));
|
|
405
|
+
const parsed = Number.parseInt(raw, 10);
|
|
406
|
+
if (Number.isInteger(parsed) && parsed >= config.work.priority_min && parsed <= config.work.priority_max) {
|
|
407
|
+
return parsed;
|
|
408
|
+
}
|
|
409
|
+
return config.work.priority_max;
|
|
410
|
+
}
|
|
411
|
+
function normalizeMaterializationMode(options, template) {
|
|
412
|
+
if (options.planningOnly || options.noChildren) {
|
|
413
|
+
return "planning_only";
|
|
414
|
+
}
|
|
415
|
+
const raw = (options.materializationMode ?? frontmatterString(template.frontmatter, "materialization_mode", "default_children")).toLowerCase();
|
|
416
|
+
if (!LOOP_MATERIALIZATION_MODES.has(raw)) {
|
|
417
|
+
throw new errors_1.UsageError("--materialization must be one of default_children, planning_only, manual");
|
|
418
|
+
}
|
|
419
|
+
return raw;
|
|
420
|
+
}
|
|
421
|
+
function buildLoopBody(template, options) {
|
|
422
|
+
const childSummary = options.childIds.length > 0 ? options.childIds.join(", ") : "none yet";
|
|
423
|
+
const templateBody = template.body.trimStart();
|
|
424
|
+
return [
|
|
425
|
+
"# Fork Context",
|
|
426
|
+
"",
|
|
427
|
+
`- Source template: ${template.ref}`,
|
|
428
|
+
`- Source path: ${template.path}`,
|
|
429
|
+
`- Source hash: ${template.hash}`,
|
|
430
|
+
`- Scope: ${options.scope.input}`,
|
|
431
|
+
`- Materialization mode: ${options.materializationMode}`,
|
|
432
|
+
`- Materialized child refs: ${childSummary}`,
|
|
433
|
+
"",
|
|
434
|
+
"# Template Operating Model",
|
|
435
|
+
"",
|
|
436
|
+
templateBody.length > 0 ? templateBody : "No template body was provided.",
|
|
437
|
+
"",
|
|
438
|
+
blockerContinuationMarkdown(template.frontmatter.blocker_policy),
|
|
439
|
+
].join("\n");
|
|
440
|
+
}
|
|
441
|
+
function workFilePath(root, config, ws, id, title) {
|
|
442
|
+
const wsEntry = config.workspaces[ws];
|
|
443
|
+
if (!wsEntry) {
|
|
444
|
+
throw new errors_1.NotFoundError(`workspace not found: ${ws}`);
|
|
445
|
+
}
|
|
446
|
+
return path_1.default.resolve(root, wsEntry.path, wsEntry.mdkg_dir, "work", `${id}-${slugifyTitle(title)}.md`);
|
|
447
|
+
}
|
|
448
|
+
function planLoopFork(options) {
|
|
449
|
+
const config = (0, config_1.loadConfig)(options.root);
|
|
450
|
+
const ws = normalizeWorkspace(config, options.ws);
|
|
451
|
+
const { index, stale, rebuilt, warnings: indexWarnings } = (0, index_cache_1.loadIndex)({
|
|
452
|
+
root: options.root,
|
|
453
|
+
config,
|
|
454
|
+
useCache: !options.noCache,
|
|
455
|
+
allowReindex: !options.noReindex,
|
|
456
|
+
persistReindex: !options.dryRun,
|
|
457
|
+
});
|
|
458
|
+
const warnings = [...indexWarnings];
|
|
459
|
+
if (stale && !rebuilt && !options.noCache) {
|
|
460
|
+
warnings.push("index is stale; run mdkg index to refresh");
|
|
461
|
+
}
|
|
462
|
+
const template = resolveLoopTemplate(options.root, config, index, options.template, ws);
|
|
463
|
+
const scope = resolveScope(index, options.scope, ws);
|
|
464
|
+
warnings.push(...scope.warnings);
|
|
465
|
+
const materializationMode = normalizeMaterializationMode(options, template);
|
|
466
|
+
const allocateId = createIdAllocator(options.root, config, index, ws, !options.dryRun);
|
|
467
|
+
const loopId = allocateId("loop");
|
|
468
|
+
const priority = priorityDefault(config, template);
|
|
469
|
+
const status = statusDefault(config);
|
|
470
|
+
const title = options.title?.trim() || `${template.title} for ${scope.input}`;
|
|
471
|
+
const tags = uniqueLowercase([...frontmatterList(template.frontmatter, "tags"), "loop-fork"]);
|
|
472
|
+
const skills = uniqueLowercase(frontmatterList(template.frontmatter, "skills"));
|
|
473
|
+
const today = (0, date_1.formatDate)(options.now ?? new Date());
|
|
474
|
+
const childSpecs = materializationMode === "default_children" ? CHILD_NODE_SPECS : [];
|
|
475
|
+
const children = childSpecs.map((spec) => {
|
|
476
|
+
const childId = allocateId(spec.prefix);
|
|
477
|
+
const childTitle = `${title} ${spec.titleSuffix}`;
|
|
478
|
+
const childPath = workFilePath(options.root, config, ws, childId, childTitle);
|
|
479
|
+
const childFrontmatter = {
|
|
480
|
+
id: childId,
|
|
481
|
+
type: spec.type,
|
|
482
|
+
title: childTitle,
|
|
483
|
+
status,
|
|
484
|
+
priority: String(priority),
|
|
485
|
+
parent: loopId,
|
|
486
|
+
tags: uniqueLowercase([...tags, "loop-child", spec.type]),
|
|
487
|
+
owners: [],
|
|
488
|
+
links: [],
|
|
489
|
+
artifacts: [],
|
|
490
|
+
relates: [loopId],
|
|
491
|
+
blocked_by: [],
|
|
492
|
+
blocks: [],
|
|
493
|
+
refs: [loopId, template.ref],
|
|
494
|
+
context_refs: scope.context_refs,
|
|
495
|
+
evidence_refs: [],
|
|
496
|
+
aliases: [],
|
|
497
|
+
skills,
|
|
498
|
+
created: today,
|
|
499
|
+
updated: today,
|
|
500
|
+
};
|
|
501
|
+
if (spec.type === "test") {
|
|
502
|
+
childFrontmatter.cases = [];
|
|
503
|
+
}
|
|
504
|
+
const body = childBody(spec.type, title, scope.input, template.ref);
|
|
505
|
+
return nodePlan(ws, childId, spec.type, childTitle, status, priority, childPath, childFrontmatter, body, options.root);
|
|
506
|
+
});
|
|
507
|
+
const childRefs = children.map((child) => child.id);
|
|
508
|
+
const loopPath = workFilePath(options.root, config, ws, loopId, title);
|
|
509
|
+
const loopFrontmatter = {
|
|
510
|
+
id: loopId,
|
|
511
|
+
type: "loop",
|
|
512
|
+
title,
|
|
513
|
+
status,
|
|
514
|
+
priority: String(priority),
|
|
515
|
+
loop_mode: frontmatterString(template.frontmatter, "loop_mode", "planning"),
|
|
516
|
+
loop_role: "scoped",
|
|
517
|
+
scope_refs: scope.refs,
|
|
518
|
+
scope_description: scope.input,
|
|
519
|
+
template_refs: [template.ref],
|
|
520
|
+
materialization_mode: materializationMode,
|
|
521
|
+
child_refs: childRefs,
|
|
522
|
+
pre_run_questions: frontmatterList(template.frontmatter, "pre_run_questions"),
|
|
523
|
+
question_answer_refs: [],
|
|
524
|
+
pre_approved_actions: frontmatterList(template.frontmatter, "pre_approved_actions"),
|
|
525
|
+
approval_gated_actions: frontmatterList(template.frontmatter, "approval_gated_actions"),
|
|
526
|
+
required_actions: frontmatterList(template.frontmatter, "required_actions"),
|
|
527
|
+
requested_actions: frontmatterList(template.frontmatter, "requested_actions"),
|
|
528
|
+
prohibited_actions: frontmatterList(template.frontmatter, "prohibited_actions"),
|
|
529
|
+
action_approval_refs: [],
|
|
530
|
+
evidence_lanes: frontmatterList(template.frontmatter, "evidence_lanes"),
|
|
531
|
+
evidence_lane_refs: [],
|
|
532
|
+
lane_waiver_refs: [],
|
|
533
|
+
lane_waiver_decision_refs: [],
|
|
534
|
+
lane_waiver_approval_refs: [],
|
|
535
|
+
run_refs: [],
|
|
536
|
+
decision_refs: [],
|
|
537
|
+
output_refs: [],
|
|
538
|
+
approval_refs: [],
|
|
539
|
+
evaluation_refs: children.filter((child) => child.type === "test").map((child) => child.id),
|
|
540
|
+
definition_of_done: frontmatterString(template.frontmatter, "definition_of_done", "Loop reaches its definition of done with source-grounded evidence and linked follow-up nodes."),
|
|
541
|
+
blocker_policy: frontmatterString(template.frontmatter, "blocker_policy", "spike_proposal_recommendation_continue"),
|
|
542
|
+
tags,
|
|
543
|
+
owners: uniqueLowercase(frontmatterList(template.frontmatter, "owners")),
|
|
544
|
+
links: frontmatterList(template.frontmatter, "links"),
|
|
545
|
+
artifacts: [`template_path=${template.path}`, `template_hash=${template.hash}`],
|
|
546
|
+
relates: scope.relates,
|
|
547
|
+
blocked_by: [],
|
|
548
|
+
blocks: [],
|
|
549
|
+
refs: [template.ref],
|
|
550
|
+
context_refs: scope.context_refs,
|
|
551
|
+
evidence_refs: [],
|
|
552
|
+
aliases: [],
|
|
553
|
+
skills,
|
|
554
|
+
created: today,
|
|
555
|
+
updated: today,
|
|
556
|
+
};
|
|
557
|
+
const loopBody = buildLoopBody(template, {
|
|
558
|
+
scope,
|
|
559
|
+
materializationMode,
|
|
560
|
+
childIds: childRefs,
|
|
561
|
+
});
|
|
562
|
+
const loop = nodePlan(ws, loopId, "loop", title, status, priority, loopPath, loopFrontmatter, loopBody, options.root);
|
|
563
|
+
return {
|
|
564
|
+
config,
|
|
565
|
+
index,
|
|
566
|
+
ws,
|
|
567
|
+
template,
|
|
568
|
+
scope,
|
|
569
|
+
loop,
|
|
570
|
+
children,
|
|
571
|
+
materializationMode,
|
|
572
|
+
warnings,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
function childBody(type, loopTitle, scope, templateRef) {
|
|
576
|
+
if (type === "spike") {
|
|
577
|
+
return [
|
|
578
|
+
"# Research Question",
|
|
579
|
+
"",
|
|
580
|
+
`What source-grounded context, constraints, risks, and viable options should ${loopTitle} use for ${scope}?`,
|
|
581
|
+
"",
|
|
582
|
+
"# Search Plan",
|
|
583
|
+
"",
|
|
584
|
+
"- Inspect mdkg context before broad source exploration.",
|
|
585
|
+
"- Use source and web grounding when the loop hits a blocker.",
|
|
586
|
+
"",
|
|
587
|
+
"# Findings",
|
|
588
|
+
"",
|
|
589
|
+
"# Recommendation",
|
|
590
|
+
"",
|
|
591
|
+
"# Follow-Up Nodes To Create",
|
|
592
|
+
"",
|
|
593
|
+
"# Skill Candidates",
|
|
594
|
+
"",
|
|
595
|
+
"# Evidence And Sources",
|
|
596
|
+
"",
|
|
597
|
+
`Template: ${templateRef}`,
|
|
598
|
+
].join("\n");
|
|
599
|
+
}
|
|
600
|
+
if (type === "test") {
|
|
601
|
+
return [
|
|
602
|
+
"# Test Contract",
|
|
603
|
+
"",
|
|
604
|
+
`Validate that ${loopTitle} reaches its definition of done for ${scope}.`,
|
|
605
|
+
"",
|
|
606
|
+
"# Cases",
|
|
607
|
+
"",
|
|
608
|
+
"- Loop and linked child nodes are discoverable.",
|
|
609
|
+
"- Evidence and follow-up work are linked back to the scoped loop.",
|
|
610
|
+
"- Blockers route to spike/proposal/recommendation guidance instead of early hard-stop.",
|
|
611
|
+
"",
|
|
612
|
+
"# Evidence",
|
|
613
|
+
"",
|
|
614
|
+
`Template: ${templateRef}`,
|
|
615
|
+
].join("\n");
|
|
616
|
+
}
|
|
617
|
+
return [
|
|
618
|
+
"# Overview",
|
|
619
|
+
"",
|
|
620
|
+
`Plan and coordinate execution work for ${loopTitle} over ${scope}.`,
|
|
621
|
+
"",
|
|
622
|
+
"# Acceptance Criteria",
|
|
623
|
+
"",
|
|
624
|
+
"- Work remains scoped to the loop definition of done.",
|
|
625
|
+
"- Outputs, evidence, and follow-up nodes are linked to the loop.",
|
|
626
|
+
"- If blocked, the loop records blocker evidence and continues useful scoped work where possible.",
|
|
627
|
+
"",
|
|
628
|
+
"# Test Plan",
|
|
629
|
+
"",
|
|
630
|
+
`Template: ${templateRef}`,
|
|
631
|
+
].join("\n");
|
|
632
|
+
}
|
|
633
|
+
function nodePlan(ws, id, type, title, status, priority, absolutePath, frontmatter, body, root) {
|
|
634
|
+
return {
|
|
635
|
+
workspace: ws,
|
|
636
|
+
id,
|
|
637
|
+
qid: `${ws}:${id}`,
|
|
638
|
+
path: relativeNodePath(root, absolutePath),
|
|
639
|
+
type,
|
|
640
|
+
title,
|
|
641
|
+
status,
|
|
642
|
+
priority,
|
|
643
|
+
absolutePath,
|
|
644
|
+
frontmatter,
|
|
645
|
+
body,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
function writePlannedNode(root, node) {
|
|
649
|
+
try {
|
|
650
|
+
(0, filesystem_authority_1.writeContainedFileExclusive)({ root, relativePath: node.path }, renderNodeFile(node.frontmatter, node.body));
|
|
651
|
+
}
|
|
652
|
+
catch (err) {
|
|
653
|
+
const code = typeof err === "object" && err !== null && "code" in err ? String(err.code) : "";
|
|
654
|
+
if (code === "EEXIST") {
|
|
655
|
+
throw new errors_1.UsageError(`node already exists: ${node.path}`);
|
|
656
|
+
}
|
|
657
|
+
throw err;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
function receiptNode(node) {
|
|
661
|
+
return {
|
|
662
|
+
workspace: node.workspace,
|
|
663
|
+
id: node.id,
|
|
664
|
+
qid: node.qid,
|
|
665
|
+
path: node.path,
|
|
666
|
+
type: node.type,
|
|
667
|
+
title: node.title,
|
|
668
|
+
status: node.status,
|
|
669
|
+
priority: node.priority,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
function forkReadinessRequirements(frontmatter, loopId) {
|
|
673
|
+
const questions = frontmatterList(frontmatter, "pre_run_questions");
|
|
674
|
+
const preApprovedActions = frontmatterList(frontmatter, "pre_approved_actions");
|
|
675
|
+
const approvalGatedActions = frontmatterList(frontmatter, "approval_gated_actions");
|
|
676
|
+
const requiredActions = frontmatterList(frontmatter, "required_actions");
|
|
677
|
+
const requestedActions = frontmatterList(frontmatter, "requested_actions");
|
|
678
|
+
const prohibitedActions = frontmatterList(frontmatter, "prohibited_actions");
|
|
679
|
+
const requiredActionSet = new Set(requiredActions);
|
|
680
|
+
const requestedActionSet = new Set(requestedActions);
|
|
681
|
+
const evidenceLanes = frontmatterList(frontmatter, "evidence_lanes");
|
|
682
|
+
const nextActions = [];
|
|
683
|
+
if (questions.length > 0) {
|
|
684
|
+
nextActions.push("answer each pre-run question with an accepted decision and bind it by identity in question_answer_refs");
|
|
685
|
+
}
|
|
686
|
+
if (approvalGatedActions.some((action) => requiredActionSet.has(action) || requestedActionSet.has(action))) {
|
|
687
|
+
nextActions.push("record verified action_approval_refs for required or requested approval-gated actions");
|
|
688
|
+
}
|
|
689
|
+
nextActions.push(`inspect the scoped readiness projection with mdkg loop plan ${loopId} --json`);
|
|
690
|
+
return {
|
|
691
|
+
pre_run_questions: questions,
|
|
692
|
+
required_decisions: questions.map((identity) => ({
|
|
693
|
+
identity,
|
|
694
|
+
binding_field: "question_answer_refs",
|
|
695
|
+
required_evidence: "accepted decision ref",
|
|
696
|
+
})),
|
|
697
|
+
actions: {
|
|
698
|
+
pre_approved: preApprovedActions,
|
|
699
|
+
approval_gated: approvalGatedActions.map((identity) => ({
|
|
700
|
+
identity,
|
|
701
|
+
required: requiredActionSet.has(identity),
|
|
702
|
+
requested: requestedActionSet.has(identity),
|
|
703
|
+
binding_field: "action_approval_refs",
|
|
704
|
+
})),
|
|
705
|
+
required: requiredActions,
|
|
706
|
+
requested: requestedActions,
|
|
707
|
+
prohibited: prohibitedActions,
|
|
708
|
+
},
|
|
709
|
+
evidence_lanes: evidenceLanes.map((identity) => ({
|
|
710
|
+
identity,
|
|
711
|
+
evidence_binding_field: "evidence_lane_refs",
|
|
712
|
+
waiver_decision_binding_field: "lane_waiver_decision_refs",
|
|
713
|
+
waiver_approval_binding_field: "lane_waiver_approval_refs",
|
|
714
|
+
})),
|
|
715
|
+
next_actions: nextActions,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
function forkReceipt(plan, dryRun) {
|
|
719
|
+
const pending = plan.materializationMode === "default_children"
|
|
720
|
+
? []
|
|
721
|
+
: [
|
|
722
|
+
"child materialization is pending; rerun loop fork with --materialization default_children or create linked child nodes explicitly",
|
|
723
|
+
];
|
|
724
|
+
return {
|
|
725
|
+
action: dryRun ? "planned" : "forked",
|
|
726
|
+
dry_run: dryRun,
|
|
727
|
+
template: {
|
|
728
|
+
kind: plan.template.kind,
|
|
729
|
+
ref: plan.template.ref,
|
|
730
|
+
id: plan.template.id,
|
|
731
|
+
qid: plan.template.qid,
|
|
732
|
+
path: plan.template.path,
|
|
733
|
+
hash: plan.template.hash,
|
|
734
|
+
},
|
|
735
|
+
scope: plan.scope,
|
|
736
|
+
loop: receiptNode(plan.loop),
|
|
737
|
+
materialization_mode: plan.materializationMode,
|
|
738
|
+
materialized_children: plan.children.map(receiptNode),
|
|
739
|
+
pending_materialization: pending,
|
|
740
|
+
readiness_requirements: forkReadinessRequirements(plan.loop.frontmatter, plan.loop.id),
|
|
741
|
+
blocker_continuation: blockerContinuationGuidance(plan.loop.frontmatter.blocker_policy),
|
|
742
|
+
warnings: plan.warnings,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
function emitForkReceipt(plan, dryRun, json) {
|
|
746
|
+
const receipt = forkReceipt(plan, dryRun);
|
|
747
|
+
if (json) {
|
|
748
|
+
(0, query_output_1.writeJson)(receipt);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const action = dryRun ? "planned loop fork" : "loop forked";
|
|
752
|
+
console.log(`${action}: ${plan.loop.qid} (${plan.loop.path})`);
|
|
753
|
+
console.log(`template: ${plan.template.ref}`);
|
|
754
|
+
console.log(`scope: ${plan.scope.input}`);
|
|
755
|
+
console.log(`materialization: ${plan.materializationMode}`);
|
|
756
|
+
if (plan.children.length > 0) {
|
|
757
|
+
console.log(`children: ${plan.children.map((child) => child.qid).join(", ")}`);
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
console.log("children: none");
|
|
761
|
+
}
|
|
762
|
+
const questions = frontmatterList(plan.loop.frontmatter, "pre_run_questions");
|
|
763
|
+
console.log(`pre-run questions: ${questions.length > 0 ? questions.join(", ") : "none"}`);
|
|
764
|
+
console.log(`next: mdkg loop plan ${plan.loop.id} --json`);
|
|
765
|
+
for (const warning of plan.warnings) {
|
|
766
|
+
console.error(`warning: ${warning}`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
function runLoopForkCommand(options) {
|
|
770
|
+
if (!options.template.trim()) {
|
|
771
|
+
throw new errors_1.UsageError("loop fork requires a template");
|
|
772
|
+
}
|
|
773
|
+
if (!options.scope.trim()) {
|
|
774
|
+
throw new errors_1.UsageError("loop fork requires --scope");
|
|
775
|
+
}
|
|
776
|
+
const execute = () => {
|
|
777
|
+
const plan = planLoopFork(options);
|
|
778
|
+
if (options.dryRun) {
|
|
779
|
+
emitForkReceipt(plan, true, options.json);
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
writePlannedNode(options.root, plan.loop);
|
|
783
|
+
for (const child of plan.children) {
|
|
784
|
+
writePlannedNode(options.root, child);
|
|
785
|
+
}
|
|
786
|
+
if (plan.config.index.auto_reindex && !options.noReindex) {
|
|
787
|
+
const updatedIndex = (0, indexer_1.buildIndex)(options.root, plan.config, { tolerant: plan.config.index.tolerant });
|
|
788
|
+
(0, reindex_1.writeDerivedIndexes)(options.root, plan.config, updatedIndex);
|
|
789
|
+
}
|
|
790
|
+
const refs = [plan.loop.id, ...plan.children.map((child) => child.id)];
|
|
791
|
+
(0, event_support_1.appendAutomaticEvent)({
|
|
792
|
+
root: options.root,
|
|
793
|
+
ws: plan.ws,
|
|
794
|
+
kind: "LOOP_FORKED",
|
|
795
|
+
status: "ok",
|
|
796
|
+
refs,
|
|
797
|
+
artifacts: [plan.template.ref, plan.template.hash],
|
|
798
|
+
notes: `loop forked from ${plan.template.ref}`,
|
|
799
|
+
runId: options.runId,
|
|
800
|
+
now: options.now,
|
|
801
|
+
});
|
|
802
|
+
emitForkReceipt(plan, false, options.json);
|
|
803
|
+
};
|
|
804
|
+
if (options.dryRun) {
|
|
805
|
+
execute();
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
const config = (0, config_1.loadConfig)(options.root);
|
|
809
|
+
(0, lock_1.withMutationLock)(options.root, config.index.lock_timeout_ms, execute);
|
|
810
|
+
}
|
|
811
|
+
function loadLoopIndex(options) {
|
|
812
|
+
const config = (0, config_1.loadConfig)(options.root);
|
|
813
|
+
const ws = options.ws && options.ws !== "all" ? options.ws.toLowerCase() : undefined;
|
|
814
|
+
if (ws && !config.workspaces[ws] && !config.subgraphs[ws]) {
|
|
815
|
+
throw new errors_1.NotFoundError(`workspace not found: ${ws}`);
|
|
816
|
+
}
|
|
817
|
+
const { index, rebuilt, stale, warnings } = (0, index_cache_1.loadIndex)({
|
|
818
|
+
root: options.root,
|
|
819
|
+
config,
|
|
820
|
+
useCache: !options.noCache,
|
|
821
|
+
allowReindex: !options.noReindex,
|
|
822
|
+
persistReindex: false,
|
|
823
|
+
});
|
|
824
|
+
const outputWarnings = [...warnings];
|
|
825
|
+
if (stale && !rebuilt && !options.noCache) {
|
|
826
|
+
outputWarnings.push("index is stale; run mdkg index to refresh");
|
|
827
|
+
}
|
|
828
|
+
return { config, index, ws, warnings: outputWarnings };
|
|
829
|
+
}
|
|
830
|
+
function resolveLoopNode(index, id, ws) {
|
|
831
|
+
const resolved = (0, qid_1.resolveQid)(index, id, ws);
|
|
832
|
+
if (resolved.status !== "ok") {
|
|
833
|
+
throw new errors_1.NotFoundError((0, qid_1.formatResolveError)("loop", id, resolved, ws));
|
|
834
|
+
}
|
|
835
|
+
const node = index.nodes[resolved.qid];
|
|
836
|
+
if (!node) {
|
|
837
|
+
throw new errors_1.NotFoundError(`loop not found: ${id}`);
|
|
838
|
+
}
|
|
839
|
+
if (node.type !== "loop") {
|
|
840
|
+
throw new errors_1.UsageError(`expected loop node, got ${node.type}: ${node.qid}`);
|
|
841
|
+
}
|
|
842
|
+
return node;
|
|
843
|
+
}
|
|
844
|
+
function visibleLoopNodes(index, ws) {
|
|
845
|
+
return Object.values(index.nodes)
|
|
846
|
+
.filter((node) => node.type === "loop" && (!ws || node.ws === ws))
|
|
847
|
+
.sort((a, b) => a.qid.localeCompare(b.qid));
|
|
848
|
+
}
|
|
849
|
+
function qidForLoopRef(loop, ref) {
|
|
850
|
+
if (ref.includes("://")) {
|
|
851
|
+
return undefined;
|
|
852
|
+
}
|
|
853
|
+
return ref.includes(":") ? ref : `${loop.ws}:${ref}`;
|
|
854
|
+
}
|
|
855
|
+
function resolveLoopRefNode(index, loop, ref) {
|
|
856
|
+
const qid = qidForLoopRef(loop, ref);
|
|
857
|
+
return qid ? index.nodes[qid] : undefined;
|
|
858
|
+
}
|
|
859
|
+
function isAcceptedDecisionRef(index, loop, ref) {
|
|
860
|
+
const target = resolveLoopRefNode(index, loop, ref);
|
|
861
|
+
return target?.type === "dec" && target.status === "accepted";
|
|
862
|
+
}
|
|
863
|
+
function isVerifiedApprovalRef(index, loop, ref) {
|
|
864
|
+
if (ref.includes("://")) {
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
const target = resolveLoopRefNode(index, loop, ref);
|
|
868
|
+
if (!target) {
|
|
869
|
+
return false;
|
|
870
|
+
}
|
|
871
|
+
if (target.type === "dec") {
|
|
872
|
+
return target.status === "accepted";
|
|
873
|
+
}
|
|
874
|
+
if (target.type === "checkpoint") {
|
|
875
|
+
return target.status === "done";
|
|
876
|
+
}
|
|
877
|
+
return target.type === "receipt" && target.attributes.receipt_status === "verified";
|
|
878
|
+
}
|
|
879
|
+
function isExistingEvidenceRef(index, loop, ref) {
|
|
880
|
+
if (ref.includes("://")) {
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
const target = resolveLoopRefNode(index, loop, ref);
|
|
884
|
+
if (!target)
|
|
885
|
+
return false;
|
|
886
|
+
if (target.type === "receipt")
|
|
887
|
+
return target.attributes.receipt_status === "verified";
|
|
888
|
+
if (target.type === "dec")
|
|
889
|
+
return target.status === "accepted";
|
|
890
|
+
return target.status === "done" || target.attributes.goal_state === "achieved";
|
|
891
|
+
}
|
|
892
|
+
function isOwnedLoopChild(loop, child) {
|
|
893
|
+
return child.edges.parent === loop.qid || child.edges.epic === loop.qid || child.edges.relates.includes(loop.qid);
|
|
894
|
+
}
|
|
895
|
+
function explicitBindingMap(node, key) {
|
|
896
|
+
return (0, loop_bindings_1.groupLoopRefBindings)((0, loop_bindings_1.parseLoopRefBindings)(attributeStringList(node, key)));
|
|
897
|
+
}
|
|
898
|
+
function requirementRefs(identity, identities, explicit, legacyRefs, isValid) {
|
|
899
|
+
if (explicit.size > 0) {
|
|
900
|
+
const refs = (explicit.get(identity) ?? []).filter(isValid);
|
|
901
|
+
return { refs, source: refs.length > 0 ? "explicit" : "none" };
|
|
902
|
+
}
|
|
903
|
+
if (identities.length === 1) {
|
|
904
|
+
const refs = legacyRefs.filter(isValid);
|
|
905
|
+
return { refs, source: refs.length > 0 ? "legacy_unambiguous" : "none" };
|
|
906
|
+
}
|
|
907
|
+
return { refs: [], source: "none" };
|
|
908
|
+
}
|
|
909
|
+
function invalidBindingDiagnostics(node, key, isValid) {
|
|
910
|
+
return (0, loop_bindings_1.parseLoopRefBindings)(attributeStringList(node, key))
|
|
911
|
+
.filter((binding) => !isValid(binding.ref))
|
|
912
|
+
.map((binding) => `${key} ${binding.identity} references missing, wrong-kind, or incomplete evidence ${binding.ref}`);
|
|
913
|
+
}
|
|
914
|
+
function childLaneState(child) {
|
|
915
|
+
if (!child) {
|
|
916
|
+
return "missing";
|
|
917
|
+
}
|
|
918
|
+
if (child.status === "done" || child.attributes.goal_state === "achieved") {
|
|
919
|
+
return "completed";
|
|
920
|
+
}
|
|
921
|
+
if (child.status === "blocked" || child.edges.blocked_by.length > 0) {
|
|
922
|
+
return "blocked";
|
|
923
|
+
}
|
|
924
|
+
if (child.status === "review" || child.status === "waiting") {
|
|
925
|
+
return "waiting";
|
|
926
|
+
}
|
|
927
|
+
return "actionable";
|
|
928
|
+
}
|
|
929
|
+
function incrementLaneCount(counts, state) {
|
|
930
|
+
counts[state] += 1;
|
|
931
|
+
}
|
|
932
|
+
function buildLoopReadinessProjection(root, config, index, node) {
|
|
933
|
+
const childRefs = attributeStringList(node, "child_refs");
|
|
934
|
+
const runRefs = attributeStringList(node, "run_refs");
|
|
935
|
+
const outputRefs = attributeStringList(node, "output_refs");
|
|
936
|
+
const evaluationRefs = attributeStringList(node, "evaluation_refs");
|
|
937
|
+
const laneWaiverRefs = attributeStringList(node, "lane_waiver_refs");
|
|
938
|
+
const decisionRefs = attributeStringList(node, "decision_refs");
|
|
939
|
+
const approvalRefs = attributeStringList(node, "approval_refs");
|
|
940
|
+
const preRunQuestions = attributeStringList(node, "pre_run_questions");
|
|
941
|
+
const preApprovedActions = attributeStringList(node, "pre_approved_actions");
|
|
942
|
+
const approvalGatedActions = attributeStringList(node, "approval_gated_actions");
|
|
943
|
+
const declaredRequiredActions = attributeStringList(node, "required_actions");
|
|
944
|
+
const declaredRequestedActions = attributeStringList(node, "requested_actions");
|
|
945
|
+
const requiredActions = Array.isArray(node.attributes.required_actions)
|
|
946
|
+
? declaredRequiredActions
|
|
947
|
+
: [...preApprovedActions];
|
|
948
|
+
const requestedActions = Array.isArray(node.attributes.requested_actions)
|
|
949
|
+
? declaredRequestedActions
|
|
950
|
+
: [...requiredActions];
|
|
951
|
+
const prohibitedActions = attributeStringList(node, "prohibited_actions");
|
|
952
|
+
const evidenceLaneNames = attributeStringList(node, "evidence_lanes");
|
|
953
|
+
const evidenceRefs = node.edges.evidence_refs ?? [];
|
|
954
|
+
const materializationMode = attributeString(node, "materialization_mode");
|
|
955
|
+
const pendingMaterialization = childRefs.length === 0
|
|
956
|
+
? ["no child refs are materialized yet; fork with default_children or create linked child nodes explicitly"]
|
|
957
|
+
: [];
|
|
958
|
+
const questionAnswerBindings = explicitBindingMap(node, "question_answer_refs");
|
|
959
|
+
const actionApprovalBindings = explicitBindingMap(node, "action_approval_refs");
|
|
960
|
+
const evidenceLaneBindings = explicitBindingMap(node, "evidence_lane_refs");
|
|
961
|
+
const waiverDecisionBindings = explicitBindingMap(node, "lane_waiver_decision_refs");
|
|
962
|
+
const waiverApprovalBindings = explicitBindingMap(node, "lane_waiver_approval_refs");
|
|
963
|
+
const validDecision = (ref) => isAcceptedDecisionRef(index, node, ref);
|
|
964
|
+
const validApproval = (ref) => isVerifiedApprovalRef(index, node, ref);
|
|
965
|
+
const validEvidence = (ref) => isExistingEvidenceRef(index, node, ref);
|
|
966
|
+
const questionItems = preRunQuestions.map((id) => {
|
|
967
|
+
const match = requirementRefs(id, preRunQuestions, questionAnswerBindings, decisionRefs, validDecision);
|
|
968
|
+
return {
|
|
969
|
+
id,
|
|
970
|
+
state: match.refs.length > 0 ? "answered" : "unanswered",
|
|
971
|
+
decision_refs: match.refs,
|
|
972
|
+
binding_source: match.source,
|
|
973
|
+
};
|
|
974
|
+
});
|
|
975
|
+
const unansweredPreRunQuestions = questionItems
|
|
976
|
+
.filter((item) => item.state === "unanswered")
|
|
977
|
+
.map((item) => item.id);
|
|
978
|
+
const requiredActionSet = new Set(requiredActions);
|
|
979
|
+
const requestedActionSet = new Set(requestedActions);
|
|
980
|
+
const actionItems = approvalGatedActions.map((id) => {
|
|
981
|
+
const match = requirementRefs(id, approvalGatedActions, actionApprovalBindings, approvalRefs, validApproval);
|
|
982
|
+
const required = requiredActionSet.has(id);
|
|
983
|
+
const requested = requestedActionSet.has(id);
|
|
984
|
+
return {
|
|
985
|
+
id,
|
|
986
|
+
required,
|
|
987
|
+
requested,
|
|
988
|
+
state: match.refs.length > 0
|
|
989
|
+
? "approved"
|
|
990
|
+
: required || requested
|
|
991
|
+
? "pending"
|
|
992
|
+
: "optional",
|
|
993
|
+
approval_refs: match.refs,
|
|
994
|
+
binding_source: match.source,
|
|
995
|
+
};
|
|
996
|
+
});
|
|
997
|
+
const pendingApprovalActions = actionItems
|
|
998
|
+
.filter((item) => item.state === "pending")
|
|
999
|
+
.map((item) => item.id);
|
|
1000
|
+
const allAvailableActions = [...new Set([...preApprovedActions, ...approvalGatedActions])];
|
|
1001
|
+
const optionalActions = allAvailableActions.filter((action) => !requiredActionSet.has(action) && !requestedActionSet.has(action));
|
|
1002
|
+
const children = childRefs.map((ref) => {
|
|
1003
|
+
const child = resolveLoopRefNode(index, node, ref);
|
|
1004
|
+
const owned = child ? isOwnedLoopChild(node, child) : false;
|
|
1005
|
+
const state = owned ? childLaneState(child) : "missing";
|
|
1006
|
+
return {
|
|
1007
|
+
ref,
|
|
1008
|
+
qid: child?.qid ?? qidForLoopRef(node, ref),
|
|
1009
|
+
type: child?.type,
|
|
1010
|
+
title: child?.title,
|
|
1011
|
+
status: child?.status,
|
|
1012
|
+
state,
|
|
1013
|
+
owned,
|
|
1014
|
+
blocked_by: child ? [...child.edges.blocked_by] : [],
|
|
1015
|
+
};
|
|
1016
|
+
});
|
|
1017
|
+
const evidenceCandidates = [...new Set([...runRefs, ...outputRefs, ...evaluationRefs, ...evidenceRefs])];
|
|
1018
|
+
const hasExplicitWaiverBindings = waiverDecisionBindings.size > 0 || waiverApprovalBindings.size > 0;
|
|
1019
|
+
const evidence = evidenceLaneNames.map((name) => {
|
|
1020
|
+
const evidenceMatch = requirementRefs(name, evidenceLaneNames, evidenceLaneBindings, evidenceCandidates, validEvidence);
|
|
1021
|
+
const waiverDecisionMatch = requirementRefs(name, evidenceLaneNames, waiverDecisionBindings, laneWaiverRefs.filter((ref) => decisionRefs.includes(ref)), validDecision);
|
|
1022
|
+
const waiverApprovalMatch = requirementRefs(name, evidenceLaneNames, waiverApprovalBindings, hasExplicitWaiverBindings || laneWaiverRefs.length === 0 ? [] : approvalRefs, validApproval);
|
|
1023
|
+
const waived = waiverDecisionMatch.refs.length > 0 && waiverApprovalMatch.refs.length > 0;
|
|
1024
|
+
return {
|
|
1025
|
+
name,
|
|
1026
|
+
state: waived
|
|
1027
|
+
? "waived"
|
|
1028
|
+
: evidenceMatch.refs.length > 0
|
|
1029
|
+
? "completed"
|
|
1030
|
+
: "waiting",
|
|
1031
|
+
evidence_refs: evidenceMatch.refs,
|
|
1032
|
+
waiver_refs: [...new Set([...waiverDecisionMatch.refs, ...waiverApprovalMatch.refs])],
|
|
1033
|
+
waiver_decision_refs: waiverDecisionMatch.refs,
|
|
1034
|
+
waiver_approval_refs: waiverApprovalMatch.refs,
|
|
1035
|
+
binding_source: waived
|
|
1036
|
+
? waiverDecisionMatch.source === "explicit" || waiverApprovalMatch.source === "explicit"
|
|
1037
|
+
? "explicit"
|
|
1038
|
+
: "legacy_unambiguous"
|
|
1039
|
+
: evidenceMatch.source,
|
|
1040
|
+
};
|
|
1041
|
+
});
|
|
1042
|
+
const invalidBindings = [
|
|
1043
|
+
...invalidBindingDiagnostics(node, "question_answer_refs", validDecision),
|
|
1044
|
+
...invalidBindingDiagnostics(node, "action_approval_refs", validApproval),
|
|
1045
|
+
...invalidBindingDiagnostics(node, "evidence_lane_refs", validEvidence),
|
|
1046
|
+
...invalidBindingDiagnostics(node, "lane_waiver_decision_refs", validDecision),
|
|
1047
|
+
...invalidBindingDiagnostics(node, "lane_waiver_approval_refs", validApproval),
|
|
1048
|
+
];
|
|
1049
|
+
for (const lane of evidence) {
|
|
1050
|
+
const hasWaiverDecision = waiverDecisionBindings.has(lane.name);
|
|
1051
|
+
const hasWaiverApproval = waiverApprovalBindings.has(lane.name);
|
|
1052
|
+
if (hasWaiverDecision !== hasWaiverApproval) {
|
|
1053
|
+
invalidBindings.push(`lane ${lane.name} waiver requires both decision and approval bindings`);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
if (evidenceLaneNames.length > 1 && laneWaiverRefs.length > 0 && !hasExplicitWaiverBindings) {
|
|
1057
|
+
invalidBindings.push("legacy lane_waiver_refs are ambiguous across multiple evidence lanes; add typed waiver bindings");
|
|
1058
|
+
}
|
|
1059
|
+
const counts = {
|
|
1060
|
+
completed: 0,
|
|
1061
|
+
blocked: 0,
|
|
1062
|
+
waiting: 0,
|
|
1063
|
+
waived: 0,
|
|
1064
|
+
actionable: 0,
|
|
1065
|
+
missing: 0,
|
|
1066
|
+
};
|
|
1067
|
+
for (const child of children) {
|
|
1068
|
+
incrementLaneCount(counts, child.state);
|
|
1069
|
+
}
|
|
1070
|
+
for (const lane of evidence) {
|
|
1071
|
+
incrementLaneCount(counts, lane.state);
|
|
1072
|
+
}
|
|
1073
|
+
const blockedChildren = children.filter((child) => child.state === "blocked").map((child) => child.qid ?? child.ref);
|
|
1074
|
+
const waitingChildren = children.filter((child) => child.state === "waiting").map((child) => child.qid ?? child.ref);
|
|
1075
|
+
const actionableChildren = children.filter((child) => child.state === "actionable").map((child) => child.qid ?? child.ref);
|
|
1076
|
+
const missingChildren = children.filter((child) => child.state === "missing").map((child) => child.ref);
|
|
1077
|
+
const waitingEvidenceLanes = evidence.filter((lane) => lane.state === "waiting").map((lane) => lane.name);
|
|
1078
|
+
const missing = [];
|
|
1079
|
+
if (invalidBindings.length > 0) {
|
|
1080
|
+
missing.push("repair invalid or incomplete readiness bindings");
|
|
1081
|
+
}
|
|
1082
|
+
if (pendingMaterialization.length > 0 && materializationMode !== "planning_only") {
|
|
1083
|
+
missing.push("materialize child lanes or intentionally use planning-only materialization");
|
|
1084
|
+
}
|
|
1085
|
+
if (unansweredPreRunQuestions.length > 0) {
|
|
1086
|
+
missing.push("answer pre-run questions through typed question decision refs");
|
|
1087
|
+
}
|
|
1088
|
+
if (pendingApprovalActions.length > 0) {
|
|
1089
|
+
missing.push("record typed approval refs for required or requested approval-gated actions");
|
|
1090
|
+
}
|
|
1091
|
+
if (node.edges.blocked_by.length > 0) {
|
|
1092
|
+
missing.push("resolve loop blockers");
|
|
1093
|
+
}
|
|
1094
|
+
if (blockedChildren.length > 0) {
|
|
1095
|
+
missing.push("resolve or waive blocked child lanes");
|
|
1096
|
+
}
|
|
1097
|
+
if (waitingChildren.length > 0) {
|
|
1098
|
+
missing.push("resolve waiting child lanes");
|
|
1099
|
+
}
|
|
1100
|
+
if (actionableChildren.length > 0) {
|
|
1101
|
+
missing.push("complete actionable child lanes");
|
|
1102
|
+
}
|
|
1103
|
+
if (missingChildren.length > 0) {
|
|
1104
|
+
missing.push("repair missing child refs");
|
|
1105
|
+
}
|
|
1106
|
+
if (waitingEvidenceLanes.length > 0) {
|
|
1107
|
+
missing.push("record evidence, run, output, or lane waiver refs for evidence lanes");
|
|
1108
|
+
}
|
|
1109
|
+
const nextActions = [];
|
|
1110
|
+
if (pendingMaterialization.length > 0 && materializationMode !== "planning_only") {
|
|
1111
|
+
nextActions.push("materialize default child lanes or switch the loop to planning-only/no-child materialization");
|
|
1112
|
+
}
|
|
1113
|
+
if (actionableChildren.length > 0) {
|
|
1114
|
+
nextActions.push(`work next actionable child lane ${actionableChildren[0]}`);
|
|
1115
|
+
}
|
|
1116
|
+
if (blockedChildren.length > 0) {
|
|
1117
|
+
nextActions.push("work blocker recovery through spike, proposal, recommended path, and continued unblocked lanes");
|
|
1118
|
+
}
|
|
1119
|
+
if (unansweredPreRunQuestions.length > 0) {
|
|
1120
|
+
nextActions.push("answer remaining pre-run questions before taking the actions they govern");
|
|
1121
|
+
}
|
|
1122
|
+
if (pendingApprovalActions.length > 0) {
|
|
1123
|
+
nextActions.push("record typed approval refs for required or requested approval-gated actions before taking them");
|
|
1124
|
+
}
|
|
1125
|
+
if (waitingChildren.length > 0) {
|
|
1126
|
+
nextActions.push(`review waiting child lane ${waitingChildren[0]}`);
|
|
1127
|
+
}
|
|
1128
|
+
if (waitingEvidenceLanes.length > 0) {
|
|
1129
|
+
nextActions.push(`record evidence for lane ${waitingEvidenceLanes[0]}`);
|
|
1130
|
+
}
|
|
1131
|
+
if (nextActions.length === 0) {
|
|
1132
|
+
nextActions.push("close the loop when final validation and closeout evidence are recorded");
|
|
1133
|
+
}
|
|
1134
|
+
const ready = missing.length === 0;
|
|
1135
|
+
return {
|
|
1136
|
+
identity: {
|
|
1137
|
+
id: node.id,
|
|
1138
|
+
qid: node.qid,
|
|
1139
|
+
title: node.title,
|
|
1140
|
+
status: node.status,
|
|
1141
|
+
mode: attributeString(node, "loop_mode"),
|
|
1142
|
+
role: attributeString(node, "loop_role"),
|
|
1143
|
+
},
|
|
1144
|
+
scope: {
|
|
1145
|
+
refs: attributeStringList(node, "scope_refs"),
|
|
1146
|
+
description: attributeString(node, "scope_description"),
|
|
1147
|
+
},
|
|
1148
|
+
template_lineage: {
|
|
1149
|
+
template_refs: attributeStringList(node, "template_refs"),
|
|
1150
|
+
materialization_mode: materializationMode,
|
|
1151
|
+
provenance: loopTemplateProvenance(root, config, index, node),
|
|
1152
|
+
},
|
|
1153
|
+
questions: {
|
|
1154
|
+
pre_run_questions: preRunQuestions,
|
|
1155
|
+
unanswered_pre_run_questions: unansweredPreRunQuestions,
|
|
1156
|
+
items: questionItems,
|
|
1157
|
+
},
|
|
1158
|
+
approvals: {
|
|
1159
|
+
pre_approved_actions: preApprovedActions,
|
|
1160
|
+
approval_gated_actions: approvalGatedActions,
|
|
1161
|
+
required_actions: requiredActions,
|
|
1162
|
+
requested_actions: requestedActions,
|
|
1163
|
+
optional_actions: optionalActions,
|
|
1164
|
+
prohibited_actions: prohibitedActions,
|
|
1165
|
+
pending_approval_actions: pendingApprovalActions,
|
|
1166
|
+
decision_refs: decisionRefs,
|
|
1167
|
+
approval_refs: approvalRefs,
|
|
1168
|
+
items: actionItems,
|
|
1169
|
+
},
|
|
1170
|
+
references: {
|
|
1171
|
+
child_refs: childRefs,
|
|
1172
|
+
run_refs: runRefs,
|
|
1173
|
+
output_refs: outputRefs,
|
|
1174
|
+
evidence_refs: evidenceRefs,
|
|
1175
|
+
evaluation_refs: evaluationRefs,
|
|
1176
|
+
lane_waiver_refs: laneWaiverRefs,
|
|
1177
|
+
question_answer_refs: attributeStringList(node, "question_answer_refs"),
|
|
1178
|
+
action_approval_refs: attributeStringList(node, "action_approval_refs"),
|
|
1179
|
+
evidence_lane_refs: attributeStringList(node, "evidence_lane_refs"),
|
|
1180
|
+
lane_waiver_decision_refs: attributeStringList(node, "lane_waiver_decision_refs"),
|
|
1181
|
+
lane_waiver_approval_refs: attributeStringList(node, "lane_waiver_approval_refs"),
|
|
1182
|
+
},
|
|
1183
|
+
lanes: {
|
|
1184
|
+
children,
|
|
1185
|
+
evidence,
|
|
1186
|
+
},
|
|
1187
|
+
blockers: {
|
|
1188
|
+
loop_blocked_by: [...node.edges.blocked_by],
|
|
1189
|
+
blocked_children: blockedChildren,
|
|
1190
|
+
waiting_children: waitingChildren,
|
|
1191
|
+
},
|
|
1192
|
+
pending_materialization: pendingMaterialization,
|
|
1193
|
+
invalid_bindings: invalidBindings,
|
|
1194
|
+
next_actions: nextActions,
|
|
1195
|
+
closeout: {
|
|
1196
|
+
ready,
|
|
1197
|
+
state: ready ? "ready" : "not_ready",
|
|
1198
|
+
missing,
|
|
1199
|
+
counts,
|
|
1200
|
+
},
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
function buildLoopNextSelection(readiness) {
|
|
1204
|
+
const skipped = [];
|
|
1205
|
+
for (const child of readiness.lanes.children) {
|
|
1206
|
+
if (child.state === "completed" || child.state === "waived") {
|
|
1207
|
+
skipped.push({
|
|
1208
|
+
ref: child.qid ?? child.ref,
|
|
1209
|
+
state: child.state,
|
|
1210
|
+
reason: `lane is ${child.state}`,
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
if (readiness.closeout.ready) {
|
|
1215
|
+
return {
|
|
1216
|
+
selected: null,
|
|
1217
|
+
skipped,
|
|
1218
|
+
rationale: "loop closeout is ready; no actionable lane remains",
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
if (readiness.approvals.pending_approval_actions.length > 0) {
|
|
1222
|
+
for (const action of readiness.approvals.pending_approval_actions) {
|
|
1223
|
+
skipped.push({ ref: action, state: "approval_pending", reason: "required approval-gated action remains pending" });
|
|
1224
|
+
}
|
|
1225
|
+
return {
|
|
1226
|
+
selected: null,
|
|
1227
|
+
skipped,
|
|
1228
|
+
rationale: "required approval-gated work is pending typed approval",
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
const actionableChild = readiness.lanes.children.find((child) => child.state === "actionable");
|
|
1232
|
+
if (actionableChild) {
|
|
1233
|
+
for (const question of readiness.questions.unanswered_pre_run_questions) {
|
|
1234
|
+
skipped.push({
|
|
1235
|
+
ref: question,
|
|
1236
|
+
state: "waiting",
|
|
1237
|
+
reason: "question remains open, but it does not gate this pre-approved child lane",
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
for (const action of readiness.approvals.pending_approval_actions) {
|
|
1241
|
+
skipped.push({
|
|
1242
|
+
ref: action,
|
|
1243
|
+
state: "approval_pending",
|
|
1244
|
+
reason: "approval-gated action is pending, but another child lane is actionable",
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
for (const child of readiness.lanes.children) {
|
|
1248
|
+
if (child.state === "blocked" || child.state === "waiting" || child.state === "missing") {
|
|
1249
|
+
skipped.push({
|
|
1250
|
+
ref: child.qid ?? child.ref,
|
|
1251
|
+
state: child.state,
|
|
1252
|
+
reason: `lane is ${child.state}, but another child lane is actionable`,
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
return {
|
|
1257
|
+
selected: {
|
|
1258
|
+
kind: "child",
|
|
1259
|
+
ref: actionableChild.ref,
|
|
1260
|
+
qid: actionableChild.qid,
|
|
1261
|
+
title: actionableChild.title,
|
|
1262
|
+
state: actionableChild.state,
|
|
1263
|
+
reason: "first unblocked actionable child lane in loop order",
|
|
1264
|
+
},
|
|
1265
|
+
skipped,
|
|
1266
|
+
rationale: "selected an unblocked actionable child lane before blocker or approval recovery",
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
const blockedChild = readiness.lanes.children.find((child) => child.state === "blocked");
|
|
1270
|
+
if (blockedChild) {
|
|
1271
|
+
for (const question of readiness.questions.unanswered_pre_run_questions) {
|
|
1272
|
+
skipped.push({
|
|
1273
|
+
ref: question,
|
|
1274
|
+
state: "waiting",
|
|
1275
|
+
reason: "question remains open, but blocker recovery is still authorized",
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
return {
|
|
1279
|
+
selected: {
|
|
1280
|
+
kind: "recovery",
|
|
1281
|
+
ref: blockedChild.ref,
|
|
1282
|
+
qid: blockedChild.qid,
|
|
1283
|
+
title: blockedChild.title,
|
|
1284
|
+
state: blockedChild.state,
|
|
1285
|
+
reason: "blocked child lane needs spike/proposal/recommendation recovery before the loop blocks",
|
|
1286
|
+
},
|
|
1287
|
+
skipped,
|
|
1288
|
+
rationale: "no unblocked child lane remains, so route to blocker recovery",
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
if (readiness.questions.unanswered_pre_run_questions.length > 0) {
|
|
1292
|
+
return {
|
|
1293
|
+
selected: {
|
|
1294
|
+
kind: "lane",
|
|
1295
|
+
ref: readiness.questions.unanswered_pre_run_questions[0],
|
|
1296
|
+
state: "waiting",
|
|
1297
|
+
reason: "no authorized child or recovery lane remains before this question must be resolved",
|
|
1298
|
+
},
|
|
1299
|
+
skipped,
|
|
1300
|
+
rationale: "the loop exhausted currently authorized work and now requires a pre-run decision",
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
const waitingChild = readiness.lanes.children.find((child) => child.state === "waiting");
|
|
1304
|
+
if (waitingChild) {
|
|
1305
|
+
return {
|
|
1306
|
+
selected: {
|
|
1307
|
+
kind: "lane",
|
|
1308
|
+
ref: waitingChild.ref,
|
|
1309
|
+
qid: waitingChild.qid,
|
|
1310
|
+
title: waitingChild.title,
|
|
1311
|
+
state: waitingChild.state,
|
|
1312
|
+
reason: "waiting child lane needs review, approval, or external state resolution",
|
|
1313
|
+
},
|
|
1314
|
+
skipped,
|
|
1315
|
+
rationale: "no actionable child lane remains, so route to waiting lane resolution",
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
const pendingApproval = readiness.approvals.pending_approval_actions[0];
|
|
1319
|
+
if (pendingApproval) {
|
|
1320
|
+
return {
|
|
1321
|
+
selected: {
|
|
1322
|
+
kind: "lane",
|
|
1323
|
+
ref: pendingApproval,
|
|
1324
|
+
state: "waiting",
|
|
1325
|
+
reason: "approval-gated action needs an approval_ref before execution",
|
|
1326
|
+
},
|
|
1327
|
+
skipped,
|
|
1328
|
+
rationale: "approval-gated actions are the next unresolved readiness lane",
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
const waitingEvidence = readiness.lanes.evidence.find((lane) => lane.state === "waiting");
|
|
1332
|
+
if (waitingEvidence) {
|
|
1333
|
+
return {
|
|
1334
|
+
selected: {
|
|
1335
|
+
kind: "lane",
|
|
1336
|
+
ref: waitingEvidence.name,
|
|
1337
|
+
state: waitingEvidence.state,
|
|
1338
|
+
reason: "evidence lane needs run, output, evidence, or waiver refs",
|
|
1339
|
+
},
|
|
1340
|
+
skipped,
|
|
1341
|
+
rationale: "evidence is the next unresolved readiness lane",
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
const missingChild = readiness.lanes.children.find((child) => child.state === "missing");
|
|
1345
|
+
if (missingChild) {
|
|
1346
|
+
return {
|
|
1347
|
+
selected: {
|
|
1348
|
+
kind: "lane",
|
|
1349
|
+
ref: missingChild.ref,
|
|
1350
|
+
qid: missingChild.qid,
|
|
1351
|
+
state: missingChild.state,
|
|
1352
|
+
reason: "child ref is missing from the graph and needs repair or waiver",
|
|
1353
|
+
},
|
|
1354
|
+
skipped,
|
|
1355
|
+
rationale: "missing child refs are the next unresolved lane",
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
return {
|
|
1359
|
+
selected: {
|
|
1360
|
+
kind: "closeout",
|
|
1361
|
+
state: "waiting",
|
|
1362
|
+
reason: readiness.closeout.missing[0] ?? "loop has unresolved closeout requirements",
|
|
1363
|
+
},
|
|
1364
|
+
skipped,
|
|
1365
|
+
rationale: "no child lane was actionable, so route to closeout readiness cleanup",
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
function truncateSummary(value, max = 120) {
|
|
1369
|
+
const normalized = value?.replace(/\s+/g, " ").trim();
|
|
1370
|
+
if (!normalized) {
|
|
1371
|
+
return undefined;
|
|
1372
|
+
}
|
|
1373
|
+
return normalized.length > max ? `${normalized.slice(0, max - 1).trimEnd()}…` : normalized;
|
|
1374
|
+
}
|
|
1375
|
+
function firstPurposeLine(body) {
|
|
1376
|
+
return body
|
|
1377
|
+
.split(/\r?\n/)
|
|
1378
|
+
.map((line) => line.trim())
|
|
1379
|
+
.find((line) => line.length > 0 && !line.startsWith("#") && !line.startsWith("- "));
|
|
1380
|
+
}
|
|
1381
|
+
function templateComparison(template) {
|
|
1382
|
+
const definition = frontmatterString(template.frontmatter, "definition_of_done", "");
|
|
1383
|
+
const scope = frontmatterString(template.frontmatter, "scope_description", "");
|
|
1384
|
+
const purpose = truncateSummary(firstPurposeLine(template.body) || definition || scope);
|
|
1385
|
+
return {
|
|
1386
|
+
kind: template.kind,
|
|
1387
|
+
ref: template.ref,
|
|
1388
|
+
id: template.id,
|
|
1389
|
+
qid: template.qid,
|
|
1390
|
+
title: template.title,
|
|
1391
|
+
path: template.path,
|
|
1392
|
+
hash: template.hash,
|
|
1393
|
+
mode: frontmatterString(template.frontmatter, "loop_mode", "planning"),
|
|
1394
|
+
role: frontmatterString(template.frontmatter, "loop_role", "template"),
|
|
1395
|
+
tags: frontmatterList(template.frontmatter, "tags"),
|
|
1396
|
+
default_materialization: frontmatterString(template.frontmatter, "materialization_mode", "default_children"),
|
|
1397
|
+
purpose,
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
function loopComparison(root, config, index, node) {
|
|
1401
|
+
return {
|
|
1402
|
+
kind: "node",
|
|
1403
|
+
id: node.id,
|
|
1404
|
+
qid: node.qid,
|
|
1405
|
+
title: node.title,
|
|
1406
|
+
status: node.status,
|
|
1407
|
+
path: node.path,
|
|
1408
|
+
mode: attributeString(node, "loop_mode"),
|
|
1409
|
+
role: attributeString(node, "loop_role"),
|
|
1410
|
+
tags: [...node.tags],
|
|
1411
|
+
materialization_mode: attributeString(node, "materialization_mode"),
|
|
1412
|
+
scope_description: truncateSummary(attributeString(node, "scope_description")),
|
|
1413
|
+
template_lineage: loopTemplateProvenance(root, config, index, node),
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
function formatComparisonDetails(entry) {
|
|
1417
|
+
const tags = Array.isArray(entry.tags) && entry.tags.length > 0 ? ` tags=${entry.tags.join(",")}` : "";
|
|
1418
|
+
const materialization = typeof entry.default_materialization === "string"
|
|
1419
|
+
? entry.default_materialization
|
|
1420
|
+
: typeof entry.materialization_mode === "string"
|
|
1421
|
+
? entry.materialization_mode
|
|
1422
|
+
: "unknown";
|
|
1423
|
+
const scopeOrPurpose = typeof entry.purpose === "string"
|
|
1424
|
+
? ` purpose=${entry.purpose}`
|
|
1425
|
+
: typeof entry.scope_description === "string"
|
|
1426
|
+
? ` scope=${entry.scope_description}`
|
|
1427
|
+
: "";
|
|
1428
|
+
const lineage = entry.template_lineage;
|
|
1429
|
+
const provenance = lineage ? ` provenance=${lineage.state}` : "";
|
|
1430
|
+
return [
|
|
1431
|
+
`mode=${String(entry.mode ?? "unknown")}`,
|
|
1432
|
+
`role=${String(entry.role ?? "unknown")}`,
|
|
1433
|
+
`materialization=${materialization}`,
|
|
1434
|
+
].join(" ") + tags + scopeOrPurpose + provenance;
|
|
1435
|
+
}
|
|
1436
|
+
function runLoopListCommand(options) {
|
|
1437
|
+
const { config, index, ws, warnings } = loadLoopIndex(options);
|
|
1438
|
+
const nodes = visibleLoopNodes(index, ws);
|
|
1439
|
+
const templates = options.templates === false ? [] : loadSeedTemplates(options.root, config);
|
|
1440
|
+
const loopCatalog = nodes.map((node) => loopComparison(options.root, config, index, node));
|
|
1441
|
+
const templateCatalog = templates.map(templateComparison);
|
|
1442
|
+
const provenanceWarnings = loopCatalog.flatMap((entry) => {
|
|
1443
|
+
const lineage = entry.template_lineage;
|
|
1444
|
+
return lineage?.warning ? [`${String(entry.qid)}: ${lineage.warning}`] : [];
|
|
1445
|
+
});
|
|
1446
|
+
const outputWarnings = [...warnings, ...provenanceWarnings];
|
|
1447
|
+
if (options.json) {
|
|
1448
|
+
(0, query_output_1.writeJson)({
|
|
1449
|
+
action: "listed",
|
|
1450
|
+
loops: nodes.map(query_output_1.toNodeSummaryJson),
|
|
1451
|
+
templates: templateCatalog,
|
|
1452
|
+
catalog: {
|
|
1453
|
+
loops: loopCatalog,
|
|
1454
|
+
templates: templateCatalog,
|
|
1455
|
+
},
|
|
1456
|
+
warnings: outputWarnings,
|
|
1457
|
+
});
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
console.log(`${nodes.length} indexed loop${nodes.length === 1 ? "" : "s"} matched`);
|
|
1461
|
+
for (const [index, node] of nodes.entries()) {
|
|
1462
|
+
console.log((0, node_card_1.formatNodeCard)(node));
|
|
1463
|
+
console.log(` ${formatComparisonDetails(loopCatalog[index] ?? {})}`);
|
|
1464
|
+
}
|
|
1465
|
+
if (templates.length > 0) {
|
|
1466
|
+
console.log(`${templates.length} seed loop template${templates.length === 1 ? "" : "s"} available`);
|
|
1467
|
+
for (const [index, template] of templates.entries()) {
|
|
1468
|
+
const entry = templateCatalog[index] ?? {};
|
|
1469
|
+
console.log(`${template.ref} ${template.title} (${template.path})`);
|
|
1470
|
+
console.log(` ${formatComparisonDetails(entry)}`);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
for (const warning of outputWarnings) {
|
|
1474
|
+
console.error(`warning: ${warning}`);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
function runLoopShowCommand(options) {
|
|
1478
|
+
const { config, index, ws, warnings } = loadLoopIndex(options);
|
|
1479
|
+
const resolved = (0, qid_1.resolveQid)(index, options.id, ws);
|
|
1480
|
+
if (resolved.status === "ok") {
|
|
1481
|
+
const node = resolveLoopNode(index, options.id, ws);
|
|
1482
|
+
const body = options.metaOnly ? undefined : (0, node_body_1.readNodeBody)(options.root, node);
|
|
1483
|
+
const templateLineage = loopTemplateProvenance(options.root, config, index, node);
|
|
1484
|
+
const outputWarnings = templateLineage.warning
|
|
1485
|
+
? [...warnings, `${node.qid}: ${templateLineage.warning}`]
|
|
1486
|
+
: warnings;
|
|
1487
|
+
if (options.json) {
|
|
1488
|
+
(0, query_output_1.writeJson)({
|
|
1489
|
+
action: "showed",
|
|
1490
|
+
loop: (0, query_output_1.toNodeDetailJson)(node, body),
|
|
1491
|
+
template_lineage: templateLineage,
|
|
1492
|
+
warnings: outputWarnings,
|
|
1493
|
+
});
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
console.log((0, node_card_1.formatNodeCard)(node));
|
|
1497
|
+
console.log(`template provenance: ${templateLineage.state}`);
|
|
1498
|
+
if (body) {
|
|
1499
|
+
console.log("");
|
|
1500
|
+
console.log(body);
|
|
1501
|
+
}
|
|
1502
|
+
for (const warning of outputWarnings) {
|
|
1503
|
+
console.error(`warning: ${warning}`);
|
|
1504
|
+
}
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
const template = resolveLoopTemplate(options.root, config, index, options.id, ws);
|
|
1508
|
+
if (options.json) {
|
|
1509
|
+
(0, query_output_1.writeJson)({
|
|
1510
|
+
action: "showed",
|
|
1511
|
+
template: {
|
|
1512
|
+
kind: template.kind,
|
|
1513
|
+
ref: template.ref,
|
|
1514
|
+
id: template.id,
|
|
1515
|
+
qid: template.qid,
|
|
1516
|
+
title: template.title,
|
|
1517
|
+
path: template.path,
|
|
1518
|
+
hash: template.hash,
|
|
1519
|
+
frontmatter: template.frontmatter,
|
|
1520
|
+
body: options.metaOnly ? undefined : template.body,
|
|
1521
|
+
},
|
|
1522
|
+
warnings,
|
|
1523
|
+
});
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
console.log(`${template.ref} ${template.title} (${template.path})`);
|
|
1527
|
+
if (!options.metaOnly && template.body.trim().length > 0) {
|
|
1528
|
+
console.log("");
|
|
1529
|
+
console.log(template.body.trimStart());
|
|
1530
|
+
}
|
|
1531
|
+
for (const warning of warnings) {
|
|
1532
|
+
console.error(`warning: ${warning}`);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
function runLoopPlanCommand(options) {
|
|
1536
|
+
const { config, index, ws, warnings } = loadLoopIndex(options);
|
|
1537
|
+
const node = resolveLoopNode(index, options.id, ws);
|
|
1538
|
+
const readiness = buildLoopReadinessProjection(options.root, config, index, node);
|
|
1539
|
+
const provenanceWarning = readiness.template_lineage.provenance.warning;
|
|
1540
|
+
const outputWarnings = provenanceWarning
|
|
1541
|
+
? [...warnings, `${node.qid}: ${provenanceWarning}`]
|
|
1542
|
+
: warnings;
|
|
1543
|
+
const materializationMode = readiness.template_lineage.materialization_mode;
|
|
1544
|
+
const childRefs = readiness.references.child_refs;
|
|
1545
|
+
const payload = {
|
|
1546
|
+
action: "planned",
|
|
1547
|
+
loop: (0, query_output_1.toNodeSummaryJson)(node),
|
|
1548
|
+
materialization_mode: materializationMode,
|
|
1549
|
+
child_refs: childRefs,
|
|
1550
|
+
pending_materialization: readiness.pending_materialization,
|
|
1551
|
+
readiness,
|
|
1552
|
+
blocker_continuation: blockerContinuationGuidance(node.attributes.blocker_policy),
|
|
1553
|
+
warnings: outputWarnings,
|
|
1554
|
+
};
|
|
1555
|
+
if (options.json) {
|
|
1556
|
+
(0, query_output_1.writeJson)(payload);
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
console.log(`loop plan: ${node.qid}`);
|
|
1560
|
+
console.log(`materialization: ${String(materializationMode ?? "unknown")}`);
|
|
1561
|
+
console.log(`template provenance: ${readiness.template_lineage.provenance.state}`);
|
|
1562
|
+
console.log(`children: ${childRefs.length > 0 ? childRefs.join(", ") : "none"}`);
|
|
1563
|
+
console.log(`readiness: ${readiness.closeout.state}`);
|
|
1564
|
+
console.log(`closeout_ready: ${readiness.closeout.ready ? "yes" : "no"}`);
|
|
1565
|
+
console.log(`safe_to_run: ${readiness.approvals.pre_approved_actions.length > 0 ? readiness.approvals.pre_approved_actions.join(", ") : "none"}`);
|
|
1566
|
+
console.log(`approval_gated: ${readiness.approvals.approval_gated_actions.length > 0 ? readiness.approvals.approval_gated_actions.join(", ") : "none"}`);
|
|
1567
|
+
console.log(`required_actions: ${readiness.approvals.required_actions.length > 0 ? readiness.approvals.required_actions.join(", ") : "none"}`);
|
|
1568
|
+
console.log(`requested_actions: ${readiness.approvals.requested_actions.length > 0 ? readiness.approvals.requested_actions.join(", ") : "none"}`);
|
|
1569
|
+
console.log(`optional_actions: ${readiness.approvals.optional_actions.length > 0 ? readiness.approvals.optional_actions.join(", ") : "none"}`);
|
|
1570
|
+
console.log(`prohibited_actions: ${readiness.approvals.prohibited_actions.length > 0 ? readiness.approvals.prohibited_actions.join(", ") : "none"}`);
|
|
1571
|
+
if (readiness.questions.unanswered_pre_run_questions.length > 0) {
|
|
1572
|
+
console.log(`pre_run_questions: ${readiness.questions.unanswered_pre_run_questions.join(", ")}`);
|
|
1573
|
+
}
|
|
1574
|
+
if (readiness.approvals.pending_approval_actions.length > 0) {
|
|
1575
|
+
console.log(`pending_approvals: ${readiness.approvals.pending_approval_actions.join(", ")}`);
|
|
1576
|
+
}
|
|
1577
|
+
console.log(`evidence_lanes: ${readiness.lanes.evidence.length > 0
|
|
1578
|
+
? readiness.lanes.evidence.map((lane) => `${lane.name}:${lane.state}`).join(", ")
|
|
1579
|
+
: "none"}`);
|
|
1580
|
+
console.log(`run_refs: ${readiness.references.run_refs.length > 0 ? readiness.references.run_refs.join(", ") : "none"}`);
|
|
1581
|
+
console.log(`output_refs: ${readiness.references.output_refs.length > 0 ? readiness.references.output_refs.join(", ") : "none"}`);
|
|
1582
|
+
console.log(`lane_waivers: ${readiness.references.lane_waiver_refs.length > 0 ? readiness.references.lane_waiver_refs.join(", ") : "none"}`);
|
|
1583
|
+
console.log(`blockers: ${readiness.blockers.loop_blocked_by.length + readiness.blockers.blocked_children.length > 0
|
|
1584
|
+
? [...readiness.blockers.loop_blocked_by, ...readiness.blockers.blocked_children].join(", ")
|
|
1585
|
+
: "none"}`);
|
|
1586
|
+
console.log(`missing: ${readiness.closeout.missing.length > 0 ? readiness.closeout.missing.join("; ") : "none"}`);
|
|
1587
|
+
if (readiness.invalid_bindings.length > 0) {
|
|
1588
|
+
console.log(`invalid_bindings: ${readiness.invalid_bindings.join("; ")}`);
|
|
1589
|
+
}
|
|
1590
|
+
if (readiness.next_actions.length > 0) {
|
|
1591
|
+
console.log(`next_action: ${readiness.next_actions[0]}`);
|
|
1592
|
+
}
|
|
1593
|
+
for (const warning of outputWarnings) {
|
|
1594
|
+
console.error(`warning: ${warning}`);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
function runLoopNextCommand(options) {
|
|
1598
|
+
const { config, index, ws, warnings } = loadLoopIndex(options);
|
|
1599
|
+
const node = resolveLoopNode(index, options.id, ws);
|
|
1600
|
+
const readiness = buildLoopReadinessProjection(options.root, config, index, node);
|
|
1601
|
+
const selection = buildLoopNextSelection(readiness);
|
|
1602
|
+
const remainingPath = selection.selected !== null && selection.selected.kind !== "closeout";
|
|
1603
|
+
const exhaustion = {
|
|
1604
|
+
authorized_work_remaining: selection.selected?.kind === "child" || selection.selected?.kind === "recovery",
|
|
1605
|
+
decision_or_evidence_path_remaining: selection.selected?.kind === "lane",
|
|
1606
|
+
whole_loop_blocked: !readiness.closeout.ready && !remainingPath,
|
|
1607
|
+
};
|
|
1608
|
+
const payload = {
|
|
1609
|
+
action: "selected",
|
|
1610
|
+
loop: (0, query_output_1.toNodeSummaryJson)(node),
|
|
1611
|
+
selected: selection.selected,
|
|
1612
|
+
rationale: selection.rationale,
|
|
1613
|
+
skipped: selection.skipped,
|
|
1614
|
+
exhaustion,
|
|
1615
|
+
readiness: {
|
|
1616
|
+
closeout: readiness.closeout,
|
|
1617
|
+
next_actions: readiness.next_actions,
|
|
1618
|
+
blockers: readiness.blockers,
|
|
1619
|
+
pending_approval_actions: readiness.approvals.pending_approval_actions,
|
|
1620
|
+
unanswered_pre_run_questions: readiness.questions.unanswered_pre_run_questions,
|
|
1621
|
+
},
|
|
1622
|
+
warnings,
|
|
1623
|
+
};
|
|
1624
|
+
if (options.json) {
|
|
1625
|
+
(0, query_output_1.writeJson)(payload);
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
console.log(`loop next: ${node.qid}`);
|
|
1629
|
+
if (selection.selected) {
|
|
1630
|
+
const selectedRef = selection.selected.qid ?? selection.selected.ref ?? selection.selected.kind;
|
|
1631
|
+
console.log(`selected: ${selection.selected.kind} ${selectedRef}`);
|
|
1632
|
+
console.log(`reason: ${selection.selected.reason}`);
|
|
1633
|
+
}
|
|
1634
|
+
else {
|
|
1635
|
+
console.log("selected: none");
|
|
1636
|
+
console.log(`reason: ${selection.rationale}`);
|
|
1637
|
+
}
|
|
1638
|
+
if (selection.skipped.length > 0) {
|
|
1639
|
+
console.log(`skipped: ${selection.skipped.map((entry) => `${entry.ref}:${entry.reason}`).join("; ")}`);
|
|
1640
|
+
}
|
|
1641
|
+
console.log(`whole_loop_blocked: ${exhaustion.whole_loop_blocked ? "yes" : "no"}`);
|
|
1642
|
+
for (const warning of warnings) {
|
|
1643
|
+
console.error(`warning: ${warning}`);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
function runLoopRunsCommand(options) {
|
|
1647
|
+
const { index, ws, warnings } = loadLoopIndex(options);
|
|
1648
|
+
const node = resolveLoopNode(index, options.id, ws);
|
|
1649
|
+
const runRefs = Array.isArray(node.attributes.run_refs) ? node.attributes.run_refs : [];
|
|
1650
|
+
const evidenceRefs = node.edges.evidence_refs ?? [];
|
|
1651
|
+
const payload = {
|
|
1652
|
+
action: "listed",
|
|
1653
|
+
loop: (0, query_output_1.toNodeSummaryJson)(node),
|
|
1654
|
+
run_refs: runRefs,
|
|
1655
|
+
evidence_refs: evidenceRefs,
|
|
1656
|
+
warnings,
|
|
1657
|
+
};
|
|
1658
|
+
if (options.json) {
|
|
1659
|
+
(0, query_output_1.writeJson)(payload);
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
console.log(`loop runs: ${node.qid}`);
|
|
1663
|
+
console.log(`run_refs: ${runRefs.length > 0 ? runRefs.join(", ") : "none"}`);
|
|
1664
|
+
console.log(`evidence_refs: ${evidenceRefs.length > 0 ? evidenceRefs.join(", ") : "none"}`);
|
|
1665
|
+
for (const warning of warnings) {
|
|
1666
|
+
console.error(`warning: ${warning}`);
|
|
1667
|
+
}
|
|
1668
|
+
}
|