@titan-design/brain 0.6.1 → 0.7.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/README.md +66 -44
- package/dist/{brain-service-4ETWBOIO.js → brain-service-TYFNTBT6.js} +2 -2
- package/dist/{chunk-KSJZ7CMP.js → chunk-4GDSQB2E.js} +1 -1
- package/dist/{chunk-HNC656YT.js → chunk-IESQY2UZ.js} +7 -5
- package/dist/{chunk-AJKFX2TM.js → chunk-LLAHWRO4.js} +2 -1
- package/dist/cli.js +2472 -223
- package/dist/{command-resolution-MO7LSFOT.js → command-resolution-EJ6LTC2Z.js} +1 -1
- package/dist/{search-AKSAQJOR.js → search-NPTRJV4W.js} +2 -2
- package/dist/templates/brainstorm-design.md +30 -0
- package/dist/templates/brainstorm-explore.md +30 -0
- package/dist/templates/brainstorm-interview.md +26 -0
- package/dist/templates/brainstorm-propose.md +28 -0
- package/dist/templates/brainstorm-write-doc.md +30 -0
- package/dist/templates/implementation-compact.md +46 -0
- package/dist/templates/implementation.md +92 -0
- package/dist/templates/ops.md +18 -0
- package/dist/templates/planning-critic.md +123 -0
- package/dist/templates/planning-decompose.md +221 -0
- package/dist/templates/planning-design.md +162 -0
- package/dist/templates/planning-interview.md +74 -0
- package/dist/templates/planning-research.md +114 -0
- package/dist/templates/planning-spectests.md +84 -0
- package/dist/templates/review-agent.md +155 -0
- package/dist/templates/review-fixup.md +79 -0
- package/dist/templates/validation-gates.md +84 -0
- package/dist/templates/writing-plans.md +48 -0
- package/package.json +11 -3
- package/skill/SKILL.md +53 -25
package/dist/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
saveConfig,
|
|
12
12
|
withBrain,
|
|
13
13
|
withDb
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-IESQY2UZ.js";
|
|
15
15
|
import {
|
|
16
16
|
createEmbedder,
|
|
17
17
|
getEmbedderInfo
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
computeFacets,
|
|
21
21
|
search,
|
|
22
22
|
searchMemories
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-4GDSQB2E.js";
|
|
24
24
|
import {
|
|
25
25
|
VALID_CORE_NOTE_TYPES,
|
|
26
26
|
VALID_INBOX_SOURCES,
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
processInbox,
|
|
40
40
|
slugify,
|
|
41
41
|
traverseGraph
|
|
42
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-LLAHWRO4.js";
|
|
43
43
|
import {
|
|
44
44
|
INDEXABLE_EXTENSIONS,
|
|
45
45
|
scanForChanges
|
|
@@ -53,7 +53,7 @@ import {
|
|
|
53
53
|
|
|
54
54
|
// src/cli.ts
|
|
55
55
|
import { createRequire } from "module";
|
|
56
|
-
import { Command as
|
|
56
|
+
import { Command as Command49 } from "@commander-js/extra-typings";
|
|
57
57
|
|
|
58
58
|
// src/commands/init.ts
|
|
59
59
|
import { Command } from "@commander-js/extra-typings";
|
|
@@ -1317,7 +1317,8 @@ var TYPE_DIRS = {
|
|
|
1317
1317
|
pattern: "patterns",
|
|
1318
1318
|
meeting: "logs",
|
|
1319
1319
|
"session-log": "logs",
|
|
1320
|
-
guide: "guides"
|
|
1320
|
+
guide: "guides",
|
|
1321
|
+
workflow: "workflows"
|
|
1321
1322
|
};
|
|
1322
1323
|
function resolveOutputPath(notesDir, tier, type, id) {
|
|
1323
1324
|
if (tier === "fast") {
|
|
@@ -1395,7 +1396,7 @@ function validateEnum(value, valid, label) {
|
|
|
1395
1396
|
}
|
|
1396
1397
|
var addCommand = new Command5("add").description("Create a new note from file or stdin").argument("[file]", "Input file path").option("--title <title>", "Note title").option(
|
|
1397
1398
|
"--type <type>",
|
|
1398
|
-
"Note type (note, decision, pattern, research, meeting, session-log, guide)"
|
|
1399
|
+
"Note type (note, decision, pattern, research, meeting, session-log, guide, workflow)"
|
|
1399
1400
|
).option("--tier <tier>", "Note tier (slow, fast)").option("--tags <tags>", "Comma-separated tags").option("--summary <text>", "One-line summary for search excerpts").option("--confidence <level>", "Confidence level (high, medium, low, speculative)").option("--status <status>", "Note status (current, outdated, deprecated, draft)").option("--category <cat>", "Category label").option("--related <ids>", "Comma-separated related note IDs").option("--review-interval <interval>", "Review interval (e.g. 90d, 30d, 180d)").option("--created <date>", "Created date (YYYY-MM-DD), defaults to today").option("--url <url>", "Fetch URL and create note from extracted content").action(async (file, opts, cmd) => {
|
|
1400
1401
|
if (opts.type && !VALID_CORE_NOTE_TYPES.includes(opts.type)) {
|
|
1401
1402
|
process.stderr.write(
|
|
@@ -2370,8 +2371,8 @@ import { join as join5 } from "path";
|
|
|
2370
2371
|
function writeQueueFile(brainDir, context, registry) {
|
|
2371
2372
|
const queueDir = join5(brainDir, "import-queue");
|
|
2372
2373
|
mkdirSync5(queueDir, { recursive: true });
|
|
2373
|
-
const
|
|
2374
|
-
const slug = slugify(
|
|
2374
|
+
const basename8 = context.sourcePath.split("/").pop()?.replace(/\.[^.]+$/, "") ?? "unknown";
|
|
2375
|
+
const slug = slugify(basename8);
|
|
2375
2376
|
const queuePath = join5(queueDir, `${slug}.md`);
|
|
2376
2377
|
const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2377
2378
|
const lines2 = [];
|
|
@@ -2476,15 +2477,19 @@ async function runExtractionPipeline(content, filePath, registry, embedder, db,
|
|
|
2476
2477
|
const lineCount = content.split("\n").length;
|
|
2477
2478
|
const ext = filePath.split(".").pop()?.toLowerCase() ?? "txt";
|
|
2478
2479
|
const format = detectFormat2(ext);
|
|
2479
|
-
const queueResult = writeQueueFile(
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2480
|
+
const queueResult = writeQueueFile(
|
|
2481
|
+
config.notesDir,
|
|
2482
|
+
{
|
|
2483
|
+
sourcePath: filePath,
|
|
2484
|
+
format,
|
|
2485
|
+
lineCount,
|
|
2486
|
+
tier1Items: tier1.items,
|
|
2487
|
+
tier2Items,
|
|
2488
|
+
lowConfidenceRegions: [],
|
|
2489
|
+
remainderContent: tier2Remainder
|
|
2490
|
+
},
|
|
2491
|
+
registry
|
|
2492
|
+
);
|
|
2488
2493
|
result.queuedFiles.push({ path: queueResult.queuePath, reason: queueResult.reason });
|
|
2489
2494
|
}
|
|
2490
2495
|
const handlers = registry.getContentHandlers();
|
|
@@ -3640,14 +3645,14 @@ async function runAllChecks(db, embedderBackend, ollamaUrl, ollamaModel, notesDi
|
|
|
3640
3645
|
checkStaleNotes(db)
|
|
3641
3646
|
];
|
|
3642
3647
|
if (notesDir) {
|
|
3643
|
-
const { readdirSync:
|
|
3644
|
-
const { join:
|
|
3648
|
+
const { readdirSync: readdirSync7, statSync: statSync5 } = await import("fs");
|
|
3649
|
+
const { join: join23 } = await import("path");
|
|
3645
3650
|
const { INDEXABLE_EXTENSIONS: INDEXABLE_EXTENSIONS2 } = await import("./file-scanner-LBBH5I44.js");
|
|
3646
3651
|
const diskFiles = /* @__PURE__ */ new Set();
|
|
3647
3652
|
try {
|
|
3648
|
-
const entries =
|
|
3653
|
+
const entries = readdirSync7(notesDir, { recursive: true });
|
|
3649
3654
|
for (const entry of entries) {
|
|
3650
|
-
const full =
|
|
3655
|
+
const full = join23(notesDir, entry);
|
|
3651
3656
|
try {
|
|
3652
3657
|
if (statSync5(full).isFile()) {
|
|
3653
3658
|
const ext = full.slice(full.lastIndexOf(".")).toLowerCase();
|
|
@@ -3746,11 +3751,11 @@ var doctorCommand = new Command21("doctor").description("Check system health and
|
|
|
3746
3751
|
}
|
|
3747
3752
|
const fsCheck = report.checks.find((c) => c.name === "Filesystem sync");
|
|
3748
3753
|
if (fsCheck?.status === "warning" && fsCheck.message.includes("orphaned")) {
|
|
3749
|
-
const { existsSync:
|
|
3754
|
+
const { existsSync: existsSync26 } = await import("fs");
|
|
3750
3755
|
const dbFiles = db.getAllFiles();
|
|
3751
3756
|
let cleaned = 0;
|
|
3752
3757
|
for (const [path] of dbFiles) {
|
|
3753
|
-
if (!
|
|
3758
|
+
if (!existsSync26(path)) {
|
|
3754
3759
|
db.deleteFile(path);
|
|
3755
3760
|
cleaned++;
|
|
3756
3761
|
}
|
|
@@ -3983,18 +3988,34 @@ notesCommand.command("list").description("List all notes").option("--module <nam
|
|
|
3983
3988
|
});
|
|
3984
3989
|
|
|
3985
3990
|
// src/modules/pm/index.ts
|
|
3986
|
-
import { Command as
|
|
3991
|
+
import { Command as Command43 } from "@commander-js/extra-typings";
|
|
3987
3992
|
|
|
3988
3993
|
// src/modules/pm/commands/project.ts
|
|
3989
3994
|
import { Command as Command26 } from "@commander-js/extra-typings";
|
|
3990
3995
|
import { existsSync as existsSync13, readFileSync as readFileSync13 } from "fs";
|
|
3991
3996
|
|
|
3992
|
-
// src/
|
|
3997
|
+
// src/errors.ts
|
|
3993
3998
|
function ok(data) {
|
|
3994
3999
|
return { ok: true, data };
|
|
3995
4000
|
}
|
|
3996
4001
|
function fail(code, message, details) {
|
|
3997
|
-
|
|
4002
|
+
const error = {
|
|
4003
|
+
error: true,
|
|
4004
|
+
code,
|
|
4005
|
+
message
|
|
4006
|
+
};
|
|
4007
|
+
if (details !== void 0) {
|
|
4008
|
+
error.details = details;
|
|
4009
|
+
}
|
|
4010
|
+
return { ok: false, error };
|
|
4011
|
+
}
|
|
4012
|
+
|
|
4013
|
+
// src/modules/pm/errors.ts
|
|
4014
|
+
function ok2(data) {
|
|
4015
|
+
return ok(data);
|
|
4016
|
+
}
|
|
4017
|
+
function fail2(code, message, details) {
|
|
4018
|
+
return fail(code, message, details);
|
|
3998
4019
|
}
|
|
3999
4020
|
function pmError(code, message, details) {
|
|
4000
4021
|
const err = { error: true, code, message };
|
|
@@ -4060,13 +4081,13 @@ function nextWorkstreamNumber(db, prefix) {
|
|
|
4060
4081
|
function resolveWorkstreamFilter(input) {
|
|
4061
4082
|
const asInt = Number(input);
|
|
4062
4083
|
if (Number.isInteger(asInt) && asInt > 0) {
|
|
4063
|
-
return
|
|
4084
|
+
return ok2(asInt);
|
|
4064
4085
|
}
|
|
4065
4086
|
const parsed = parseDisplayId(input);
|
|
4066
4087
|
if (parsed && parsed.workstream !== void 0 && parsed.task === void 0) {
|
|
4067
|
-
return
|
|
4088
|
+
return ok2(parsed.workstream);
|
|
4068
4089
|
}
|
|
4069
|
-
return
|
|
4090
|
+
return fail2(
|
|
4070
4091
|
"INVALID_INPUT",
|
|
4071
4092
|
`Invalid workstream filter "${input}". Use a number (6) or display ID (PREFIX-NN). Run "brain pm workstream list" to see options.`
|
|
4072
4093
|
);
|
|
@@ -4091,7 +4112,7 @@ var PM_ACTIVE_PROJECT_KEY = "pm_active_project";
|
|
|
4091
4112
|
function resolveDisplayId(db, displayId) {
|
|
4092
4113
|
const noteIds = db.getModuleNoteIds({ module: "pm" });
|
|
4093
4114
|
if (noteIds.length === 0) {
|
|
4094
|
-
return
|
|
4115
|
+
return fail2("NOT_FOUND", `No PM notes found. Run "brain pm list" to check projects.`);
|
|
4095
4116
|
}
|
|
4096
4117
|
const notes = db.getNotesByIds(noteIds);
|
|
4097
4118
|
const knownPrefixes = /* @__PURE__ */ new Set();
|
|
@@ -4099,7 +4120,7 @@ function resolveDisplayId(db, displayId) {
|
|
|
4099
4120
|
if (!note.metadata) continue;
|
|
4100
4121
|
const meta = JSON.parse(note.metadata);
|
|
4101
4122
|
if (meta.display_id === displayId) {
|
|
4102
|
-
return
|
|
4123
|
+
return ok2(note.id);
|
|
4103
4124
|
}
|
|
4104
4125
|
if (typeof meta.prefix === "string") {
|
|
4105
4126
|
knownPrefixes.add(meta.prefix);
|
|
@@ -4107,7 +4128,7 @@ function resolveDisplayId(db, displayId) {
|
|
|
4107
4128
|
}
|
|
4108
4129
|
const prefixList = [...knownPrefixes].sort().join(", ");
|
|
4109
4130
|
const hint = prefixList ? ` Known projects: ${prefixList}. Run "brain pm list" to see all projects.` : ' Run "brain pm list" to check available projects.';
|
|
4110
|
-
return
|
|
4131
|
+
return fail2("NOT_FOUND", `No PM note found with display ID "${displayId}".${hint}`);
|
|
4111
4132
|
}
|
|
4112
4133
|
function getProjectNotes(db, prefix) {
|
|
4113
4134
|
const noteIds = db.getModuleNoteIds({ module: "pm" });
|
|
@@ -4140,35 +4161,35 @@ function resolveProject(db, explicit) {
|
|
|
4140
4161
|
const m = JSON.parse(p.metadata);
|
|
4141
4162
|
return m.prefix;
|
|
4142
4163
|
});
|
|
4143
|
-
return
|
|
4164
|
+
return fail2(
|
|
4144
4165
|
"NOT_FOUND",
|
|
4145
4166
|
`Project "${upper}" not found. Available projects: ${available.sort().join(", ")}.`
|
|
4146
4167
|
);
|
|
4147
4168
|
}
|
|
4148
|
-
return
|
|
4169
|
+
return fail2("NOT_FOUND", `Project "${upper}" not found. No projects exist yet.`);
|
|
4149
4170
|
}
|
|
4150
|
-
return
|
|
4171
|
+
return ok2(upper);
|
|
4151
4172
|
}
|
|
4152
4173
|
const active = getActiveProject(db);
|
|
4153
|
-
if (active) return
|
|
4174
|
+
if (active) return ok2(active);
|
|
4154
4175
|
const projects = getPmNotes(db, "project");
|
|
4155
4176
|
if (projects.length === 1) {
|
|
4156
4177
|
const meta = JSON.parse(projects[0].metadata);
|
|
4157
4178
|
const prefix = meta.prefix;
|
|
4158
4179
|
setActiveProject(db, prefix);
|
|
4159
|
-
return
|
|
4180
|
+
return ok2(prefix);
|
|
4160
4181
|
}
|
|
4161
4182
|
if (projects.length > 1) {
|
|
4162
4183
|
const prefixes = projects.map((p) => {
|
|
4163
4184
|
const m = JSON.parse(p.metadata);
|
|
4164
4185
|
return m.prefix;
|
|
4165
4186
|
});
|
|
4166
|
-
return
|
|
4187
|
+
return fail2(
|
|
4167
4188
|
"INVALID_INPUT",
|
|
4168
4189
|
`Multiple projects found: ${prefixes.join(", ")}. Use --project <prefix> or "brain pm use <prefix>".`
|
|
4169
4190
|
);
|
|
4170
4191
|
}
|
|
4171
|
-
return
|
|
4192
|
+
return fail2("INVALID_INPUT", 'No projects found. Run "brain pm onboard <name>" to create one.');
|
|
4172
4193
|
}
|
|
4173
4194
|
function resolveProjectOrAll(db, explicit) {
|
|
4174
4195
|
if (explicit) {
|
|
@@ -4176,15 +4197,15 @@ function resolveProjectOrAll(db, explicit) {
|
|
|
4176
4197
|
return result;
|
|
4177
4198
|
}
|
|
4178
4199
|
const active = getActiveProject(db);
|
|
4179
|
-
if (active) return
|
|
4200
|
+
if (active) return ok2(active);
|
|
4180
4201
|
const projects = getPmNotes(db, "project");
|
|
4181
4202
|
if (projects.length === 1) {
|
|
4182
4203
|
const meta = JSON.parse(projects[0].metadata);
|
|
4183
4204
|
const prefix = meta.prefix;
|
|
4184
4205
|
setActiveProject(db, prefix);
|
|
4185
|
-
return
|
|
4206
|
+
return ok2(prefix);
|
|
4186
4207
|
}
|
|
4187
|
-
return
|
|
4208
|
+
return ok2(null);
|
|
4188
4209
|
}
|
|
4189
4210
|
function getAllProjectPrefixes(db) {
|
|
4190
4211
|
const projects = getPmNotes(db, "project");
|
|
@@ -4241,19 +4262,28 @@ function buildProjectMarkdown(input) {
|
|
|
4241
4262
|
if (input.wipLimit !== void 0) {
|
|
4242
4263
|
lines2.push(`wip_limit: ${input.wipLimit}`);
|
|
4243
4264
|
}
|
|
4265
|
+
if (input.path) lines2.push(`path: "${input.path}"`);
|
|
4266
|
+
if (input.remote) lines2.push(`remote: "${input.remote}"`);
|
|
4267
|
+
if (input.defaultBranch) lines2.push(`default_branch: "${input.defaultBranch}"`);
|
|
4268
|
+
if (input.reviewThreshold !== void 0) lines2.push(`review_threshold: ${input.reviewThreshold}`);
|
|
4269
|
+
if (input.packageName) lines2.push(`package_name: "${input.packageName}"`);
|
|
4270
|
+
if (input.packageManager) lines2.push(`package_manager: "${input.packageManager}"`);
|
|
4271
|
+
if (input.commands) lines2.push(`commands: '${JSON.stringify(input.commands)}'`);
|
|
4272
|
+
if (input.branchPrefix) lines2.push(`branch_prefix: '${JSON.stringify(input.branchPrefix)}'`);
|
|
4273
|
+
if (input.notes) lines2.push(`notes: '${JSON.stringify(input.notes)}'`);
|
|
4244
4274
|
lines2.push(`created: ${now}`, `modified: ${now}`, "---", "", `# ${input.name}`, "");
|
|
4245
4275
|
return lines2.join("\n");
|
|
4246
4276
|
}
|
|
4247
4277
|
async function createProject(db, config, embedder, input) {
|
|
4248
4278
|
if (!validatePrefix(input.prefix)) {
|
|
4249
|
-
return
|
|
4279
|
+
return fail2(
|
|
4250
4280
|
"INVALID_INPUT",
|
|
4251
4281
|
`Invalid prefix "${input.prefix}": must be 2-5 uppercase alphanumeric characters`
|
|
4252
4282
|
);
|
|
4253
4283
|
}
|
|
4254
4284
|
const existing = getPmNotes(db, "project", { prefix: input.prefix });
|
|
4255
4285
|
if (existing.length > 0) {
|
|
4256
|
-
return
|
|
4286
|
+
return fail2("PROJECT_EXISTS", `Project with prefix "${input.prefix}" already exists`);
|
|
4257
4287
|
}
|
|
4258
4288
|
const filePath = projectFilePath(config, input.prefix);
|
|
4259
4289
|
const dir = dirname3(filePath);
|
|
@@ -4269,18 +4299,48 @@ async function createProject(db, config, embedder, input) {
|
|
|
4269
4299
|
prefix: input.prefix,
|
|
4270
4300
|
status: "active",
|
|
4271
4301
|
phase: input.phase,
|
|
4272
|
-
wip_limit: input.wipLimit
|
|
4302
|
+
wip_limit: input.wipLimit,
|
|
4303
|
+
path: input.path,
|
|
4304
|
+
remote: input.remote,
|
|
4305
|
+
default_branch: input.defaultBranch,
|
|
4306
|
+
review_threshold: input.reviewThreshold,
|
|
4307
|
+
package_name: input.packageName,
|
|
4308
|
+
package_manager: input.packageManager,
|
|
4309
|
+
commands: input.commands,
|
|
4310
|
+
branch_prefix: input.branchPrefix,
|
|
4311
|
+
notes: input.notes
|
|
4273
4312
|
};
|
|
4274
|
-
return
|
|
4313
|
+
return ok2(metadata);
|
|
4275
4314
|
}
|
|
4276
4315
|
function projectMetaFromRecord(meta) {
|
|
4316
|
+
const parseJsonField = (val) => {
|
|
4317
|
+
if (!val) return void 0;
|
|
4318
|
+
if (typeof val === "object") return val;
|
|
4319
|
+
if (typeof val === "string") {
|
|
4320
|
+
try {
|
|
4321
|
+
return JSON.parse(val);
|
|
4322
|
+
} catch {
|
|
4323
|
+
return void 0;
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
return void 0;
|
|
4327
|
+
};
|
|
4277
4328
|
return {
|
|
4278
4329
|
title: meta.title ?? void 0,
|
|
4279
4330
|
display_id: meta.display_id,
|
|
4280
4331
|
prefix: meta.prefix,
|
|
4281
4332
|
status: meta.status,
|
|
4282
4333
|
phase: meta.phase,
|
|
4283
|
-
wip_limit: meta.wip_limit
|
|
4334
|
+
wip_limit: meta.wip_limit,
|
|
4335
|
+
path: meta.path,
|
|
4336
|
+
remote: meta.remote,
|
|
4337
|
+
default_branch: meta.default_branch,
|
|
4338
|
+
review_threshold: meta.review_threshold,
|
|
4339
|
+
package_name: meta.package_name,
|
|
4340
|
+
package_manager: meta.package_manager,
|
|
4341
|
+
commands: parseJsonField(meta.commands),
|
|
4342
|
+
branch_prefix: parseJsonField(meta.branch_prefix),
|
|
4343
|
+
notes: parseJsonField(meta.notes)
|
|
4284
4344
|
};
|
|
4285
4345
|
}
|
|
4286
4346
|
function listProjects(db) {
|
|
@@ -4289,7 +4349,7 @@ function listProjects(db) {
|
|
|
4289
4349
|
const meta = JSON.parse(note.metadata);
|
|
4290
4350
|
return projectMetaFromRecord(meta);
|
|
4291
4351
|
});
|
|
4292
|
-
return
|
|
4352
|
+
return ok2(projects);
|
|
4293
4353
|
}
|
|
4294
4354
|
function availableProjectsList(db) {
|
|
4295
4355
|
const notes = getPmNotes(db, "project");
|
|
@@ -4303,20 +4363,20 @@ function availableProjectsList(db) {
|
|
|
4303
4363
|
function getProject(db, prefix) {
|
|
4304
4364
|
const notes = getPmNotes(db, "project", { prefix });
|
|
4305
4365
|
if (notes.length === 0) {
|
|
4306
|
-
return
|
|
4366
|
+
return fail2("NOT_FOUND", `Project "${prefix}" not found.${availableProjectsList(db)}`);
|
|
4307
4367
|
}
|
|
4308
4368
|
const meta = JSON.parse(notes[0].metadata);
|
|
4309
|
-
return
|
|
4369
|
+
return ok2(projectMetaFromRecord(meta));
|
|
4310
4370
|
}
|
|
4311
4371
|
async function updateProject(db, config, embedder, prefix, updates) {
|
|
4312
4372
|
const notes = getPmNotes(db, "project", { prefix });
|
|
4313
4373
|
if (notes.length === 0) {
|
|
4314
|
-
return
|
|
4374
|
+
return fail2("NOT_FOUND", `Project "${prefix}" not found.${availableProjectsList(db)}`);
|
|
4315
4375
|
}
|
|
4316
4376
|
const note = notes[0];
|
|
4317
4377
|
const filePath = note.filePath;
|
|
4318
4378
|
if (!existsSync7(filePath)) {
|
|
4319
|
-
return
|
|
4379
|
+
return fail2("NOT_FOUND", `Project file not found at "${filePath}"`);
|
|
4320
4380
|
}
|
|
4321
4381
|
const content = readFileSync8(filePath, "utf-8");
|
|
4322
4382
|
let updated = content;
|
|
@@ -4329,6 +4389,41 @@ async function updateProject(db, config, embedder, prefix, updates) {
|
|
|
4329
4389
|
if (updates.wip_limit !== void 0) {
|
|
4330
4390
|
updated = replaceFrontmatterField(updated, "wip_limit", String(updates.wip_limit));
|
|
4331
4391
|
}
|
|
4392
|
+
if (updates.path !== void 0) {
|
|
4393
|
+
updated = replaceFrontmatterField(updated, "path", `"${updates.path}"`);
|
|
4394
|
+
}
|
|
4395
|
+
if (updates.remote !== void 0) {
|
|
4396
|
+
updated = replaceFrontmatterField(updated, "remote", `"${updates.remote}"`);
|
|
4397
|
+
}
|
|
4398
|
+
if (updates.default_branch !== void 0) {
|
|
4399
|
+
updated = replaceFrontmatterField(updated, "default_branch", `"${updates.default_branch}"`);
|
|
4400
|
+
}
|
|
4401
|
+
if (updates.review_threshold !== void 0) {
|
|
4402
|
+
updated = replaceFrontmatterField(
|
|
4403
|
+
updated,
|
|
4404
|
+
"review_threshold",
|
|
4405
|
+
String(updates.review_threshold)
|
|
4406
|
+
);
|
|
4407
|
+
}
|
|
4408
|
+
if (updates.package_name !== void 0) {
|
|
4409
|
+
updated = replaceFrontmatterField(updated, "package_name", `"${updates.package_name}"`);
|
|
4410
|
+
}
|
|
4411
|
+
if (updates.package_manager !== void 0) {
|
|
4412
|
+
updated = replaceFrontmatterField(updated, "package_manager", `"${updates.package_manager}"`);
|
|
4413
|
+
}
|
|
4414
|
+
if (updates.commands !== void 0) {
|
|
4415
|
+
updated = replaceFrontmatterField(updated, "commands", `'${JSON.stringify(updates.commands)}'`);
|
|
4416
|
+
}
|
|
4417
|
+
if (updates.branch_prefix !== void 0) {
|
|
4418
|
+
updated = replaceFrontmatterField(
|
|
4419
|
+
updated,
|
|
4420
|
+
"branch_prefix",
|
|
4421
|
+
`'${JSON.stringify(updates.branch_prefix)}'`
|
|
4422
|
+
);
|
|
4423
|
+
}
|
|
4424
|
+
if (updates.notes !== void 0) {
|
|
4425
|
+
updated = replaceFrontmatterField(updated, "notes", `'${JSON.stringify(updates.notes)}'`);
|
|
4426
|
+
}
|
|
4332
4427
|
const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4333
4428
|
updated = replaceFrontmatterField(updated, "modified", now);
|
|
4334
4429
|
writeFileSync7(filePath, updated, "utf-8");
|
|
@@ -4336,17 +4431,17 @@ async function updateProject(db, config, embedder, prefix, updates) {
|
|
|
4336
4431
|
const noteId = await indexSingleFile(db, embedder, filePath, updated, hash, Date.now());
|
|
4337
4432
|
const refreshedNote = db.getNoteById(noteId);
|
|
4338
4433
|
const meta = JSON.parse(refreshedNote.metadata);
|
|
4339
|
-
return
|
|
4434
|
+
return ok2(projectMetaFromRecord(meta));
|
|
4340
4435
|
}
|
|
4341
4436
|
async function deleteProject(db, config, prefix, force) {
|
|
4342
4437
|
const notes = getPmNotes(db, "project", { prefix });
|
|
4343
4438
|
if (notes.length === 0) {
|
|
4344
|
-
return
|
|
4439
|
+
return fail2("NOT_FOUND", `Project "${prefix}" not found.${availableProjectsList(db)}`);
|
|
4345
4440
|
}
|
|
4346
4441
|
const projectNotes = getProjectNotes(db, prefix);
|
|
4347
4442
|
const nonProjectNotes = projectNotes.filter((n) => n.id !== notes[0].id);
|
|
4348
4443
|
if (nonProjectNotes.length > 0 && !force) {
|
|
4349
|
-
return
|
|
4444
|
+
return fail2(
|
|
4350
4445
|
"HAS_DEPENDENTS",
|
|
4351
4446
|
`Project "${prefix}" has ${nonProjectNotes.length} dependent note(s). Use force to delete.`,
|
|
4352
4447
|
{ count: nonProjectNotes.length }
|
|
@@ -4370,7 +4465,7 @@ async function deleteProject(db, config, prefix, force) {
|
|
|
4370
4465
|
if (existsSync7(projectDir)) {
|
|
4371
4466
|
rmSync2(projectDir, { recursive: true, force: true });
|
|
4372
4467
|
}
|
|
4373
|
-
return
|
|
4468
|
+
return ok2(void 0);
|
|
4374
4469
|
}
|
|
4375
4470
|
function replaceFrontmatterField(content, field, value) {
|
|
4376
4471
|
const endOfFrontmatter = content.indexOf("\n---", 4);
|
|
@@ -4378,7 +4473,8 @@ function replaceFrontmatterField(content, field, value) {
|
|
|
4378
4473
|
const frontmatter = content.slice(0, endOfFrontmatter);
|
|
4379
4474
|
const rest = content.slice(endOfFrontmatter);
|
|
4380
4475
|
const fieldRegex = new RegExp(`^${field}:.*$`, "m");
|
|
4381
|
-
const
|
|
4476
|
+
const isAlreadyQuoted = value.startsWith("'") && value.endsWith("'") || value.startsWith('"') && value.endsWith('"');
|
|
4477
|
+
const quoted = isAlreadyQuoted ? value : value.includes(" ") ? `"${value}"` : value;
|
|
4382
4478
|
if (fieldRegex.test(frontmatter)) {
|
|
4383
4479
|
return frontmatter.replace(fieldRegex, `${field}: ${quoted}`) + rest;
|
|
4384
4480
|
}
|
|
@@ -4393,23 +4489,24 @@ import { join as join12, dirname as dirname6 } from "path";
|
|
|
4393
4489
|
|
|
4394
4490
|
// src/modules/pm/engine/state-machine.ts
|
|
4395
4491
|
var TRANSITIONS = {
|
|
4396
|
-
pending: ["claimed", "blocked", "cancelled"],
|
|
4492
|
+
pending: ["claimed", "blocked", "cancelled", "pruned"],
|
|
4397
4493
|
claimed: ["in-progress", "pending", "cancelled"],
|
|
4398
4494
|
"in-progress": ["done", "blocked", "cancelled", "pending"],
|
|
4399
4495
|
done: [],
|
|
4400
|
-
blocked: ["pending", "cancelled"],
|
|
4401
|
-
cancelled: []
|
|
4496
|
+
blocked: ["pending", "cancelled", "pruned"],
|
|
4497
|
+
cancelled: [],
|
|
4498
|
+
pruned: []
|
|
4402
4499
|
};
|
|
4403
4500
|
var DEFAULT_STALE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
4404
4501
|
function validateTransition(from, to) {
|
|
4405
4502
|
const allowed = TRANSITIONS[from];
|
|
4406
4503
|
if (allowed.includes(to)) {
|
|
4407
|
-
return
|
|
4504
|
+
return ok2(void 0);
|
|
4408
4505
|
}
|
|
4409
4506
|
const validList = allowed.length > 0 ? allowed.join(", ") : "none (terminal state)";
|
|
4410
4507
|
const hint = buildTransitionHint(from, to);
|
|
4411
4508
|
const message = `Cannot transition from '${from}' to '${to}'. Valid: ${validList}.${hint}`;
|
|
4412
|
-
return
|
|
4509
|
+
return fail2("INVALID_TRANSITION", message, { from, to });
|
|
4413
4510
|
}
|
|
4414
4511
|
function buildTransitionHint(from, to) {
|
|
4415
4512
|
if (from === "pending" && to === "done") {
|
|
@@ -4423,6 +4520,7 @@ function buildTransitionHint(from, to) {
|
|
|
4423
4520
|
function computeVirtualState(input) {
|
|
4424
4521
|
const states = [];
|
|
4425
4522
|
const { status, dependenciesComplete, hasDependencies, claimedAt, dueDate } = input;
|
|
4523
|
+
if (status === "pruned") return states;
|
|
4426
4524
|
if (status === "pending") {
|
|
4427
4525
|
if (!hasDependencies || dependenciesComplete) {
|
|
4428
4526
|
states.push("+READY", "+ELIGIBLE");
|
|
@@ -4441,7 +4539,7 @@ function computeVirtualState(input) {
|
|
|
4441
4539
|
states.push("+STALE");
|
|
4442
4540
|
}
|
|
4443
4541
|
}
|
|
4444
|
-
if (dueDate && !["done", "cancelled"].includes(status)) {
|
|
4542
|
+
if (dueDate && !["done", "cancelled", "pruned"].includes(status)) {
|
|
4445
4543
|
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
4446
4544
|
const due = /* @__PURE__ */ new Date(dueDate + "T23:59:59");
|
|
4447
4545
|
if (due < now) {
|
|
@@ -4519,7 +4617,10 @@ function computeEligible(db, prefix) {
|
|
|
4519
4617
|
for (const t of tasks) {
|
|
4520
4618
|
if (t.status !== "pending") continue;
|
|
4521
4619
|
const deps = graph.get(t.displayId) ?? [];
|
|
4522
|
-
const allDepsDone = deps.every((dep) =>
|
|
4620
|
+
const allDepsDone = deps.every((dep) => {
|
|
4621
|
+
const s = statusByDisplay.get(dep);
|
|
4622
|
+
return s === "done" || s === "pruned";
|
|
4623
|
+
});
|
|
4523
4624
|
if (allDepsDone) {
|
|
4524
4625
|
eligible.push(t.displayId);
|
|
4525
4626
|
}
|
|
@@ -4531,7 +4632,7 @@ function detectCycles(graph) {
|
|
|
4531
4632
|
const visited = /* @__PURE__ */ new Set();
|
|
4532
4633
|
const inStack = /* @__PURE__ */ new Set();
|
|
4533
4634
|
const path = [];
|
|
4534
|
-
function
|
|
4635
|
+
function dfs2(node) {
|
|
4535
4636
|
if (inStack.has(node)) {
|
|
4536
4637
|
const cycleStart = path.indexOf(node);
|
|
4537
4638
|
cycles.push(path.slice(cycleStart).concat(node));
|
|
@@ -4542,20 +4643,22 @@ function detectCycles(graph) {
|
|
|
4542
4643
|
inStack.add(node);
|
|
4543
4644
|
path.push(node);
|
|
4544
4645
|
for (const neighbor of graph.get(node) ?? []) {
|
|
4545
|
-
|
|
4646
|
+
dfs2(neighbor);
|
|
4546
4647
|
}
|
|
4547
4648
|
path.pop();
|
|
4548
4649
|
inStack.delete(node);
|
|
4549
4650
|
}
|
|
4550
4651
|
for (const node of graph.keys()) {
|
|
4551
|
-
|
|
4652
|
+
dfs2(node);
|
|
4552
4653
|
}
|
|
4553
4654
|
return cycles;
|
|
4554
4655
|
}
|
|
4555
4656
|
function computeWaves(db, prefix) {
|
|
4556
4657
|
const tasks = getProjectTasks(db, prefix);
|
|
4557
4658
|
const graph = buildDependencyGraph(db, prefix);
|
|
4558
|
-
const activeTasks = tasks.filter(
|
|
4659
|
+
const activeTasks = tasks.filter(
|
|
4660
|
+
(t) => t.status !== "done" && t.status !== "cancelled" && t.status !== "pruned"
|
|
4661
|
+
);
|
|
4559
4662
|
const activeIds = new Set(activeTasks.map((t) => t.displayId));
|
|
4560
4663
|
const statusByDisplay = /* @__PURE__ */ new Map();
|
|
4561
4664
|
for (const t of tasks) {
|
|
@@ -4748,7 +4851,7 @@ function computeHash(task, deps, decisions, prompt) {
|
|
|
4748
4851
|
function assembleContext(db, taskDisplayId, options) {
|
|
4749
4852
|
const taskNotes = getPmNotes(db, "task", { display_id: taskDisplayId });
|
|
4750
4853
|
if (taskNotes.length === 0) {
|
|
4751
|
-
return
|
|
4854
|
+
return fail2("NOT_FOUND", `Task "${taskDisplayId}" not found`);
|
|
4752
4855
|
}
|
|
4753
4856
|
const taskNote = taskNotes[0];
|
|
4754
4857
|
const task = JSON.parse(taskNote.metadata);
|
|
@@ -4759,7 +4862,7 @@ function assembleContext(db, taskDisplayId, options) {
|
|
|
4759
4862
|
const decisions = findImpactingDecisions(db, taskDisplayId, task.project);
|
|
4760
4863
|
const prompt = findPromptContent(db, taskDisplayId, task.project);
|
|
4761
4864
|
const contextHash = computeHash(task, dependencies, decisions, prompt);
|
|
4762
|
-
return
|
|
4865
|
+
return ok2({
|
|
4763
4866
|
task,
|
|
4764
4867
|
body,
|
|
4765
4868
|
workstream,
|
|
@@ -4845,7 +4948,7 @@ async function assembleDispatch(db, embedder, config, taskDisplayId, options) {
|
|
|
4845
4948
|
const wsDescription = ctx.workstream ? findWorkstreamDescription(db, ctx.task.project, ctx.task.workstream) : "";
|
|
4846
4949
|
const allTasksResult = listTasks(db, ctx.task.project);
|
|
4847
4950
|
const downstreamDependents = allTasksResult.ok ? allTasksResult.data.filter((t) => t.depends_on?.includes(taskDisplayId)).map((t) => ({ displayId: t.display_id, title: t.title ?? t.display_id })) : [];
|
|
4848
|
-
return
|
|
4951
|
+
return ok2({
|
|
4849
4952
|
...ctx,
|
|
4850
4953
|
peerTasks,
|
|
4851
4954
|
workstreamDescription: wsDescription,
|
|
@@ -4861,7 +4964,7 @@ function findWorkstreamDescription(db, project, workstreamNum) {
|
|
|
4861
4964
|
function assembleProjectContext(db, prefix) {
|
|
4862
4965
|
const projectNotes = getPmNotes(db, "project", { prefix });
|
|
4863
4966
|
if (projectNotes.length === 0) {
|
|
4864
|
-
return
|
|
4967
|
+
return fail2("NOT_FOUND", `Project "${prefix}" not found`);
|
|
4865
4968
|
}
|
|
4866
4969
|
const projectNote = projectNotes[0];
|
|
4867
4970
|
const meta = JSON.parse(projectNote.metadata);
|
|
@@ -4883,14 +4986,14 @@ function assembleProjectContext(db, prefix) {
|
|
|
4883
4986
|
});
|
|
4884
4987
|
}
|
|
4885
4988
|
const allTasks = listTasks(db, prefix, { priority: "critical" });
|
|
4886
|
-
const criticalTasks = (allTasks.ok ? allTasks.data : []).filter((t) => t.status !== "done" && t.status !== "cancelled").map(({ virtualStates: _vs, ...rest }) => rest);
|
|
4989
|
+
const criticalTasks = (allTasks.ok ? allTasks.data : []).filter((t) => t.status !== "done" && t.status !== "cancelled" && t.status !== "pruned").map(({ virtualStates: _vs, ...rest }) => rest);
|
|
4887
4990
|
const allProjectTasks = listTasks(db, prefix);
|
|
4888
4991
|
const dist = {};
|
|
4889
4992
|
for (const t of allProjectTasks.ok ? allProjectTasks.data : []) {
|
|
4890
4993
|
dist[t.status] = (dist[t.status] ?? 0) + 1;
|
|
4891
4994
|
}
|
|
4892
4995
|
const decisions = findProjectDecisions(db, prefix);
|
|
4893
|
-
return
|
|
4996
|
+
return ok2({
|
|
4894
4997
|
project: {
|
|
4895
4998
|
prefix,
|
|
4896
4999
|
name: meta.title ?? meta.name ?? prefix,
|
|
@@ -4907,7 +5010,7 @@ function assembleProjectContext(db, prefix) {
|
|
|
4907
5010
|
async function assembleWorkstreamContext(db, wsDisplayId, embedder, config) {
|
|
4908
5011
|
const wsNotes = getPmNotes(db, "workstream", { display_id: wsDisplayId });
|
|
4909
5012
|
if (wsNotes.length === 0) {
|
|
4910
|
-
return
|
|
5013
|
+
return fail2("NOT_FOUND", `Workstream "${wsDisplayId}" not found`);
|
|
4911
5014
|
}
|
|
4912
5015
|
const wsNote = wsNotes[0];
|
|
4913
5016
|
const wsMeta = JSON.parse(wsNote.metadata);
|
|
@@ -4953,7 +5056,7 @@ async function assembleWorkstreamContext(db, wsDisplayId, embedder, config) {
|
|
|
4953
5056
|
}
|
|
4954
5057
|
}
|
|
4955
5058
|
}
|
|
4956
|
-
return
|
|
5059
|
+
return ok2({
|
|
4957
5060
|
workstream: {
|
|
4958
5061
|
displayId: wsDisplayId,
|
|
4959
5062
|
title: wsMeta.title ?? wsNote.title,
|
|
@@ -5069,7 +5172,7 @@ function workstreamMetaFromRecord(meta, description) {
|
|
|
5069
5172
|
async function createWorkstream(db, config, embedder, input) {
|
|
5070
5173
|
const projectNotes = getPmNotes(db, "project", { prefix: input.project });
|
|
5071
5174
|
if (projectNotes.length === 0) {
|
|
5072
|
-
return
|
|
5175
|
+
return fail2("NOT_FOUND", `Project "${input.project}" not found`);
|
|
5073
5176
|
}
|
|
5074
5177
|
const number = nextWorkstreamNumber(db, input.project);
|
|
5075
5178
|
const displayId = formatDisplayId(input.project, number);
|
|
@@ -5094,7 +5197,7 @@ async function createWorkstream(db, config, embedder, input) {
|
|
|
5094
5197
|
number,
|
|
5095
5198
|
status: "active"
|
|
5096
5199
|
};
|
|
5097
|
-
return
|
|
5200
|
+
return ok2(metadata);
|
|
5098
5201
|
}
|
|
5099
5202
|
function listWorkstreams(db, prefix) {
|
|
5100
5203
|
const notes = getPmNotes(db, "workstream", { project: prefix });
|
|
@@ -5103,26 +5206,26 @@ function listWorkstreams(db, prefix) {
|
|
|
5103
5206
|
const description = extractDescription(note.filePath);
|
|
5104
5207
|
return workstreamMetaFromRecord(meta, description);
|
|
5105
5208
|
});
|
|
5106
|
-
return
|
|
5209
|
+
return ok2(workstreams);
|
|
5107
5210
|
}
|
|
5108
5211
|
function getWorkstream(db, displayId) {
|
|
5109
5212
|
const notes = getPmNotes(db, "workstream", { display_id: displayId });
|
|
5110
5213
|
if (notes.length === 0) {
|
|
5111
|
-
return
|
|
5214
|
+
return fail2("NOT_FOUND", `Workstream "${displayId}" not found`);
|
|
5112
5215
|
}
|
|
5113
5216
|
const meta = JSON.parse(notes[0].metadata);
|
|
5114
5217
|
const description = extractDescription(notes[0].filePath);
|
|
5115
|
-
return
|
|
5218
|
+
return ok2(workstreamMetaFromRecord(meta, description));
|
|
5116
5219
|
}
|
|
5117
5220
|
async function updateWorkstream(db, config, embedder, displayId, updates) {
|
|
5118
5221
|
const notes = getPmNotes(db, "workstream", { display_id: displayId });
|
|
5119
5222
|
if (notes.length === 0) {
|
|
5120
|
-
return
|
|
5223
|
+
return fail2("NOT_FOUND", `Workstream "${displayId}" not found`);
|
|
5121
5224
|
}
|
|
5122
5225
|
const note = notes[0];
|
|
5123
5226
|
const filePath = note.filePath;
|
|
5124
5227
|
if (!existsSync9(filePath)) {
|
|
5125
|
-
return
|
|
5228
|
+
return fail2("NOT_FOUND", `Workstream file not found at "${filePath}"`);
|
|
5126
5229
|
}
|
|
5127
5230
|
const content = readFileSync10(filePath, "utf-8");
|
|
5128
5231
|
let updated = content;
|
|
@@ -5136,23 +5239,23 @@ async function updateWorkstream(db, config, embedder, displayId, updates) {
|
|
|
5136
5239
|
const noteId = await indexSingleFile(db, embedder, filePath, updated, hash, Date.now());
|
|
5137
5240
|
const refreshedNote = db.getNoteById(noteId);
|
|
5138
5241
|
const meta = JSON.parse(refreshedNote.metadata);
|
|
5139
|
-
return
|
|
5242
|
+
return ok2(workstreamMetaFromRecord(meta));
|
|
5140
5243
|
}
|
|
5141
5244
|
async function deleteWorkstream(db, config, displayId, force) {
|
|
5142
5245
|
const notes = getPmNotes(db, "workstream", { display_id: displayId });
|
|
5143
5246
|
if (notes.length === 0) {
|
|
5144
|
-
return
|
|
5247
|
+
return fail2("NOT_FOUND", `Workstream "${displayId}" not found`);
|
|
5145
5248
|
}
|
|
5146
5249
|
const parsed = parseDisplayId(displayId);
|
|
5147
5250
|
if (!parsed || parsed.workstream === void 0) {
|
|
5148
|
-
return
|
|
5251
|
+
return fail2("INVALID_INPUT", `Invalid workstream display ID "${displayId}"`);
|
|
5149
5252
|
}
|
|
5150
5253
|
const taskNotes = getPmNotes(db, "task", {
|
|
5151
5254
|
project: parsed.prefix,
|
|
5152
5255
|
workstream: parsed.workstream
|
|
5153
5256
|
});
|
|
5154
5257
|
if (taskNotes.length > 0 && !force) {
|
|
5155
|
-
return
|
|
5258
|
+
return fail2(
|
|
5156
5259
|
"HAS_DEPENDENTS",
|
|
5157
5260
|
`Workstream "${displayId}" has ${taskNotes.length} task(s). Use force to delete.`,
|
|
5158
5261
|
{ count: taskNotes.length }
|
|
@@ -5171,7 +5274,7 @@ async function deleteWorkstream(db, config, displayId, force) {
|
|
|
5171
5274
|
if (existsSync9(wsNote.filePath)) {
|
|
5172
5275
|
unlinkSync3(wsNote.filePath);
|
|
5173
5276
|
}
|
|
5174
|
-
return
|
|
5277
|
+
return ok2(void 0);
|
|
5175
5278
|
}
|
|
5176
5279
|
|
|
5177
5280
|
// src/modules/pm/engine/activity.ts
|
|
@@ -5367,15 +5470,15 @@ function mergeDependsOn(db, noteId, frontmatterDeps) {
|
|
|
5367
5470
|
async function createTask(db, config, embedder, input) {
|
|
5368
5471
|
const projectNotes = getPmNotes(db, "project", { prefix: input.project });
|
|
5369
5472
|
if (projectNotes.length === 0) {
|
|
5370
|
-
return
|
|
5473
|
+
return fail2("NOT_FOUND", `Project "${input.project}" not found`);
|
|
5371
5474
|
}
|
|
5372
5475
|
const wsDisplayId = formatDisplayId(input.project, input.workstream);
|
|
5373
5476
|
const wsNotes = getPmNotes(db, "workstream", { display_id: wsDisplayId });
|
|
5374
5477
|
if (wsNotes.length === 0) {
|
|
5375
|
-
return
|
|
5478
|
+
return fail2("NOT_FOUND", `Workstream "${wsDisplayId}" not found`);
|
|
5376
5479
|
}
|
|
5377
5480
|
if (!input.description?.trim()) {
|
|
5378
|
-
return
|
|
5481
|
+
return fail2(
|
|
5379
5482
|
"INVALID_INPUT",
|
|
5380
5483
|
"Task description is required \u2014 tasks without body content are invisible to search"
|
|
5381
5484
|
);
|
|
@@ -5385,7 +5488,7 @@ async function createTask(db, config, embedder, input) {
|
|
|
5385
5488
|
for (const depDisplayId of input.dependsOn) {
|
|
5386
5489
|
const resolved = resolveDisplayId(db, depDisplayId);
|
|
5387
5490
|
if (!resolved.ok) {
|
|
5388
|
-
return
|
|
5491
|
+
return fail2("NOT_FOUND", `Dependency "${depDisplayId}" not found`);
|
|
5389
5492
|
}
|
|
5390
5493
|
resolvedDepIds.push({ displayId: depDisplayId, noteId: resolved.data });
|
|
5391
5494
|
}
|
|
@@ -5445,7 +5548,7 @@ async function createTask(db, config, embedder, input) {
|
|
|
5445
5548
|
acceptance_criteria: input.acceptanceCriteria,
|
|
5446
5549
|
references: input.references
|
|
5447
5550
|
};
|
|
5448
|
-
return
|
|
5551
|
+
return ok2(metadata);
|
|
5449
5552
|
}
|
|
5450
5553
|
function listTasks(db, prefix, filters, listMode = "default") {
|
|
5451
5554
|
const VIRTUAL_STATE_FILTERS = {
|
|
@@ -5545,7 +5648,7 @@ function listTasks(db, prefix, filters, listMode = "default") {
|
|
|
5545
5648
|
);
|
|
5546
5649
|
}
|
|
5547
5650
|
const cleaned = tasks.map(({ _body, ...rest }) => rest);
|
|
5548
|
-
return
|
|
5651
|
+
return ok2(cleaned);
|
|
5549
5652
|
}
|
|
5550
5653
|
function didYouMeanTask(db, displayId) {
|
|
5551
5654
|
const parsed = parseDisplayId(displayId);
|
|
@@ -5565,7 +5668,7 @@ function getTask(db, displayId) {
|
|
|
5565
5668
|
if (notes.length === 0) {
|
|
5566
5669
|
const suggestion = didYouMeanTask(db, displayId);
|
|
5567
5670
|
const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
|
|
5568
|
-
return
|
|
5671
|
+
return fail2("NOT_FOUND", `Task "${displayId}" not found.${hint}`);
|
|
5569
5672
|
}
|
|
5570
5673
|
const taskNote = notes[0];
|
|
5571
5674
|
const meta = JSON.parse(taskNote.metadata);
|
|
@@ -5580,19 +5683,19 @@ function getTask(db, displayId) {
|
|
|
5580
5683
|
claimedAt: taskMeta.claimed_at,
|
|
5581
5684
|
dueDate: taskMeta.due_date
|
|
5582
5685
|
});
|
|
5583
|
-
return
|
|
5686
|
+
return ok2({ ...taskMeta, virtualStates });
|
|
5584
5687
|
}
|
|
5585
5688
|
async function updateTaskStatus(db, config, embedder, displayId, newStatus) {
|
|
5586
5689
|
const notes = getPmNotes(db, "task", { display_id: displayId });
|
|
5587
5690
|
if (notes.length === 0) {
|
|
5588
|
-
return
|
|
5691
|
+
return fail2("NOT_FOUND", `Task "${displayId}" not found`);
|
|
5589
5692
|
}
|
|
5590
5693
|
const note = notes[0];
|
|
5591
5694
|
const meta = JSON.parse(note.metadata);
|
|
5592
5695
|
const currentStatus = meta.status;
|
|
5593
5696
|
const transitionResult = validateTransition(currentStatus, newStatus);
|
|
5594
5697
|
if (!transitionResult.ok) {
|
|
5595
|
-
return
|
|
5698
|
+
return fail2(
|
|
5596
5699
|
"INVALID_TRANSITION",
|
|
5597
5700
|
transitionResult.error.message,
|
|
5598
5701
|
transitionResult.error.details
|
|
@@ -5600,7 +5703,7 @@ async function updateTaskStatus(db, config, embedder, displayId, newStatus) {
|
|
|
5600
5703
|
}
|
|
5601
5704
|
const filePath = note.filePath;
|
|
5602
5705
|
if (!existsSync11(filePath)) {
|
|
5603
|
-
return
|
|
5706
|
+
return fail2("NOT_FOUND", `Task file not found at "${filePath}"`);
|
|
5604
5707
|
}
|
|
5605
5708
|
let content = readFileSync11(filePath, "utf-8");
|
|
5606
5709
|
content = replaceFrontmatterField3(content, "status", newStatus);
|
|
@@ -5631,17 +5734,17 @@ async function updateTaskStatus(db, config, embedder, displayId, newStatus) {
|
|
|
5631
5734
|
newlyEligible
|
|
5632
5735
|
});
|
|
5633
5736
|
}
|
|
5634
|
-
return
|
|
5737
|
+
return ok2(taskMetaFromRecord(refreshedMeta));
|
|
5635
5738
|
}
|
|
5636
5739
|
async function updateTask(db, config, embedder, displayId, updates) {
|
|
5637
5740
|
const notes = getPmNotes(db, "task", { display_id: displayId });
|
|
5638
5741
|
if (notes.length === 0) {
|
|
5639
|
-
return
|
|
5742
|
+
return fail2("NOT_FOUND", `Task "${displayId}" not found`);
|
|
5640
5743
|
}
|
|
5641
5744
|
const note = notes[0];
|
|
5642
5745
|
const filePath = note.filePath;
|
|
5643
5746
|
if (!existsSync11(filePath)) {
|
|
5644
|
-
return
|
|
5747
|
+
return fail2("NOT_FOUND", `Task file not found at "${filePath}"`);
|
|
5645
5748
|
}
|
|
5646
5749
|
let content = readFileSync11(filePath, "utf-8");
|
|
5647
5750
|
if (updates.mode !== void 0) {
|
|
@@ -5666,19 +5769,19 @@ async function updateTask(db, config, embedder, displayId, updates) {
|
|
|
5666
5769
|
const noteId = await indexSingleFile(db, embedder, filePath, content, hash, Date.now());
|
|
5667
5770
|
const refreshedNote = db.getNoteById(noteId);
|
|
5668
5771
|
const refreshedMeta = JSON.parse(refreshedNote.metadata);
|
|
5669
|
-
return
|
|
5772
|
+
return ok2(taskMetaFromRecord(refreshedMeta));
|
|
5670
5773
|
}
|
|
5671
5774
|
async function deleteTask(db, config, displayId, force) {
|
|
5672
5775
|
const notes = getPmNotes(db, "task", { display_id: displayId });
|
|
5673
5776
|
if (notes.length === 0) {
|
|
5674
|
-
return
|
|
5777
|
+
return fail2("NOT_FOUND", `Task "${displayId}" not found`);
|
|
5675
5778
|
}
|
|
5676
5779
|
const taskNote = notes[0];
|
|
5677
5780
|
if (!force) {
|
|
5678
5781
|
const incomingRelations = db.getRelationsTo(taskNote.id);
|
|
5679
5782
|
const dependents = incomingRelations.filter((r) => r.type === "depends_on");
|
|
5680
5783
|
if (dependents.length > 0) {
|
|
5681
|
-
return
|
|
5784
|
+
return fail2(
|
|
5682
5785
|
"HAS_DEPENDENTS",
|
|
5683
5786
|
`Task "${displayId}" has ${dependents.length} dependent task(s). Use force to delete.`,
|
|
5684
5787
|
{ count: dependents.length }
|
|
@@ -5694,7 +5797,7 @@ async function deleteTask(db, config, displayId, force) {
|
|
|
5694
5797
|
if (contentDir && existsSync11(contentDir)) {
|
|
5695
5798
|
rmSync3(contentDir, { recursive: true, force: true });
|
|
5696
5799
|
}
|
|
5697
|
-
return
|
|
5800
|
+
return ok2(void 0);
|
|
5698
5801
|
}
|
|
5699
5802
|
|
|
5700
5803
|
// src/modules/pm/commands/activity.ts
|
|
@@ -5703,14 +5806,14 @@ import { existsSync as existsSync12, readFileSync as readFileSync12, unlinkSync
|
|
|
5703
5806
|
import { join as join13 } from "path";
|
|
5704
5807
|
function projectDeleteWithActivity(db, config, prefix, opts) {
|
|
5705
5808
|
if (!opts.confirm) {
|
|
5706
|
-
return
|
|
5809
|
+
return fail2(
|
|
5707
5810
|
"INVALID_INPUT",
|
|
5708
5811
|
"Deletion requires --confirm. This will delete the project and all associated notes."
|
|
5709
5812
|
);
|
|
5710
5813
|
}
|
|
5711
5814
|
const projectNotes = getPmNotes(db, "project", { prefix });
|
|
5712
5815
|
if (projectNotes.length === 0) {
|
|
5713
|
-
return
|
|
5816
|
+
return fail2("NOT_FOUND", `Project "${prefix}" not found.`);
|
|
5714
5817
|
}
|
|
5715
5818
|
const allProjectNotes = getProjectNotes(db, prefix);
|
|
5716
5819
|
const activityNotes = getPmNotes(db, "activity", { project: prefix });
|
|
@@ -5748,7 +5851,7 @@ function projectDeleteWithActivity(db, config, prefix, opts) {
|
|
|
5748
5851
|
if (existsSync12(projectDir)) {
|
|
5749
5852
|
rmSync4(projectDir, { recursive: true, force: true });
|
|
5750
5853
|
}
|
|
5751
|
-
return
|
|
5854
|
+
return ok2({
|
|
5752
5855
|
deletedCount: deletedIds.length,
|
|
5753
5856
|
untrackedCount,
|
|
5754
5857
|
deletedIds
|
|
@@ -6471,9 +6574,9 @@ function generateClaim() {
|
|
|
6471
6574
|
}
|
|
6472
6575
|
function validateClaimToken(expected, provided) {
|
|
6473
6576
|
if (expected === provided) {
|
|
6474
|
-
return
|
|
6577
|
+
return ok2(void 0);
|
|
6475
6578
|
}
|
|
6476
|
-
return
|
|
6579
|
+
return fail2("INVALID_CLAIM_TOKEN", "Claim token does not match", {
|
|
6477
6580
|
expected,
|
|
6478
6581
|
provided
|
|
6479
6582
|
});
|
|
@@ -6585,7 +6688,10 @@ function createTaskCommands() {
|
|
|
6585
6688
|
if (prefixes.length === 0) {
|
|
6586
6689
|
process.stderr.write(
|
|
6587
6690
|
formatError(
|
|
6588
|
-
pmError(
|
|
6691
|
+
pmError(
|
|
6692
|
+
"INVALID_INPUT",
|
|
6693
|
+
'No projects found. Run "brain pm onboard <name>" to create one.'
|
|
6694
|
+
),
|
|
6589
6695
|
!!opts.json
|
|
6590
6696
|
) + "\n"
|
|
6591
6697
|
);
|
|
@@ -6808,27 +6914,80 @@ function createTaskCommands() {
|
|
|
6808
6914
|
});
|
|
6809
6915
|
cmd.command("done").description('Mark task as done (low-level \u2014 use "brain pm complete" for full impact tracking)').argument("<id>", "Task display ID").option("--token <token>", "Claim token for verification").option("--json", "Output JSON").action(async (id, opts) => {
|
|
6810
6916
|
await withBrain(async (svc) => {
|
|
6811
|
-
const
|
|
6917
|
+
const displayId = id.toUpperCase();
|
|
6918
|
+
const redirectMsg = checkNamespaceMismatch(displayId, "task");
|
|
6812
6919
|
if (redirectMsg) {
|
|
6813
6920
|
process.stderr.write(`Error: ${redirectMsg}
|
|
6814
6921
|
`);
|
|
6815
6922
|
process.exitCode = 1;
|
|
6816
6923
|
return;
|
|
6817
6924
|
}
|
|
6925
|
+
const taskResult = getTask(svc.db, displayId);
|
|
6926
|
+
if (!taskResult.ok) {
|
|
6927
|
+
process.stderr.write(formatError(taskResult.error, !!opts.json) + "\n");
|
|
6928
|
+
process.exitCode = 1;
|
|
6929
|
+
return;
|
|
6930
|
+
}
|
|
6818
6931
|
if (opts.token) {
|
|
6819
|
-
|
|
6820
|
-
if (taskResult.ok && taskResult.data.claim_token && taskResult.data.claim_token !== opts.token) {
|
|
6932
|
+
if (taskResult.data.claim_token && taskResult.data.claim_token !== opts.token) {
|
|
6821
6933
|
process.stderr.write(
|
|
6822
6934
|
`Warning: Token mismatch (expected ${taskResult.data.claim_token}, got ${opts.token})
|
|
6823
6935
|
`
|
|
6824
6936
|
);
|
|
6825
6937
|
}
|
|
6826
6938
|
}
|
|
6939
|
+
const currentStatus = taskResult.data.status;
|
|
6940
|
+
if (currentStatus === "pending") {
|
|
6941
|
+
process.stderr.write(`Auto-claiming ${displayId}...
|
|
6942
|
+
`);
|
|
6943
|
+
const claim = generateClaim();
|
|
6944
|
+
const claimResult = await updateTaskMetadataFields(
|
|
6945
|
+
svc.db,
|
|
6946
|
+
svc.config,
|
|
6947
|
+
svc.embedder,
|
|
6948
|
+
displayId,
|
|
6949
|
+
{ status: "claimed", claim_token: claim.token, claimed_at: claim.claimedAt }
|
|
6950
|
+
);
|
|
6951
|
+
if (!claimResult.ok) {
|
|
6952
|
+
process.stderr.write(formatError(claimResult.error, !!opts.json) + "\n");
|
|
6953
|
+
process.exitCode = 1;
|
|
6954
|
+
return;
|
|
6955
|
+
}
|
|
6956
|
+
process.stderr.write(`Auto-starting ${displayId}...
|
|
6957
|
+
`);
|
|
6958
|
+
const startResult = await updateTaskStatus(
|
|
6959
|
+
svc.db,
|
|
6960
|
+
svc.config,
|
|
6961
|
+
svc.embedder,
|
|
6962
|
+
displayId,
|
|
6963
|
+
"in-progress"
|
|
6964
|
+
);
|
|
6965
|
+
if (!startResult.ok) {
|
|
6966
|
+
process.stderr.write(formatError(startResult.error, !!opts.json) + "\n");
|
|
6967
|
+
process.exitCode = 1;
|
|
6968
|
+
return;
|
|
6969
|
+
}
|
|
6970
|
+
} else if (currentStatus === "claimed") {
|
|
6971
|
+
process.stderr.write(`Auto-starting ${displayId}...
|
|
6972
|
+
`);
|
|
6973
|
+
const startResult = await updateTaskStatus(
|
|
6974
|
+
svc.db,
|
|
6975
|
+
svc.config,
|
|
6976
|
+
svc.embedder,
|
|
6977
|
+
displayId,
|
|
6978
|
+
"in-progress"
|
|
6979
|
+
);
|
|
6980
|
+
if (!startResult.ok) {
|
|
6981
|
+
process.stderr.write(formatError(startResult.error, !!opts.json) + "\n");
|
|
6982
|
+
process.exitCode = 1;
|
|
6983
|
+
return;
|
|
6984
|
+
}
|
|
6985
|
+
}
|
|
6827
6986
|
const result = await updateTaskStatus(
|
|
6828
6987
|
svc.db,
|
|
6829
6988
|
svc.config,
|
|
6830
6989
|
svc.embedder,
|
|
6831
|
-
|
|
6990
|
+
displayId,
|
|
6832
6991
|
"done"
|
|
6833
6992
|
);
|
|
6834
6993
|
if (!result.ok) {
|
|
@@ -7089,12 +7248,12 @@ ${field}: ${quoted}` + rest;
|
|
|
7089
7248
|
async function updateTaskMetadataFields(db, config, embedder, displayId, fields) {
|
|
7090
7249
|
const notes = getPmNotes(db, "task", { display_id: displayId });
|
|
7091
7250
|
if (notes.length === 0) {
|
|
7092
|
-
return
|
|
7251
|
+
return fail2("NOT_FOUND", `Task "${displayId}" not found`);
|
|
7093
7252
|
}
|
|
7094
7253
|
const note = notes[0];
|
|
7095
7254
|
const filePath = note.filePath;
|
|
7096
7255
|
if (!existsSync14(filePath)) {
|
|
7097
|
-
return
|
|
7256
|
+
return fail2("NOT_FOUND", `Task file not found at "${filePath}"`);
|
|
7098
7257
|
}
|
|
7099
7258
|
let content = readFileSync14(filePath, "utf-8");
|
|
7100
7259
|
for (const [key, value] of Object.entries(fields)) {
|
|
@@ -7199,18 +7358,18 @@ ${field}: ${quoted}` + rest;
|
|
|
7199
7358
|
async function createDecision(db, config, embedder, input) {
|
|
7200
7359
|
const projectNotes = getPmNotes(db, "project", { prefix: input.project });
|
|
7201
7360
|
if (projectNotes.length === 0) {
|
|
7202
|
-
return
|
|
7361
|
+
return fail2("NOT_FOUND", `Project "${input.project}" not found`);
|
|
7203
7362
|
}
|
|
7204
7363
|
const sourceResolved = resolveDisplayId(db, input.sourceTask);
|
|
7205
7364
|
if (!sourceResolved.ok) {
|
|
7206
|
-
return
|
|
7365
|
+
return fail2("NOT_FOUND", `Source task "${input.sourceTask}" not found`);
|
|
7207
7366
|
}
|
|
7208
7367
|
const resolvedImpacts = [];
|
|
7209
7368
|
if (input.impacts && input.impacts.length > 0) {
|
|
7210
7369
|
for (const impactId of input.impacts) {
|
|
7211
7370
|
const resolved = resolveDisplayId(db, impactId);
|
|
7212
7371
|
if (!resolved.ok) {
|
|
7213
|
-
return
|
|
7372
|
+
return fail2("NOT_FOUND", `Impact target "${impactId}" not found`);
|
|
7214
7373
|
}
|
|
7215
7374
|
resolvedImpacts.push({ displayId: impactId, noteId: resolved.data });
|
|
7216
7375
|
}
|
|
@@ -7241,7 +7400,7 @@ async function createDecision(db, config, embedder, input) {
|
|
|
7241
7400
|
source_task: input.sourceTask,
|
|
7242
7401
|
impacts: input.impacts
|
|
7243
7402
|
};
|
|
7244
|
-
return
|
|
7403
|
+
return ok2(metadata);
|
|
7245
7404
|
}
|
|
7246
7405
|
function listDecisions(db, prefix, filters) {
|
|
7247
7406
|
const filterObj = { project: prefix };
|
|
@@ -7252,12 +7411,12 @@ function listDecisions(db, prefix, filters) {
|
|
|
7252
7411
|
const meta = JSON.parse(note.metadata);
|
|
7253
7412
|
return decisionMetaFromRecord(meta);
|
|
7254
7413
|
});
|
|
7255
|
-
return
|
|
7414
|
+
return ok2(decisions);
|
|
7256
7415
|
}
|
|
7257
7416
|
function getDecision(db, displayId) {
|
|
7258
7417
|
const notes = getPmNotes(db, "decision", { display_id: displayId });
|
|
7259
7418
|
if (notes.length === 0) {
|
|
7260
|
-
return
|
|
7419
|
+
return fail2("NOT_FOUND", `Decision "${displayId}" not found`);
|
|
7261
7420
|
}
|
|
7262
7421
|
const note = notes[0];
|
|
7263
7422
|
const meta = JSON.parse(note.metadata);
|
|
@@ -7272,17 +7431,17 @@ function getDecision(db, displayId) {
|
|
|
7272
7431
|
content = headingEnd !== -1 ? body.slice(headingEnd + 1).trim() : "";
|
|
7273
7432
|
}
|
|
7274
7433
|
}
|
|
7275
|
-
return
|
|
7434
|
+
return ok2({ ...decisionMeta, content });
|
|
7276
7435
|
}
|
|
7277
7436
|
async function supersedeDecision(db, config, embedder, oldDisplayId, newInput) {
|
|
7278
7437
|
const oldNotes = getPmNotes(db, "decision", { display_id: oldDisplayId });
|
|
7279
7438
|
if (oldNotes.length === 0) {
|
|
7280
|
-
return
|
|
7439
|
+
return fail2("NOT_FOUND", `Decision "${oldDisplayId}" not found`);
|
|
7281
7440
|
}
|
|
7282
7441
|
const oldNote = oldNotes[0];
|
|
7283
7442
|
const oldFilePath = oldNote.filePath;
|
|
7284
7443
|
if (!existsSync15(oldFilePath)) {
|
|
7285
|
-
return
|
|
7444
|
+
return fail2("NOT_FOUND", `Decision file not found at "${oldFilePath}"`);
|
|
7286
7445
|
}
|
|
7287
7446
|
let oldContent = readFileSync15(oldFilePath, "utf-8");
|
|
7288
7447
|
oldContent = replaceFrontmatterField5(oldContent, "status", "superseded");
|
|
@@ -7313,7 +7472,7 @@ async function supersedeDecision(db, config, embedder, oldDisplayId, newInput) {
|
|
|
7313
7472
|
];
|
|
7314
7473
|
db.upsertRelations(newResolved.data, supersedesRelation);
|
|
7315
7474
|
}
|
|
7316
|
-
return
|
|
7475
|
+
return ok2({ old: oldDecision, new: newResult.data });
|
|
7317
7476
|
}
|
|
7318
7477
|
|
|
7319
7478
|
// src/modules/pm/data/prompt-ops.ts
|
|
@@ -7421,11 +7580,11 @@ async function supersedeCurrentPrompt(db, embedder, taskDisplayId, project) {
|
|
|
7421
7580
|
async function writePrompt(db, config, embedder, input) {
|
|
7422
7581
|
const projectNotes = getPmNotes(db, "project", { prefix: input.project });
|
|
7423
7582
|
if (projectNotes.length === 0) {
|
|
7424
|
-
return
|
|
7583
|
+
return fail2("NOT_FOUND", `Project "${input.project}" not found`);
|
|
7425
7584
|
}
|
|
7426
7585
|
const taskResolved = resolveDisplayId(db, input.task);
|
|
7427
7586
|
if (!taskResolved.ok) {
|
|
7428
|
-
return
|
|
7587
|
+
return fail2("NOT_FOUND", `Task "${input.task}" not found`);
|
|
7429
7588
|
}
|
|
7430
7589
|
await supersedeCurrentPrompt(db, embedder, input.task, input.project);
|
|
7431
7590
|
const version = currentVersionForTask(db, input.task, input.project) + 1;
|
|
@@ -7448,12 +7607,12 @@ async function writePrompt(db, config, embedder, input) {
|
|
|
7448
7607
|
prompt_status: status,
|
|
7449
7608
|
version
|
|
7450
7609
|
};
|
|
7451
|
-
return
|
|
7610
|
+
return ok2(metadata);
|
|
7452
7611
|
}
|
|
7453
7612
|
function getPrompt(db, taskDisplayId, version) {
|
|
7454
7613
|
const notes = getPmNotes(db, "prompt", { task: taskDisplayId });
|
|
7455
7614
|
if (notes.length === 0) {
|
|
7456
|
-
return
|
|
7615
|
+
return fail2("NOT_FOUND", `No prompts found for task "${taskDisplayId}"`);
|
|
7457
7616
|
}
|
|
7458
7617
|
let target = notes[0];
|
|
7459
7618
|
let targetMeta = JSON.parse(target.metadata);
|
|
@@ -7463,7 +7622,7 @@ function getPrompt(db, taskDisplayId, version) {
|
|
|
7463
7622
|
return m.version === version;
|
|
7464
7623
|
});
|
|
7465
7624
|
if (!match) {
|
|
7466
|
-
return
|
|
7625
|
+
return fail2("NOT_FOUND", `Prompt version ${version} not found for task "${taskDisplayId}"`);
|
|
7467
7626
|
}
|
|
7468
7627
|
target = match;
|
|
7469
7628
|
targetMeta = JSON.parse(match.metadata);
|
|
@@ -7488,7 +7647,7 @@ function getPrompt(db, taskDisplayId, version) {
|
|
|
7488
7647
|
content = headingEnd !== -1 ? body.slice(headingEnd + 1).trim() : "";
|
|
7489
7648
|
}
|
|
7490
7649
|
}
|
|
7491
|
-
return
|
|
7650
|
+
return ok2({ ...promptMetaFromRecord(targetMeta), content });
|
|
7492
7651
|
}
|
|
7493
7652
|
function listPrompts(db, prefix, filters) {
|
|
7494
7653
|
const filterObj = { project: prefix };
|
|
@@ -7499,19 +7658,19 @@ function listPrompts(db, prefix, filters) {
|
|
|
7499
7658
|
const meta = JSON.parse(note.metadata);
|
|
7500
7659
|
return promptMetaFromRecord(meta);
|
|
7501
7660
|
});
|
|
7502
|
-
return
|
|
7661
|
+
return ok2(prompts);
|
|
7503
7662
|
}
|
|
7504
7663
|
function getPromptHistory(db, taskDisplayId) {
|
|
7505
7664
|
const notes = getPmNotes(db, "prompt", { task: taskDisplayId });
|
|
7506
7665
|
if (notes.length === 0) {
|
|
7507
|
-
return
|
|
7666
|
+
return fail2("NOT_FOUND", `No prompts found for task "${taskDisplayId}"`);
|
|
7508
7667
|
}
|
|
7509
7668
|
const prompts = notes.map((note) => {
|
|
7510
7669
|
const meta = JSON.parse(note.metadata);
|
|
7511
7670
|
return promptMetaFromRecord(meta);
|
|
7512
7671
|
});
|
|
7513
7672
|
prompts.sort((a, b) => (a.version ?? 0) - (b.version ?? 0));
|
|
7514
|
-
return
|
|
7673
|
+
return ok2(prompts);
|
|
7515
7674
|
}
|
|
7516
7675
|
function getIndexedAt(db, filePath) {
|
|
7517
7676
|
const file = db.getFile(filePath);
|
|
@@ -7520,7 +7679,7 @@ function getIndexedAt(db, filePath) {
|
|
|
7520
7679
|
function detectStalePrompts(db, prefix) {
|
|
7521
7680
|
const currentPrompts = getPmNotes(db, "prompt", { project: prefix, prompt_status: "current" });
|
|
7522
7681
|
if (currentPrompts.length === 0) {
|
|
7523
|
-
return
|
|
7682
|
+
return ok2([]);
|
|
7524
7683
|
}
|
|
7525
7684
|
const decisionNotes = getPmNotes(db, "decision", { project: prefix });
|
|
7526
7685
|
const stale = [];
|
|
@@ -7533,13 +7692,13 @@ function detectStalePrompts(db, prefix) {
|
|
|
7533
7692
|
const impacts = decMeta.impacts;
|
|
7534
7693
|
if (!impacts || !impacts.includes(taskDisplayId)) continue;
|
|
7535
7694
|
const decIndexedAt = getIndexedAt(db, decNote.filePath);
|
|
7536
|
-
if (decIndexedAt
|
|
7695
|
+
if (decIndexedAt >= promptIndexedAt) {
|
|
7537
7696
|
stale.push(promptMetaFromRecord(promptMeta));
|
|
7538
7697
|
break;
|
|
7539
7698
|
}
|
|
7540
7699
|
}
|
|
7541
7700
|
}
|
|
7542
|
-
return
|
|
7701
|
+
return ok2(stale);
|
|
7543
7702
|
}
|
|
7544
7703
|
|
|
7545
7704
|
// src/modules/pm/engine/consistency.ts
|
|
@@ -7618,7 +7777,10 @@ function findBlockedWithoutCause(db, prefix) {
|
|
|
7618
7777
|
});
|
|
7619
7778
|
continue;
|
|
7620
7779
|
}
|
|
7621
|
-
const allDone = deps.every((d) =>
|
|
7780
|
+
const allDone = deps.every((d) => {
|
|
7781
|
+
const s = statusMap.get(d);
|
|
7782
|
+
return s === "done" || s === "pruned";
|
|
7783
|
+
});
|
|
7622
7784
|
if (allDone) {
|
|
7623
7785
|
results.push({
|
|
7624
7786
|
id: meta.display_id,
|
|
@@ -7643,17 +7805,18 @@ function findCancelledDependencies(db, prefix) {
|
|
|
7643
7805
|
for (const task of tasks) {
|
|
7644
7806
|
const meta = JSON.parse(task.metadata);
|
|
7645
7807
|
const status = meta.status;
|
|
7646
|
-
if (status === "done" || status === "cancelled") continue;
|
|
7808
|
+
if (status === "done" || status === "cancelled" || status === "pruned") continue;
|
|
7647
7809
|
const deps = getDependencyDisplayIds(db, task.id);
|
|
7648
7810
|
if (deps.length === 0) continue;
|
|
7649
7811
|
for (const dep of deps) {
|
|
7650
|
-
|
|
7812
|
+
const depStatus = statusMap.get(dep);
|
|
7813
|
+
if (depStatus === "cancelled" || depStatus === "pruned") {
|
|
7651
7814
|
results.push({
|
|
7652
7815
|
task: meta.display_id,
|
|
7653
7816
|
taskTitle: task.title ?? meta.display_id,
|
|
7654
7817
|
dependsOn: dep,
|
|
7655
|
-
dependsOnStatus:
|
|
7656
|
-
reason: `Depends on
|
|
7818
|
+
dependsOnStatus: depStatus,
|
|
7819
|
+
reason: `Depends on ${depStatus} task ${dep}`
|
|
7657
7820
|
});
|
|
7658
7821
|
}
|
|
7659
7822
|
}
|
|
@@ -7925,13 +8088,13 @@ function resolveWorkstreamByName(db, prefix, input) {
|
|
|
7925
8088
|
if (idResult.ok) return idResult;
|
|
7926
8089
|
const wsResult = listWorkstreams(db, prefix);
|
|
7927
8090
|
if (!wsResult.ok)
|
|
7928
|
-
return
|
|
8091
|
+
return fail2("NOT_FOUND", `Could not list workstreams: ${wsResult.error.message}`);
|
|
7929
8092
|
const lower = input.toLowerCase();
|
|
7930
8093
|
for (const ws of wsResult.data) {
|
|
7931
8094
|
const name = ws.title?.replace(/^Workstream\s+/i, "") ?? "";
|
|
7932
|
-
if (name.toLowerCase().includes(lower)) return
|
|
8095
|
+
if (name.toLowerCase().includes(lower)) return ok2(ws.number);
|
|
7933
8096
|
}
|
|
7934
|
-
return
|
|
8097
|
+
return fail2(
|
|
7935
8098
|
"INVALID_INPUT",
|
|
7936
8099
|
`No workstream matching "${input}". Use a number, display ID, or name. Run "brain pm workstream list" to see options.`
|
|
7937
8100
|
);
|
|
@@ -7972,7 +8135,11 @@ function createOrchestrationCommands() {
|
|
|
7972
8135
|
if (!singlePrefix) {
|
|
7973
8136
|
process.stderr.write(
|
|
7974
8137
|
formatError(
|
|
7975
|
-
{
|
|
8138
|
+
{
|
|
8139
|
+
error: true,
|
|
8140
|
+
code: "INVALID_INPUT",
|
|
8141
|
+
message: "--workstream requires --project when multiple projects exist"
|
|
8142
|
+
},
|
|
7976
8143
|
!!opts.json
|
|
7977
8144
|
) + "\n"
|
|
7978
8145
|
);
|
|
@@ -9195,7 +9362,7 @@ function buildCaptureMarkdown(captureId, input) {
|
|
|
9195
9362
|
}
|
|
9196
9363
|
async function createCapture(db, config, embedder, input) {
|
|
9197
9364
|
if (!input.content || input.content.trim().length === 0) {
|
|
9198
|
-
return
|
|
9365
|
+
return fail2("INVALID_INPUT", "Capture content cannot be empty");
|
|
9199
9366
|
}
|
|
9200
9367
|
const captureId = `capture-${randomUUID6().slice(0, 12)}`;
|
|
9201
9368
|
const filePath = captureFilePath(config, captureId);
|
|
@@ -9215,7 +9382,7 @@ async function createCapture(db, config, embedder, input) {
|
|
|
9215
9382
|
processed: false,
|
|
9216
9383
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9217
9384
|
};
|
|
9218
|
-
return
|
|
9385
|
+
return ok2(record);
|
|
9219
9386
|
}
|
|
9220
9387
|
function listCaptures(db, filters) {
|
|
9221
9388
|
const filterObj = {};
|
|
@@ -9236,19 +9403,19 @@ function listCaptures(db, filters) {
|
|
|
9236
9403
|
createdAt: note.createdAt ?? ""
|
|
9237
9404
|
};
|
|
9238
9405
|
});
|
|
9239
|
-
return
|
|
9406
|
+
return ok2(captures);
|
|
9240
9407
|
}
|
|
9241
9408
|
async function processCapture(db, config, embedder, captureNoteId, asTask) {
|
|
9242
9409
|
const note = db.getNoteById(captureNoteId);
|
|
9243
9410
|
if (!note) {
|
|
9244
|
-
return
|
|
9411
|
+
return fail2("NOT_FOUND", `Capture "${captureNoteId}" not found`);
|
|
9245
9412
|
}
|
|
9246
9413
|
if (note.type !== "capture" || note.module !== "pm") {
|
|
9247
|
-
return
|
|
9414
|
+
return fail2("INVALID_INPUT", `Note "${captureNoteId}" is not a PM capture`);
|
|
9248
9415
|
}
|
|
9249
9416
|
const meta = JSON.parse(note.metadata);
|
|
9250
9417
|
if (meta.processed === true || meta.processed === "true") {
|
|
9251
|
-
return
|
|
9418
|
+
return fail2("INVALID_INPUT", `Capture "${captureNoteId}" has already been processed`);
|
|
9252
9419
|
}
|
|
9253
9420
|
const taskResult = await createTask(db, config, embedder, asTask);
|
|
9254
9421
|
if (!taskResult.ok) {
|
|
@@ -9257,7 +9424,7 @@ async function processCapture(db, config, embedder, captureNoteId, asTask) {
|
|
|
9257
9424
|
const updatedMeta = { ...meta, processed: true };
|
|
9258
9425
|
const updatedNote = { ...note, metadata: JSON.stringify(updatedMeta) };
|
|
9259
9426
|
db.upsertNote(updatedNote);
|
|
9260
|
-
return
|
|
9427
|
+
return ok2(taskResult.data);
|
|
9261
9428
|
}
|
|
9262
9429
|
|
|
9263
9430
|
// src/modules/pm/commands/capture.ts
|
|
@@ -9590,32 +9757,32 @@ import { readFileSync as readFileSync17 } from "fs";
|
|
|
9590
9757
|
import { Command as Command36 } from "@commander-js/extra-typings";
|
|
9591
9758
|
function validateImportDef(data) {
|
|
9592
9759
|
if (!data || typeof data !== "object") {
|
|
9593
|
-
return
|
|
9760
|
+
return fail2("INVALID_INPUT", "Import data must be a JSON object");
|
|
9594
9761
|
}
|
|
9595
9762
|
const obj = data;
|
|
9596
9763
|
if (typeof obj.name !== "string" || obj.name.trim().length === 0) {
|
|
9597
|
-
return
|
|
9764
|
+
return fail2("INVALID_INPUT", 'Import data must have a non-empty "name" field');
|
|
9598
9765
|
}
|
|
9599
9766
|
if (typeof obj.prefix !== "string" || obj.prefix.trim().length === 0) {
|
|
9600
|
-
return
|
|
9767
|
+
return fail2("INVALID_INPUT", 'Import data must have a non-empty "prefix" field');
|
|
9601
9768
|
}
|
|
9602
9769
|
if (obj.workstreams !== void 0 && !Array.isArray(obj.workstreams)) {
|
|
9603
|
-
return
|
|
9770
|
+
return fail2("INVALID_INPUT", '"workstreams" must be an array');
|
|
9604
9771
|
}
|
|
9605
9772
|
const workstreams = obj.workstreams ?? [];
|
|
9606
9773
|
for (let i = 0; i < workstreams.length; i++) {
|
|
9607
9774
|
const ws = workstreams[i];
|
|
9608
9775
|
if (typeof ws.name !== "string" || ws.name.trim().length === 0) {
|
|
9609
|
-
return
|
|
9776
|
+
return fail2("INVALID_INPUT", `Workstream at index ${i} must have a non-empty "name" field`);
|
|
9610
9777
|
}
|
|
9611
9778
|
if (ws.tasks !== void 0 && !Array.isArray(ws.tasks)) {
|
|
9612
|
-
return
|
|
9779
|
+
return fail2("INVALID_INPUT", `Workstream "${ws.name}" tasks must be an array`);
|
|
9613
9780
|
}
|
|
9614
9781
|
const tasks = ws.tasks ?? [];
|
|
9615
9782
|
for (let j = 0; j < tasks.length; j++) {
|
|
9616
9783
|
const t = tasks[j];
|
|
9617
9784
|
if (typeof t.name !== "string" || t.name.trim().length === 0) {
|
|
9618
|
-
return
|
|
9785
|
+
return fail2(
|
|
9619
9786
|
"INVALID_INPUT",
|
|
9620
9787
|
`Task at index ${j} in workstream "${ws.name}" must have a non-empty "name"`
|
|
9621
9788
|
);
|
|
@@ -9737,7 +9904,7 @@ function createImportCommand() {
|
|
|
9737
9904
|
rawData = JSON.parse(content);
|
|
9738
9905
|
} catch (err) {
|
|
9739
9906
|
const message = err instanceof Error ? err.message : String(err);
|
|
9740
|
-
const error =
|
|
9907
|
+
const error = fail2(
|
|
9741
9908
|
"INVALID_INPUT",
|
|
9742
9909
|
`Failed to read/parse "${opts.fromJson}": ${message}`
|
|
9743
9910
|
);
|
|
@@ -10195,10 +10362,8 @@ function createOrchestrateCommands() {
|
|
|
10195
10362
|
`Tasks: ${done.length} done, ${inProgress.length} in-progress, ${pending.length} pending, ${blocked.length} blocked (${allTasks.length} total)
|
|
10196
10363
|
`
|
|
10197
10364
|
);
|
|
10198
|
-
process.stdout.write(
|
|
10199
|
-
|
|
10200
|
-
`
|
|
10201
|
-
);
|
|
10365
|
+
process.stdout.write(`Worktrees: ${worktreeInfo.used}/${worktreeInfo.max} in use
|
|
10366
|
+
`);
|
|
10202
10367
|
if (worktreeInfo.allocations.length > 0) {
|
|
10203
10368
|
for (const a of worktreeInfo.allocations) {
|
|
10204
10369
|
process.stdout.write(` ${a.taskId}: ${a.path}
|
|
@@ -10212,18 +10377,8 @@ function createOrchestrateCommands() {
|
|
|
10212
10377
|
return cmd;
|
|
10213
10378
|
}
|
|
10214
10379
|
|
|
10215
|
-
// src/modules/pm/commands/install-hooks.ts
|
|
10216
|
-
import { Command as Command38 } from "@commander-js/extra-typings";
|
|
10217
|
-
var DEPRECATION_MESSAGE = 'DEPRECATED: "brain pm install-hooks" has been replaced by "ao hook install".\nInstall ao-cli and run: ao hook install\nTo remove existing hooks: ao hook remove\n';
|
|
10218
|
-
function createInstallHooksCommand() {
|
|
10219
|
-
return new Command38("install-hooks").description('[DEPRECATED] Use "ao hook install" instead').option("--remove", "Remove installed hooks and skill").option("--dry-run", "Preview changes without writing files").action(() => {
|
|
10220
|
-
process.stderr.write(DEPRECATION_MESSAGE);
|
|
10221
|
-
process.exitCode = 1;
|
|
10222
|
-
});
|
|
10223
|
-
}
|
|
10224
|
-
|
|
10225
10380
|
// src/modules/pm/commands/setup.ts
|
|
10226
|
-
import { Command as
|
|
10381
|
+
import { Command as Command38 } from "@commander-js/extra-typings";
|
|
10227
10382
|
async function validateDatabase() {
|
|
10228
10383
|
try {
|
|
10229
10384
|
await withBrain(() => {
|
|
@@ -10320,7 +10475,7 @@ function formatText(dbCheck, demo) {
|
|
|
10320
10475
|
return lines2.join("\n");
|
|
10321
10476
|
}
|
|
10322
10477
|
function createSetupCommand() {
|
|
10323
|
-
return new
|
|
10478
|
+
return new Command38("setup").description('[DEPRECATED] Use "ao hook install" for hooks. Use --demo for demo project only.').option("--demo", "Create a demo project").option("--json", "Output JSON status").option("--dry-run", "Show what would be done").action(async (opts) => {
|
|
10324
10479
|
if (opts.dryRun) {
|
|
10325
10480
|
const items = [];
|
|
10326
10481
|
if (opts.demo) items.push("Demo project with 2 workstreams and 4 tasks");
|
|
@@ -10343,7 +10498,9 @@ function createSetupCommand() {
|
|
|
10343
10498
|
process.stdout.write(
|
|
10344
10499
|
JSON.stringify(
|
|
10345
10500
|
{
|
|
10346
|
-
checks: [
|
|
10501
|
+
checks: [
|
|
10502
|
+
{ label: "Database accessible", passed: dbCheck.passed, error: dbCheck.error }
|
|
10503
|
+
],
|
|
10347
10504
|
demo: { success: demo.success, error: demo.error },
|
|
10348
10505
|
success: !hasFailure
|
|
10349
10506
|
},
|
|
@@ -10358,7 +10515,7 @@ function createSetupCommand() {
|
|
|
10358
10515
|
}
|
|
10359
10516
|
|
|
10360
10517
|
// src/modules/pm/commands/check.ts
|
|
10361
|
-
import { Command as
|
|
10518
|
+
import { Command as Command39 } from "@commander-js/extra-typings";
|
|
10362
10519
|
function resolvePrefix(explicit, active) {
|
|
10363
10520
|
if (explicit) return explicit.toUpperCase();
|
|
10364
10521
|
return active ?? void 0;
|
|
@@ -10444,7 +10601,7 @@ function formatCheckReport(report) {
|
|
|
10444
10601
|
}
|
|
10445
10602
|
}
|
|
10446
10603
|
function createCheckCommand() {
|
|
10447
|
-
return new
|
|
10604
|
+
return new Command39("check").description("Run consistency checks on a PM project").option("--project <prefix>", "Project prefix (uses active project if omitted)").option("--deep", "Include semantic analysis and source document clustering").option("--deps", "Check dependency graph for cycles").option("--json", "Output JSON").action(async (opts) => {
|
|
10448
10605
|
await withBrain(async (svc) => {
|
|
10449
10606
|
const prefix = resolvePrefix(opts.project, getActiveProject(svc.db));
|
|
10450
10607
|
if (!prefix) {
|
|
@@ -10487,7 +10644,7 @@ function createCheckCommand() {
|
|
|
10487
10644
|
}
|
|
10488
10645
|
|
|
10489
10646
|
// src/modules/pm/commands/onboard.ts
|
|
10490
|
-
import { Command as
|
|
10647
|
+
import { Command as Command40 } from "@commander-js/extra-typings";
|
|
10491
10648
|
import { createHash as createHash13 } from "crypto";
|
|
10492
10649
|
import { readFileSync as readFileSync19, writeFileSync as writeFileSync16, mkdirSync as mkdirSync16, existsSync as existsSync21, unlinkSync as unlinkSync6 } from "fs";
|
|
10493
10650
|
import { join as join20, dirname as dirname11, basename as basename6 } from "path";
|
|
@@ -10831,7 +10988,7 @@ async function runOnboard(db, config, embedder, opts) {
|
|
|
10831
10988
|
const now = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
10832
10989
|
const existing = getPmNotes(db, "project", { prefix: opts.prefix });
|
|
10833
10990
|
if (existing.length > 0 && !opts.reset) {
|
|
10834
|
-
return
|
|
10991
|
+
return fail2(
|
|
10835
10992
|
"PROJECT_EXISTS",
|
|
10836
10993
|
`Project "${opts.prefix}" already exists. Use --reset to re-onboard.`
|
|
10837
10994
|
);
|
|
@@ -10841,7 +10998,7 @@ async function runOnboard(db, config, embedder, opts) {
|
|
|
10841
10998
|
const projMeta = JSON.parse(projNote.metadata ?? "{}");
|
|
10842
10999
|
const projTitle = (projMeta.title ?? "").replace(/^Project\s+/i, "");
|
|
10843
11000
|
if (projTitle.toLowerCase() === opts.projectName.toLowerCase() && projMeta.prefix !== opts.prefix) {
|
|
10844
|
-
return
|
|
11001
|
+
return fail2(
|
|
10845
11002
|
"PROJECT_EXISTS",
|
|
10846
11003
|
`A project named "${opts.projectName}" already exists as ${projMeta.prefix}. Use --prefix ${projMeta.prefix} --reset to re-onboard, or brain pm use ${projMeta.prefix}.`
|
|
10847
11004
|
);
|
|
@@ -11038,7 +11195,7 @@ async function runOnboard(db, config, embedder, opts) {
|
|
|
11038
11195
|
writeFileSync16(manifestPath, manifestMd, "utf-8");
|
|
11039
11196
|
const manifestHash = createHash13("sha256").update(manifestMd).digest("hex");
|
|
11040
11197
|
await indexSingleFile(db, embedder, manifestPath, manifestMd, manifestHash, Date.now());
|
|
11041
|
-
return
|
|
11198
|
+
return ok2(manifest);
|
|
11042
11199
|
}
|
|
11043
11200
|
function syncProjectHierarchyEdges(db, prefix) {
|
|
11044
11201
|
const projectNotes = getPmNotes(db, "project", { prefix });
|
|
@@ -11110,7 +11267,7 @@ function buildManifestNote(manifest, slug, projectName) {
|
|
|
11110
11267
|
return lines2.join("\n");
|
|
11111
11268
|
}
|
|
11112
11269
|
function createOnboardCommand() {
|
|
11113
|
-
const cmd = new
|
|
11270
|
+
const cmd = new Command40("onboard");
|
|
11114
11271
|
cmd.description("Set up a PM project from a codebase").argument("<project-name>", "Project name").option(
|
|
11115
11272
|
"--prefix <prefix>",
|
|
11116
11273
|
"Project prefix (2-5 uppercase chars, derived from name if omitted)"
|
|
@@ -11158,7 +11315,7 @@ function createOnboardCommand() {
|
|
|
11158
11315
|
}
|
|
11159
11316
|
|
|
11160
11317
|
// src/modules/pm/commands/relate.ts
|
|
11161
|
-
import { Command as
|
|
11318
|
+
import { Command as Command41 } from "@commander-js/extra-typings";
|
|
11162
11319
|
async function relateAction(db, source, target, opts) {
|
|
11163
11320
|
const relType = opts.type ?? "related";
|
|
11164
11321
|
const sourceNote = db.getNoteById(source);
|
|
@@ -11188,7 +11345,7 @@ async function relateAction(db, source, target, opts) {
|
|
|
11188
11345
|
return { ok: true, message: `Created ${relType} relation: ${source} -> ${target}` };
|
|
11189
11346
|
}
|
|
11190
11347
|
function createRelateCommand() {
|
|
11191
|
-
return new
|
|
11348
|
+
return new Command41("relate").description("Create or remove a relation between two notes").argument("<source>", "Source note ID or display ID").argument("<target>", "Target note ID or display ID").option("--type <type>", "Relation type (related, depends_on, derived-from, parent)", "related").option("--remove", "Remove the relation instead of creating it").option("--json", "Output JSON").action(async (source, target, opts) => {
|
|
11192
11349
|
await withDb(async ({ db }) => {
|
|
11193
11350
|
const result = await relateAction(db, source, target, opts);
|
|
11194
11351
|
if (opts.json) {
|
|
@@ -11204,7 +11361,7 @@ function createRelateCommand() {
|
|
|
11204
11361
|
}
|
|
11205
11362
|
|
|
11206
11363
|
// src/modules/pm/commands/review.ts
|
|
11207
|
-
import { Command as
|
|
11364
|
+
import { Command as Command42 } from "@commander-js/extra-typings";
|
|
11208
11365
|
|
|
11209
11366
|
// src/modules/pm/data/review-ops.ts
|
|
11210
11367
|
function extractPrNumber(url) {
|
|
@@ -11222,11 +11379,28 @@ function findReviewWorkstream(db, project) {
|
|
|
11222
11379
|
}
|
|
11223
11380
|
return { found: false };
|
|
11224
11381
|
}
|
|
11382
|
+
var AUTO_COMPLETE_TRANSITIONS = {
|
|
11383
|
+
pending: ["claimed", "in-progress", "done"],
|
|
11384
|
+
claimed: ["in-progress", "done"],
|
|
11385
|
+
"in-progress": ["done"]
|
|
11386
|
+
};
|
|
11387
|
+
async function autoCompleteSourceTask(db, config, embedder, displayId, currentStatus) {
|
|
11388
|
+
const steps = AUTO_COMPLETE_TRANSITIONS[currentStatus];
|
|
11389
|
+
if (!steps) return false;
|
|
11390
|
+
for (const targetStatus of steps) {
|
|
11391
|
+
const result = await updateTaskStatus(db, config, embedder, displayId, targetStatus);
|
|
11392
|
+
if (!result.ok) return false;
|
|
11393
|
+
}
|
|
11394
|
+
return true;
|
|
11395
|
+
}
|
|
11225
11396
|
async function createReviewTask(db, config, embedder, input) {
|
|
11397
|
+
if (input.risk !== void 0 && (!Number.isInteger(input.risk) || input.risk < 1 || input.risk > 5)) {
|
|
11398
|
+
return fail2("INVALID_INPUT", `Risk must be an integer between 1 and 5, got ${input.risk}`);
|
|
11399
|
+
}
|
|
11226
11400
|
const sourceDisplayId = input.sourceTaskId.toUpperCase();
|
|
11227
11401
|
const sourceNotes = getPmNotes(db, "task", { display_id: sourceDisplayId });
|
|
11228
11402
|
if (sourceNotes.length === 0) {
|
|
11229
|
-
return
|
|
11403
|
+
return fail2("NOT_FOUND", `Source task "${sourceDisplayId}" not found`);
|
|
11230
11404
|
}
|
|
11231
11405
|
const sourceMeta = JSON.parse(sourceNotes[0].metadata);
|
|
11232
11406
|
const project = sourceMeta.project;
|
|
@@ -11244,7 +11418,7 @@ async function createReviewTask(db, config, embedder, input) {
|
|
|
11244
11418
|
description: "Pull request review tasks requiring human approval"
|
|
11245
11419
|
});
|
|
11246
11420
|
if (!wsResult.ok) {
|
|
11247
|
-
return
|
|
11421
|
+
return fail2(wsResult.error.code, wsResult.error.message, wsResult.error.details);
|
|
11248
11422
|
}
|
|
11249
11423
|
reviewWsNumber = wsResult.data.number;
|
|
11250
11424
|
}
|
|
@@ -11259,6 +11433,9 @@ async function createReviewTask(db, config, embedder, input) {
|
|
|
11259
11433
|
if (input.agentId) {
|
|
11260
11434
|
descriptionLines.push(`- **Agent:** ${input.agentId}`);
|
|
11261
11435
|
}
|
|
11436
|
+
if (input.risk !== void 0) {
|
|
11437
|
+
descriptionLines.push(`- **Risk:** ${input.risk}/5`);
|
|
11438
|
+
}
|
|
11262
11439
|
descriptionLines.push("", "Review and merge the pull request to unblock downstream work.");
|
|
11263
11440
|
const taskResult = await createTask(db, config, embedder, {
|
|
11264
11441
|
project,
|
|
@@ -11271,7 +11448,7 @@ async function createReviewTask(db, config, embedder, input) {
|
|
|
11271
11448
|
description: descriptionLines.join("\n")
|
|
11272
11449
|
});
|
|
11273
11450
|
if (!taskResult.ok) {
|
|
11274
|
-
return
|
|
11451
|
+
return fail2(taskResult.error.code, taskResult.error.message, taskResult.error.details);
|
|
11275
11452
|
}
|
|
11276
11453
|
const reviewDisplayId = taskResult.data.display_id;
|
|
11277
11454
|
const rewiredDeps = [];
|
|
@@ -11312,32 +11489,65 @@ async function createReviewTask(db, config, embedder, input) {
|
|
|
11312
11489
|
project
|
|
11313
11490
|
});
|
|
11314
11491
|
const captureNoteId = captureResult.ok ? captureResult.data.noteId : "";
|
|
11315
|
-
|
|
11492
|
+
let sourceAutoCompleted = false;
|
|
11493
|
+
if (input.autoComplete !== false) {
|
|
11494
|
+
const sourceStatus = sourceMeta.status ?? "pending";
|
|
11495
|
+
if (sourceStatus !== "done") {
|
|
11496
|
+
try {
|
|
11497
|
+
sourceAutoCompleted = await autoCompleteSourceTask(
|
|
11498
|
+
db,
|
|
11499
|
+
config,
|
|
11500
|
+
embedder,
|
|
11501
|
+
sourceDisplayId,
|
|
11502
|
+
sourceStatus
|
|
11503
|
+
);
|
|
11504
|
+
} catch {
|
|
11505
|
+
}
|
|
11506
|
+
}
|
|
11507
|
+
}
|
|
11508
|
+
let riskAdvisory;
|
|
11509
|
+
if (input.risk !== void 0) {
|
|
11510
|
+
riskAdvisory = input.risk >= 4 ? `Advisory: risk ${input.risk} \u2014 human review recommended` : `Advisory: risk ${input.risk} \u2014 agent review may be sufficient`;
|
|
11511
|
+
}
|
|
11512
|
+
return ok2({
|
|
11316
11513
|
reviewTaskId: reviewDisplayId,
|
|
11317
11514
|
reviewTaskMeta: taskResult.data,
|
|
11318
11515
|
rewiredDeps,
|
|
11319
|
-
captureNoteId
|
|
11516
|
+
captureNoteId,
|
|
11517
|
+
riskAdvisory,
|
|
11518
|
+
sourceAutoCompleted
|
|
11320
11519
|
});
|
|
11321
11520
|
}
|
|
11322
11521
|
|
|
11323
11522
|
// src/modules/pm/commands/review.ts
|
|
11324
11523
|
function createReviewCommands() {
|
|
11325
|
-
const cmd = new
|
|
11326
|
-
cmd.command("create").description("Create a review task for a PR").argument("<task-id>", "Source task display ID (e.g. SDK-02.01)").requiredOption("--pr <url>", "Pull request URL").requiredOption("--branch <branch>", "Branch name").option("--agent <id>", "Agent ID that created the PR").option("--no-rewire", "Skip dependency rewiring").option("--json", "Output JSON").action(async (taskId, opts) => {
|
|
11524
|
+
const cmd = new Command42("review").description("PR review lifecycle commands");
|
|
11525
|
+
cmd.command("create").description("Create a review task for a PR").argument("<task-id>", "Source task display ID (e.g. SDK-02.01)").requiredOption("--pr <url>", "Pull request URL").requiredOption("--branch <branch>", "Branch name").option("--agent <id>", "Agent ID that created the PR").option("--no-rewire", "Skip dependency rewiring").option("--no-auto-complete", "Skip auto-completing the source task").option("--risk <number>", "Risk score (1-5) for review routing advisory").option("--json", "Output JSON").action(async (taskId, opts) => {
|
|
11327
11526
|
await withBrain(async (svc) => {
|
|
11527
|
+
let risk;
|
|
11528
|
+
if (opts.risk !== void 0) {
|
|
11529
|
+
risk = Number(opts.risk);
|
|
11530
|
+
if (!Number.isInteger(risk) || risk < 1 || risk > 5) {
|
|
11531
|
+
process.stderr.write("Error: --risk must be an integer between 1 and 5\n");
|
|
11532
|
+
process.exitCode = 1;
|
|
11533
|
+
return;
|
|
11534
|
+
}
|
|
11535
|
+
}
|
|
11328
11536
|
const result = await createReviewTask(svc.db, svc.config, svc.embedder, {
|
|
11329
11537
|
sourceTaskId: taskId,
|
|
11330
11538
|
prUrl: opts.pr,
|
|
11331
11539
|
branch: opts.branch,
|
|
11332
11540
|
agentId: opts.agent,
|
|
11333
|
-
rewireDeps: opts.rewire
|
|
11541
|
+
rewireDeps: opts.rewire,
|
|
11542
|
+
autoComplete: opts.autoComplete,
|
|
11543
|
+
risk
|
|
11334
11544
|
});
|
|
11335
11545
|
if (!result.ok) {
|
|
11336
11546
|
process.stderr.write(formatError(result.error, !!opts.json) + "\n");
|
|
11337
11547
|
process.exitCode = 1;
|
|
11338
11548
|
return;
|
|
11339
11549
|
}
|
|
11340
|
-
const { reviewTaskId, reviewTaskMeta, rewiredDeps } = result.data;
|
|
11550
|
+
const { reviewTaskId, reviewTaskMeta, rewiredDeps, sourceAutoCompleted } = result.data;
|
|
11341
11551
|
if (opts.json) {
|
|
11342
11552
|
process.stdout.write(
|
|
11343
11553
|
JSON.stringify(
|
|
@@ -11345,7 +11555,9 @@ function createReviewCommands() {
|
|
|
11345
11555
|
reviewTaskId,
|
|
11346
11556
|
reviewTask: reviewTaskMeta,
|
|
11347
11557
|
rewiredDeps,
|
|
11348
|
-
captureNoteId: result.data.captureNoteId
|
|
11558
|
+
captureNoteId: result.data.captureNoteId,
|
|
11559
|
+
riskAdvisory: result.data.riskAdvisory,
|
|
11560
|
+
sourceAutoCompleted
|
|
11349
11561
|
},
|
|
11350
11562
|
null,
|
|
11351
11563
|
2
|
|
@@ -11358,8 +11570,20 @@ function createReviewCommands() {
|
|
|
11358
11570
|
` ${reviewTaskMeta.title} [${reviewTaskMeta.priority}] (${reviewTaskMeta.mode})
|
|
11359
11571
|
`
|
|
11360
11572
|
);
|
|
11573
|
+
if (sourceAutoCompleted) {
|
|
11574
|
+
process.stdout.write(` Auto-completed source task ${taskId.toUpperCase()}
|
|
11575
|
+
`);
|
|
11576
|
+
} else if (!opts.autoComplete) {
|
|
11577
|
+
} else {
|
|
11578
|
+
process.stdout.write(` Source task ${taskId.toUpperCase()} already done
|
|
11579
|
+
`);
|
|
11580
|
+
}
|
|
11361
11581
|
if (rewiredDeps.length > 0) {
|
|
11362
11582
|
process.stdout.write(` Rewired deps: ${rewiredDeps.join(", ")}
|
|
11583
|
+
`);
|
|
11584
|
+
}
|
|
11585
|
+
if (result.data.riskAdvisory) {
|
|
11586
|
+
process.stdout.write(` ${result.data.riskAdvisory}
|
|
11363
11587
|
`);
|
|
11364
11588
|
}
|
|
11365
11589
|
}
|
|
@@ -11443,7 +11667,6 @@ var PmContentHandler = class {
|
|
|
11443
11667
|
setConfig(config) {
|
|
11444
11668
|
this.config = config;
|
|
11445
11669
|
}
|
|
11446
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
11447
11670
|
canHandle(noteType, _content) {
|
|
11448
11671
|
return noteType === "task";
|
|
11449
11672
|
}
|
|
@@ -11452,11 +11675,7 @@ var PmContentHandler = class {
|
|
|
11452
11675
|
if (!this.config) {
|
|
11453
11676
|
throw new Error("PmContentHandler requires config \u2014 call setConfig() before materialize()");
|
|
11454
11677
|
}
|
|
11455
|
-
const { prefix, workstream } = await ensureProjectAndWorkstream(
|
|
11456
|
-
db,
|
|
11457
|
-
this.config,
|
|
11458
|
-
embedder
|
|
11459
|
-
);
|
|
11678
|
+
const { prefix, workstream } = await ensureProjectAndWorkstream(db, this.config, embedder);
|
|
11460
11679
|
const noteIds = [];
|
|
11461
11680
|
for (const item of items) {
|
|
11462
11681
|
const name = item.fields.name || item.title || "Imported task";
|
|
@@ -11564,7 +11783,7 @@ var pmModule = {
|
|
|
11564
11783
|
number: { type: "number", description: "Task number within workstream" },
|
|
11565
11784
|
status: {
|
|
11566
11785
|
type: "string",
|
|
11567
|
-
enum: ["pending", "claimed", "in-progress", "done", "blocked", "cancelled"],
|
|
11786
|
+
enum: ["pending", "claimed", "in-progress", "done", "blocked", "cancelled", "pruned"],
|
|
11568
11787
|
description: "Task status"
|
|
11569
11788
|
},
|
|
11570
11789
|
mode: {
|
|
@@ -11759,7 +11978,6 @@ var pmModule = {
|
|
|
11759
11978
|
pmCmd.addCommand(createAuditCommands());
|
|
11760
11979
|
pmCmd.addCommand(createImportCommand());
|
|
11761
11980
|
pmCmd.addCommand(createOrchestrateCommands());
|
|
11762
|
-
pmCmd.addCommand(createInstallHooksCommand());
|
|
11763
11981
|
pmCmd.addCommand(createSetupCommand());
|
|
11764
11982
|
pmCmd.addCommand(createCheckCommand());
|
|
11765
11983
|
pmCmd.addCommand(createOnboardCommand());
|
|
@@ -11769,10 +11987,10 @@ var pmModule = {
|
|
|
11769
11987
|
pmCmd.on("command:*", async (operands) => {
|
|
11770
11988
|
const unknown = operands[0];
|
|
11771
11989
|
if (!unknown) return;
|
|
11772
|
-
const { resolveUnknownCommand } = await import("./command-resolution-
|
|
11990
|
+
const { resolveUnknownCommand } = await import("./command-resolution-EJ6LTC2Z.js");
|
|
11773
11991
|
let resolution;
|
|
11774
11992
|
try {
|
|
11775
|
-
const { withBrain: withBrain2 } = await import("./brain-service-
|
|
11993
|
+
const { withBrain: withBrain2 } = await import("./brain-service-TYFNTBT6.js");
|
|
11776
11994
|
resolution = await withBrain2(async (svc) => {
|
|
11777
11995
|
return resolveUnknownCommand(unknown, svc.db, svc.embedder);
|
|
11778
11996
|
});
|
|
@@ -11783,7 +12001,7 @@ var pmModule = {
|
|
|
11783
12001
|
`);
|
|
11784
12002
|
process.exitCode = 1;
|
|
11785
12003
|
});
|
|
11786
|
-
const showCmd = new
|
|
12004
|
+
const showCmd = new Command43("show").description("Show details for any PM entity (task, workstream, or project)").argument("<id>", "Display ID (e.g., VOLT, VOLT-01, VOLT-01.03)").option("--json", "Output JSON").option("--format <format>", "Output format").action(async (id, opts) => {
|
|
11787
12005
|
if (opts.format === "json") opts.json = true;
|
|
11788
12006
|
const entityType = detectEntityType2(id);
|
|
11789
12007
|
const args = ["node", "brain-pm", entityType, "show", id];
|
|
@@ -11791,7 +12009,7 @@ var pmModule = {
|
|
|
11791
12009
|
await pmCmd.parseAsync(args, { from: "node" });
|
|
11792
12010
|
});
|
|
11793
12011
|
pmCmd.addCommand(showCmd);
|
|
11794
|
-
const claimCmd = new
|
|
12012
|
+
const claimCmd = new Command43("claim").description("Claim a task").argument("<id>", "Task display ID (e.g., VOLT-01.03)").option("--start", "Start working immediately after claiming").option("--json", "Output JSON").option("--format <format>", "Output format").action(async (id, opts) => {
|
|
11795
12013
|
if (opts.format === "json") opts.json = true;
|
|
11796
12014
|
const args = ["node", "brain-pm", "task", "claim", id];
|
|
11797
12015
|
if (opts.start) args.push("--start");
|
|
@@ -11813,7 +12031,7 @@ var pmModule = {
|
|
|
11813
12031
|
"release",
|
|
11814
12032
|
"complete"
|
|
11815
12033
|
]);
|
|
11816
|
-
const tasksAlias = new
|
|
12034
|
+
const tasksAlias = new Command43("tasks").description('Task management (alias for "task")').helpOption(false).allowUnknownOption().allowExcessArguments(true).action(async () => {
|
|
11817
12035
|
const idx = process.argv.indexOf("tasks");
|
|
11818
12036
|
const tail = process.argv.slice(idx + 1);
|
|
11819
12037
|
const hasSubcommand = tail.length > 0 && taskSubcommands.has(tail[0]);
|
|
@@ -11835,7 +12053,7 @@ var pmModule = {
|
|
|
11835
12053
|
});
|
|
11836
12054
|
pmCmd.addCommand(tasksAlias);
|
|
11837
12055
|
const wsSubcommands = /* @__PURE__ */ new Set(["list", "show", "add"]);
|
|
11838
|
-
const workstreamsAlias = new
|
|
12056
|
+
const workstreamsAlias = new Command43("workstreams").description('Workstream management (alias for "workstream")').helpOption(false).allowUnknownOption().allowExcessArguments(true).action(async () => {
|
|
11839
12057
|
const idx = process.argv.indexOf("workstreams");
|
|
11840
12058
|
const tail = process.argv.slice(idx + 1);
|
|
11841
12059
|
const hasSubcommand = tail.length > 0 && wsSubcommands.has(tail[0]);
|
|
@@ -11846,7 +12064,7 @@ var pmModule = {
|
|
|
11846
12064
|
});
|
|
11847
12065
|
pmCmd.addCommand(workstreamsAlias);
|
|
11848
12066
|
const projectSubcommands = /* @__PURE__ */ new Set(["list", "show", "add"]);
|
|
11849
|
-
const projectsAlias = new
|
|
12067
|
+
const projectsAlias = new Command43("projects").description('Project management (alias for "project")').helpOption(false).allowUnknownOption().allowExcessArguments(true).action(async () => {
|
|
11850
12068
|
const idx = process.argv.indexOf("projects");
|
|
11851
12069
|
const tail = process.argv.slice(idx + 1);
|
|
11852
12070
|
const hasSubcommand = tail.length > 0 && projectSubcommands.has(tail[0]);
|
|
@@ -11856,7 +12074,7 @@ var pmModule = {
|
|
|
11856
12074
|
});
|
|
11857
12075
|
});
|
|
11858
12076
|
pmCmd.addCommand(projectsAlias);
|
|
11859
|
-
const lsAlias = new
|
|
12077
|
+
const lsAlias = new Command43("ls").description('List projects (alias for "list")').helpOption(false).allowUnknownOption().allowExcessArguments(true).action(async () => {
|
|
11860
12078
|
const idx = process.argv.indexOf("ls");
|
|
11861
12079
|
const tail = process.argv.slice(idx + 1);
|
|
11862
12080
|
await pmCmd.parseAsync(["node", "brain-pm", "list", ...tail], { from: "node" });
|
|
@@ -11866,9 +12084,2040 @@ var pmModule = {
|
|
|
11866
12084
|
}
|
|
11867
12085
|
};
|
|
11868
12086
|
|
|
11869
|
-
// src/commands/
|
|
11870
|
-
import { Command as
|
|
11871
|
-
|
|
12087
|
+
// src/modules/workflow/commands/workflow.ts
|
|
12088
|
+
import { Command as Command44 } from "@commander-js/extra-typings";
|
|
12089
|
+
|
|
12090
|
+
// src/modules/workflow/data/workflow-ops.ts
|
|
12091
|
+
import { readFileSync as readFileSync20, existsSync as existsSync22 } from "fs";
|
|
12092
|
+
|
|
12093
|
+
// src/modules/workflow/engine/dag.ts
|
|
12094
|
+
function ok3(data) {
|
|
12095
|
+
return { ok: true, data };
|
|
12096
|
+
}
|
|
12097
|
+
function fail3(code, message, details) {
|
|
12098
|
+
const err = { error: true, code, message };
|
|
12099
|
+
if (details !== void 0) {
|
|
12100
|
+
err.details = details;
|
|
12101
|
+
}
|
|
12102
|
+
return { ok: false, error: err };
|
|
12103
|
+
}
|
|
12104
|
+
function getPredecessors(stepId, edges) {
|
|
12105
|
+
return edges.filter((e) => e.to === stepId).map((e) => e.from);
|
|
12106
|
+
}
|
|
12107
|
+
function validateDag(definition) {
|
|
12108
|
+
const { steps, edges } = definition;
|
|
12109
|
+
if (steps.length === 0) {
|
|
12110
|
+
return fail3("INVALID_DEFINITION", "Workflow must have at least one step");
|
|
12111
|
+
}
|
|
12112
|
+
const stepIds = new Set(steps.map((s) => s.id));
|
|
12113
|
+
for (const edge of edges) {
|
|
12114
|
+
if (!stepIds.has(edge.from)) {
|
|
12115
|
+
return fail3("INVALID_DEFINITION", `Edge references non-existent source step: ${edge.from}`);
|
|
12116
|
+
}
|
|
12117
|
+
if (!stepIds.has(edge.to)) {
|
|
12118
|
+
return fail3("INVALID_DEFINITION", `Edge references non-existent target step: ${edge.to}`);
|
|
12119
|
+
}
|
|
12120
|
+
}
|
|
12121
|
+
const cycleResult = detectCycle(steps, edges);
|
|
12122
|
+
if (cycleResult) {
|
|
12123
|
+
return fail3("CYCLE_DETECTED", `Cycle detected: ${cycleResult.join(" -> ")}`, {
|
|
12124
|
+
cycle: cycleResult
|
|
12125
|
+
});
|
|
12126
|
+
}
|
|
12127
|
+
const unconditionalEdges = edges.filter((e) => !e.condition);
|
|
12128
|
+
const hasIncoming = new Set(unconditionalEdges.map((e) => e.to));
|
|
12129
|
+
const entryNodes = steps.filter((s) => !hasIncoming.has(s.id));
|
|
12130
|
+
if (entryNodes.length === 0) {
|
|
12131
|
+
return fail3("INVALID_DEFINITION", "DAG must have at least one entry node");
|
|
12132
|
+
}
|
|
12133
|
+
const hasOutgoing = new Set(unconditionalEdges.map((e) => e.from));
|
|
12134
|
+
const terminalNodes = steps.filter((s) => !hasOutgoing.has(s.id));
|
|
12135
|
+
if (terminalNodes.length === 0) {
|
|
12136
|
+
return fail3("INVALID_DEFINITION", "DAG must have at least one terminal node");
|
|
12137
|
+
}
|
|
12138
|
+
return ok3(void 0);
|
|
12139
|
+
}
|
|
12140
|
+
function detectCycle(steps, edges) {
|
|
12141
|
+
const adj = /* @__PURE__ */ new Map();
|
|
12142
|
+
for (const s of steps) {
|
|
12143
|
+
adj.set(s.id, []);
|
|
12144
|
+
}
|
|
12145
|
+
for (const e of edges) {
|
|
12146
|
+
if (!e.condition) {
|
|
12147
|
+
adj.get(e.from)?.push(e.to);
|
|
12148
|
+
}
|
|
12149
|
+
}
|
|
12150
|
+
const visited = /* @__PURE__ */ new Set();
|
|
12151
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
12152
|
+
const path = [];
|
|
12153
|
+
for (const s of steps) {
|
|
12154
|
+
const cycle = dfs(s.id, adj, visited, inStack, path);
|
|
12155
|
+
if (cycle) return cycle;
|
|
12156
|
+
}
|
|
12157
|
+
return null;
|
|
12158
|
+
}
|
|
12159
|
+
function dfs(node, adj, visited, inStack, path) {
|
|
12160
|
+
if (inStack.has(node)) {
|
|
12161
|
+
const cycleStart = path.indexOf(node);
|
|
12162
|
+
return path.slice(cycleStart).concat(node);
|
|
12163
|
+
}
|
|
12164
|
+
if (visited.has(node)) return null;
|
|
12165
|
+
visited.add(node);
|
|
12166
|
+
inStack.add(node);
|
|
12167
|
+
path.push(node);
|
|
12168
|
+
for (const neighbor of adj.get(node) ?? []) {
|
|
12169
|
+
const cycle = dfs(neighbor, adj, visited, inStack, path);
|
|
12170
|
+
if (cycle) return cycle;
|
|
12171
|
+
}
|
|
12172
|
+
path.pop();
|
|
12173
|
+
inStack.delete(node);
|
|
12174
|
+
return null;
|
|
12175
|
+
}
|
|
12176
|
+
function topologicalSort(steps, edges) {
|
|
12177
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
12178
|
+
const adj = /* @__PURE__ */ new Map();
|
|
12179
|
+
for (const s of steps) {
|
|
12180
|
+
inDegree.set(s.id, 0);
|
|
12181
|
+
adj.set(s.id, []);
|
|
12182
|
+
}
|
|
12183
|
+
for (const e of edges) {
|
|
12184
|
+
if (!e.condition) {
|
|
12185
|
+
adj.get(e.from)?.push(e.to);
|
|
12186
|
+
inDegree.set(e.to, (inDegree.get(e.to) ?? 0) + 1);
|
|
12187
|
+
}
|
|
12188
|
+
}
|
|
12189
|
+
const queue = [];
|
|
12190
|
+
for (const [id, deg] of inDegree) {
|
|
12191
|
+
if (deg === 0) queue.push(id);
|
|
12192
|
+
}
|
|
12193
|
+
const sorted = [];
|
|
12194
|
+
while (queue.length > 0) {
|
|
12195
|
+
const node = queue.shift();
|
|
12196
|
+
sorted.push(node);
|
|
12197
|
+
for (const neighbor of adj.get(node) ?? []) {
|
|
12198
|
+
const newDeg = (inDegree.get(neighbor) ?? 1) - 1;
|
|
12199
|
+
inDegree.set(neighbor, newDeg);
|
|
12200
|
+
if (newDeg === 0) queue.push(neighbor);
|
|
12201
|
+
}
|
|
12202
|
+
}
|
|
12203
|
+
if (sorted.length !== steps.length) {
|
|
12204
|
+
return fail3("CYCLE_DETECTED", "Graph contains a cycle");
|
|
12205
|
+
}
|
|
12206
|
+
return ok3(sorted);
|
|
12207
|
+
}
|
|
12208
|
+
function getUnreachableSteps(completedSteps, edges, allSteps) {
|
|
12209
|
+
const completed = new Set(completedSteps);
|
|
12210
|
+
const reachable = new Set(completedSteps);
|
|
12211
|
+
const stepObjs = allSteps.map((id) => ({ id, name: id }));
|
|
12212
|
+
const sortResult = topologicalSort(stepObjs, edges);
|
|
12213
|
+
const order = sortResult.ok ? sortResult.data : allSteps;
|
|
12214
|
+
for (const stepId of order) {
|
|
12215
|
+
if (reachable.has(stepId)) continue;
|
|
12216
|
+
const preds = getPredecessors(stepId, edges);
|
|
12217
|
+
if (preds.length === 0) continue;
|
|
12218
|
+
const hasCompletedPred = preds.some((p) => completed.has(p));
|
|
12219
|
+
if (hasCompletedPred) {
|
|
12220
|
+
reachable.add(stepId);
|
|
12221
|
+
continue;
|
|
12222
|
+
}
|
|
12223
|
+
const reachablePreds = preds.filter((p) => reachable.has(p));
|
|
12224
|
+
if (reachablePreds.length >= 2) {
|
|
12225
|
+
reachable.add(stepId);
|
|
12226
|
+
}
|
|
12227
|
+
}
|
|
12228
|
+
return allSteps.filter((s) => !reachable.has(s));
|
|
12229
|
+
}
|
|
12230
|
+
|
|
12231
|
+
// src/modules/workflow/data/queries.ts
|
|
12232
|
+
function getWorkflowDefinition(db, workflowId) {
|
|
12233
|
+
const noteIds = db.getModuleNoteIds({ module: "workflow", type: "workflow" });
|
|
12234
|
+
if (noteIds.length === 0) {
|
|
12235
|
+
return fail("NOT_FOUND", `No workflow notes found.`);
|
|
12236
|
+
}
|
|
12237
|
+
const notes = db.getNotesByIds(noteIds);
|
|
12238
|
+
for (const [, note] of notes) {
|
|
12239
|
+
if (!note.metadata) continue;
|
|
12240
|
+
const meta = JSON.parse(note.metadata);
|
|
12241
|
+
if (meta.display_id !== workflowId) continue;
|
|
12242
|
+
const chunk = db.getFirstChunkForNote(note.id);
|
|
12243
|
+
if (!chunk) {
|
|
12244
|
+
return fail("NOT_FOUND", `Workflow "${workflowId}" has no content to parse.`);
|
|
12245
|
+
}
|
|
12246
|
+
let definition;
|
|
12247
|
+
try {
|
|
12248
|
+
const jsonMatch = chunk.content.match(/({[\s\S]*})/);
|
|
12249
|
+
const jsonStr = jsonMatch ? jsonMatch[1] : chunk.content;
|
|
12250
|
+
definition = JSON.parse(jsonStr);
|
|
12251
|
+
} catch {
|
|
12252
|
+
return fail("INVALID_INPUT", `Workflow "${workflowId}" body is not valid JSON.`);
|
|
12253
|
+
}
|
|
12254
|
+
return ok({
|
|
12255
|
+
note,
|
|
12256
|
+
definition,
|
|
12257
|
+
metadata: meta
|
|
12258
|
+
});
|
|
12259
|
+
}
|
|
12260
|
+
return fail("NOT_FOUND", `Workflow "${workflowId}" not found.`);
|
|
12261
|
+
}
|
|
12262
|
+
function listWorkflows(db, filters) {
|
|
12263
|
+
const noteIds = db.getModuleNoteIds({ module: "workflow", type: "workflow" });
|
|
12264
|
+
if (noteIds.length === 0) {
|
|
12265
|
+
return ok([]);
|
|
12266
|
+
}
|
|
12267
|
+
const notes = db.getNotesByIds(noteIds);
|
|
12268
|
+
const results = [];
|
|
12269
|
+
for (const [, note] of notes) {
|
|
12270
|
+
if (!note.metadata) continue;
|
|
12271
|
+
const meta = JSON.parse(note.metadata);
|
|
12272
|
+
if (filters?.project && meta.project !== filters.project) continue;
|
|
12273
|
+
if (filters?.status && meta.registration_status !== filters.status) continue;
|
|
12274
|
+
results.push(meta);
|
|
12275
|
+
}
|
|
12276
|
+
return ok(results);
|
|
12277
|
+
}
|
|
12278
|
+
function getInstanceByDisplayId(db, displayId) {
|
|
12279
|
+
const noteIds = db.getModuleNoteIds({ module: "pm", type: "task" });
|
|
12280
|
+
if (noteIds.length === 0) {
|
|
12281
|
+
return fail("NOT_FOUND", `No task notes found.`);
|
|
12282
|
+
}
|
|
12283
|
+
const notes = db.getNotesByIds(noteIds);
|
|
12284
|
+
for (const [, note] of notes) {
|
|
12285
|
+
if (!note.metadata) continue;
|
|
12286
|
+
const meta = JSON.parse(note.metadata);
|
|
12287
|
+
if (meta.display_id !== displayId) continue;
|
|
12288
|
+
if (!meta.workflow_id) {
|
|
12289
|
+
return fail("NOT_FOUND", `Task "${displayId}" is not a workflow instance.`);
|
|
12290
|
+
}
|
|
12291
|
+
return ok({
|
|
12292
|
+
note,
|
|
12293
|
+
metadata: {
|
|
12294
|
+
workflow_id: meta.workflow_id,
|
|
12295
|
+
workflow_version: meta.workflow_version ?? 1,
|
|
12296
|
+
instance_status: meta.instance_status ?? "placeholder",
|
|
12297
|
+
context: meta.context ?? {}
|
|
12298
|
+
}
|
|
12299
|
+
});
|
|
12300
|
+
}
|
|
12301
|
+
return fail("NOT_FOUND", `Task "${displayId}" not found.`);
|
|
12302
|
+
}
|
|
12303
|
+
function getInstanceStepStates(db, instanceDisplayId) {
|
|
12304
|
+
const instanceResult = getInstanceByDisplayId(db, instanceDisplayId);
|
|
12305
|
+
if (!instanceResult.ok) return instanceResult;
|
|
12306
|
+
const instanceNote = instanceResult.data.note;
|
|
12307
|
+
const relations = db.getRelationsFrom(instanceNote.id);
|
|
12308
|
+
const expandsToRelations = relations.filter((r) => r.type === "expands-to");
|
|
12309
|
+
if (expandsToRelations.length === 0) {
|
|
12310
|
+
return ok({
|
|
12311
|
+
steps: [],
|
|
12312
|
+
progress: { total: 0, done: 0, pruned: 0, active: 0, pending: 0 }
|
|
12313
|
+
});
|
|
12314
|
+
}
|
|
12315
|
+
const childIds = expandsToRelations.map((r) => r.targetId);
|
|
12316
|
+
const childNotes = db.getNotesByIds(childIds);
|
|
12317
|
+
const steps = [];
|
|
12318
|
+
let done = 0;
|
|
12319
|
+
let pruned = 0;
|
|
12320
|
+
let active = 0;
|
|
12321
|
+
let pending = 0;
|
|
12322
|
+
for (const [, childNote] of childNotes) {
|
|
12323
|
+
if (!childNote.metadata) continue;
|
|
12324
|
+
const meta = JSON.parse(childNote.metadata);
|
|
12325
|
+
const status = meta.status ?? "pending";
|
|
12326
|
+
const stepId = meta.step_id ?? "";
|
|
12327
|
+
const taskDisplayId = meta.display_id ?? "";
|
|
12328
|
+
steps.push({ stepId, taskDisplayId, status });
|
|
12329
|
+
switch (status) {
|
|
12330
|
+
case "done":
|
|
12331
|
+
done++;
|
|
12332
|
+
break;
|
|
12333
|
+
case "pruned":
|
|
12334
|
+
case "cancelled":
|
|
12335
|
+
pruned++;
|
|
12336
|
+
break;
|
|
12337
|
+
case "claimed":
|
|
12338
|
+
case "in-progress":
|
|
12339
|
+
active++;
|
|
12340
|
+
break;
|
|
12341
|
+
default:
|
|
12342
|
+
pending++;
|
|
12343
|
+
break;
|
|
12344
|
+
}
|
|
12345
|
+
}
|
|
12346
|
+
return ok({
|
|
12347
|
+
steps,
|
|
12348
|
+
progress: { total: steps.length, done, pruned, active, pending }
|
|
12349
|
+
});
|
|
12350
|
+
}
|
|
12351
|
+
|
|
12352
|
+
// src/modules/workflow/data/workflow-ops.ts
|
|
12353
|
+
function extractNoteBody(filePath) {
|
|
12354
|
+
if (!existsSync22(filePath)) return "";
|
|
12355
|
+
const content = readFileSync20(filePath, "utf-8");
|
|
12356
|
+
const fmEnd = content.indexOf("\n---", 4);
|
|
12357
|
+
if (fmEnd === -1) return "";
|
|
12358
|
+
let body = content.slice(fmEnd + 4).trim();
|
|
12359
|
+
if (body.startsWith("#")) {
|
|
12360
|
+
const headingEnd = body.indexOf("\n");
|
|
12361
|
+
if (headingEnd === -1) return "";
|
|
12362
|
+
body = body.slice(headingEnd + 1).trim();
|
|
12363
|
+
}
|
|
12364
|
+
return body;
|
|
12365
|
+
}
|
|
12366
|
+
async function registerWorkflow(db, _config, _embedder, noteId) {
|
|
12367
|
+
const note = db.getNoteById(noteId);
|
|
12368
|
+
if (!note) {
|
|
12369
|
+
return fail("NOT_FOUND", `Note "${noteId}" not found`);
|
|
12370
|
+
}
|
|
12371
|
+
if (note.type !== "workflow") {
|
|
12372
|
+
return fail(
|
|
12373
|
+
"INVALID_NOTE_TYPE",
|
|
12374
|
+
`Note "${noteId}" is type "${note.type}", expected "workflow"`
|
|
12375
|
+
);
|
|
12376
|
+
}
|
|
12377
|
+
const body = extractNoteBody(note.filePath);
|
|
12378
|
+
let definition;
|
|
12379
|
+
try {
|
|
12380
|
+
definition = JSON.parse(body);
|
|
12381
|
+
} catch {
|
|
12382
|
+
return fail("PARSE_ERROR", `Failed to parse workflow definition as JSON`);
|
|
12383
|
+
}
|
|
12384
|
+
const dagResult = validateDag(definition);
|
|
12385
|
+
if (!dagResult.ok) {
|
|
12386
|
+
const code = dagResult.error.code;
|
|
12387
|
+
return fail(code, dagResult.error.message, dagResult.error.details);
|
|
12388
|
+
}
|
|
12389
|
+
const existingMeta = note.metadata ? JSON.parse(note.metadata) : {};
|
|
12390
|
+
const currentVersion = existingMeta.registration_status === "registered" ? existingMeta.version ?? 0 : 0;
|
|
12391
|
+
const newVersion = currentVersion + 1;
|
|
12392
|
+
const updatedMeta = {
|
|
12393
|
+
...existingMeta,
|
|
12394
|
+
registration_status: "registered",
|
|
12395
|
+
version: newVersion,
|
|
12396
|
+
step_count: definition.steps.length,
|
|
12397
|
+
edge_count: definition.edges.length,
|
|
12398
|
+
name: definition.name,
|
|
12399
|
+
display_id: existingMeta.display_id ?? noteId
|
|
12400
|
+
};
|
|
12401
|
+
const updatedNote = { ...note, metadata: JSON.stringify(updatedMeta) };
|
|
12402
|
+
db.upsertNote(updatedNote);
|
|
12403
|
+
const workflowMeta = {
|
|
12404
|
+
display_id: updatedMeta.display_id ?? noteId,
|
|
12405
|
+
name: definition.name,
|
|
12406
|
+
version: newVersion,
|
|
12407
|
+
registration_status: "registered",
|
|
12408
|
+
step_count: definition.steps.length,
|
|
12409
|
+
edge_count: definition.edges.length
|
|
12410
|
+
};
|
|
12411
|
+
return ok(workflowMeta);
|
|
12412
|
+
}
|
|
12413
|
+
async function instantiateWorkflow(db, config, embedder, workflowId, project, context, options) {
|
|
12414
|
+
const defResult = getWorkflowDefinition(db, workflowId);
|
|
12415
|
+
if (!defResult.ok) {
|
|
12416
|
+
return fail("NOT_FOUND", defResult.error.message);
|
|
12417
|
+
}
|
|
12418
|
+
const { definition, metadata: wfMeta, note: defNote } = defResult.data;
|
|
12419
|
+
if (definition.parameters) {
|
|
12420
|
+
for (const param of definition.parameters) {
|
|
12421
|
+
if (param.required && !(param.name in context)) {
|
|
12422
|
+
return fail("MISSING_PARAMETER", `Required parameter "${param.name}" not provided`);
|
|
12423
|
+
}
|
|
12424
|
+
}
|
|
12425
|
+
}
|
|
12426
|
+
const taskResult = await createTask(db, config, embedder, {
|
|
12427
|
+
project,
|
|
12428
|
+
workstream: 1,
|
|
12429
|
+
name: `[${definition.name}] instance`,
|
|
12430
|
+
description: `Workflow instance of ${workflowId}`,
|
|
12431
|
+
category: "implementation",
|
|
12432
|
+
priority: "medium"
|
|
12433
|
+
});
|
|
12434
|
+
if (!taskResult.ok) {
|
|
12435
|
+
return fail("NOT_FOUND", taskResult.error.message);
|
|
12436
|
+
}
|
|
12437
|
+
const taskDisplayId = taskResult.data.display_id;
|
|
12438
|
+
const taskNoteIds = db.getModuleNoteIds({ module: "pm", type: "task" });
|
|
12439
|
+
const taskNotes = db.getNotesByIds(taskNoteIds);
|
|
12440
|
+
let taskNoteId;
|
|
12441
|
+
for (const [, note] of taskNotes) {
|
|
12442
|
+
if (!note.metadata) continue;
|
|
12443
|
+
const meta = JSON.parse(note.metadata);
|
|
12444
|
+
if (meta.display_id === taskDisplayId) {
|
|
12445
|
+
taskNoteId = note.id;
|
|
12446
|
+
const updatedMeta = {
|
|
12447
|
+
...meta,
|
|
12448
|
+
workflow_id: workflowId,
|
|
12449
|
+
workflow_version: wfMeta.version,
|
|
12450
|
+
instance_status: "placeholder",
|
|
12451
|
+
context
|
|
12452
|
+
};
|
|
12453
|
+
db.upsertNote({ ...note, metadata: JSON.stringify(updatedMeta) });
|
|
12454
|
+
break;
|
|
12455
|
+
}
|
|
12456
|
+
}
|
|
12457
|
+
if (taskNoteId) {
|
|
12458
|
+
const relations = [{ sourceId: taskNoteId, targetId: defNote.id, type: "instance-of" }];
|
|
12459
|
+
if (options?.iterationOf) {
|
|
12460
|
+
const prevInstanceResult = getInstanceByDisplayId(db, options.iterationOf);
|
|
12461
|
+
if (prevInstanceResult.ok) {
|
|
12462
|
+
relations.push({
|
|
12463
|
+
sourceId: taskNoteId,
|
|
12464
|
+
targetId: prevInstanceResult.data.note.id,
|
|
12465
|
+
type: "iteration-of"
|
|
12466
|
+
});
|
|
12467
|
+
}
|
|
12468
|
+
}
|
|
12469
|
+
db.upsertRelations(taskNoteId, relations);
|
|
12470
|
+
}
|
|
12471
|
+
return ok({
|
|
12472
|
+
display_id: taskDisplayId,
|
|
12473
|
+
workflow_id: workflowId,
|
|
12474
|
+
workflow_version: wfMeta.version,
|
|
12475
|
+
instance_status: "placeholder",
|
|
12476
|
+
context
|
|
12477
|
+
});
|
|
12478
|
+
}
|
|
12479
|
+
async function expandWorkflow(db, config, embedder, instanceDisplayId) {
|
|
12480
|
+
const instanceResult = getInstanceByDisplayId(db, instanceDisplayId);
|
|
12481
|
+
if (!instanceResult.ok) {
|
|
12482
|
+
return fail("NOT_FOUND", instanceResult.error.message);
|
|
12483
|
+
}
|
|
12484
|
+
const { note: instanceNote, metadata: instanceMeta } = instanceResult.data;
|
|
12485
|
+
if (instanceMeta.instance_status === "expanded" || instanceMeta.instance_status === "executing" || instanceMeta.instance_status === "completed" || instanceMeta.instance_status === "collapsed") {
|
|
12486
|
+
return fail("ALREADY_EXPANDED", `Instance "${instanceDisplayId}" is already expanded`);
|
|
12487
|
+
}
|
|
12488
|
+
const relations = db.getRelationsFrom(instanceNote.id);
|
|
12489
|
+
const instanceOfRel = relations.find((r) => r.type === "instance-of");
|
|
12490
|
+
if (!instanceOfRel) {
|
|
12491
|
+
return fail("DEFINITION_NOT_FOUND", `No instance-of relation found for "${instanceDisplayId}"`);
|
|
12492
|
+
}
|
|
12493
|
+
const defNote = db.getNoteById(instanceOfRel.targetId);
|
|
12494
|
+
if (!defNote) {
|
|
12495
|
+
return fail("DEFINITION_NOT_FOUND", `Workflow definition note not found`);
|
|
12496
|
+
}
|
|
12497
|
+
const chunk = db.getFirstChunkForNote(defNote.id);
|
|
12498
|
+
if (!chunk) {
|
|
12499
|
+
return fail("DEFINITION_NOT_FOUND", `Workflow definition has no content`);
|
|
12500
|
+
}
|
|
12501
|
+
let definition;
|
|
12502
|
+
try {
|
|
12503
|
+
const jsonMatch = chunk.content.match(/({[\s\S]*})/);
|
|
12504
|
+
definition = JSON.parse(jsonMatch ? jsonMatch[1] : chunk.content);
|
|
12505
|
+
} catch {
|
|
12506
|
+
return fail("DEFINITION_NOT_FOUND", `Failed to parse workflow definition`);
|
|
12507
|
+
}
|
|
12508
|
+
const sortResult = topologicalSort(definition.steps, definition.edges);
|
|
12509
|
+
if (!sortResult.ok) {
|
|
12510
|
+
return fail("DEFINITION_NOT_FOUND", `Failed to sort workflow steps`);
|
|
12511
|
+
}
|
|
12512
|
+
const topoOrder = sortResult.data;
|
|
12513
|
+
const stepToTaskNoteId = /* @__PURE__ */ new Map();
|
|
12514
|
+
const stepToDisplayId = /* @__PURE__ */ new Map();
|
|
12515
|
+
const instanceParsedMeta = JSON.parse(instanceNote.metadata);
|
|
12516
|
+
const instanceProject = instanceParsedMeta.project ?? "TST";
|
|
12517
|
+
const instanceWorkstream = instanceParsedMeta.workstream ?? 1;
|
|
12518
|
+
for (const stepId of topoOrder) {
|
|
12519
|
+
const stepDef = definition.steps.find((s) => s.id === stepId);
|
|
12520
|
+
const stepName = stepDef?.name ?? stepId;
|
|
12521
|
+
const taskResult = await createTask(db, config, embedder, {
|
|
12522
|
+
project: instanceProject,
|
|
12523
|
+
workstream: instanceWorkstream,
|
|
12524
|
+
name: `[${definition.name}] ${stepName}`,
|
|
12525
|
+
description: `Step "${stepId}" of workflow ${instanceDisplayId}`,
|
|
12526
|
+
category: stepDef?.category ?? "implementation",
|
|
12527
|
+
priority: stepDef?.priority ?? "medium"
|
|
12528
|
+
});
|
|
12529
|
+
if (!taskResult.ok) continue;
|
|
12530
|
+
const childDisplayId = taskResult.data.display_id;
|
|
12531
|
+
stepToDisplayId.set(stepId, childDisplayId);
|
|
12532
|
+
const matchingNotes = getPmNotes(db, "task", { display_id: childDisplayId });
|
|
12533
|
+
if (matchingNotes.length > 0) {
|
|
12534
|
+
const childNote = matchingNotes[0];
|
|
12535
|
+
const meta = childNote.metadata ? JSON.parse(childNote.metadata) : {};
|
|
12536
|
+
const updatedChildMeta = { ...meta, step_id: stepId };
|
|
12537
|
+
db.upsertNote({ ...childNote, metadata: JSON.stringify(updatedChildMeta) });
|
|
12538
|
+
stepToTaskNoteId.set(stepId, childNote.id);
|
|
12539
|
+
}
|
|
12540
|
+
}
|
|
12541
|
+
const existingInstanceRelations = db.getRelationsFrom(instanceNote.id);
|
|
12542
|
+
const instanceRelations = [...existingInstanceRelations];
|
|
12543
|
+
for (const [, childNoteId] of stepToTaskNoteId) {
|
|
12544
|
+
instanceRelations.push({
|
|
12545
|
+
sourceId: instanceNote.id,
|
|
12546
|
+
targetId: childNoteId,
|
|
12547
|
+
type: "expands-to"
|
|
12548
|
+
});
|
|
12549
|
+
}
|
|
12550
|
+
db.upsertRelations(instanceNote.id, instanceRelations);
|
|
12551
|
+
const unconditionalEdges = definition.edges.filter((e) => !e.condition);
|
|
12552
|
+
const edgesBySource = /* @__PURE__ */ new Map();
|
|
12553
|
+
let edgeCount = 0;
|
|
12554
|
+
for (const edge of unconditionalEdges) {
|
|
12555
|
+
const fromNoteId = stepToTaskNoteId.get(edge.from);
|
|
12556
|
+
const toNoteId = stepToTaskNoteId.get(edge.to);
|
|
12557
|
+
if (fromNoteId && toNoteId) {
|
|
12558
|
+
if (!edgesBySource.has(toNoteId)) {
|
|
12559
|
+
edgesBySource.set(toNoteId, []);
|
|
12560
|
+
}
|
|
12561
|
+
edgesBySource.get(toNoteId).push({
|
|
12562
|
+
sourceId: toNoteId,
|
|
12563
|
+
targetId: fromNoteId,
|
|
12564
|
+
type: "depends_on"
|
|
12565
|
+
});
|
|
12566
|
+
edgeCount++;
|
|
12567
|
+
}
|
|
12568
|
+
}
|
|
12569
|
+
for (const [sourceNoteId, rels] of edgesBySource) {
|
|
12570
|
+
const existing = db.getRelationsFrom(sourceNoteId);
|
|
12571
|
+
db.upsertRelations(sourceNoteId, [...existing, ...rels]);
|
|
12572
|
+
}
|
|
12573
|
+
const currentMeta = JSON.parse(instanceNote.metadata);
|
|
12574
|
+
const expandedMeta = { ...currentMeta, instance_status: "expanded" };
|
|
12575
|
+
db.upsertNote({ ...instanceNote, metadata: JSON.stringify(expandedMeta) });
|
|
12576
|
+
return ok({ tasksCreated: stepToTaskNoteId.size, edges: edgeCount });
|
|
12577
|
+
}
|
|
12578
|
+
function getWorkflowStatus(db, instanceDisplayId) {
|
|
12579
|
+
const instanceResult = getInstanceByDisplayId(db, instanceDisplayId);
|
|
12580
|
+
if (!instanceResult.ok) {
|
|
12581
|
+
return fail("NOT_FOUND", instanceResult.error.message);
|
|
12582
|
+
}
|
|
12583
|
+
const stepsResult = getInstanceStepStates(db, instanceDisplayId);
|
|
12584
|
+
if (!stepsResult.ok) {
|
|
12585
|
+
return fail("NOT_FOUND", stepsResult.error.message);
|
|
12586
|
+
}
|
|
12587
|
+
const { steps } = stepsResult.data;
|
|
12588
|
+
const defResult = getWorkflowDefinition(db, instanceResult.data.metadata.workflow_id);
|
|
12589
|
+
if (defResult.ok) {
|
|
12590
|
+
const { definition } = defResult.data;
|
|
12591
|
+
const sortResult = topologicalSort(definition.steps, definition.edges);
|
|
12592
|
+
if (sortResult.ok) {
|
|
12593
|
+
const orderIndex = new Map(sortResult.data.map((id, i) => [id, i]));
|
|
12594
|
+
steps.sort((a, b) => {
|
|
12595
|
+
const ai = orderIndex.get(a.stepId) ?? Infinity;
|
|
12596
|
+
const bi = orderIndex.get(b.stepId) ?? Infinity;
|
|
12597
|
+
return ai - bi;
|
|
12598
|
+
});
|
|
12599
|
+
}
|
|
12600
|
+
}
|
|
12601
|
+
return ok({
|
|
12602
|
+
instance: instanceResult.data.metadata,
|
|
12603
|
+
steps,
|
|
12604
|
+
progress: stepsResult.data.progress
|
|
12605
|
+
});
|
|
12606
|
+
}
|
|
12607
|
+
async function collapseWorkflow(db, config, embedder, instanceDisplayId) {
|
|
12608
|
+
const instanceResult = getInstanceByDisplayId(db, instanceDisplayId);
|
|
12609
|
+
if (!instanceResult.ok) {
|
|
12610
|
+
return fail("NOT_FOUND", instanceResult.error.message);
|
|
12611
|
+
}
|
|
12612
|
+
const { note: instanceNote, metadata: instanceMeta } = instanceResult.data;
|
|
12613
|
+
if (instanceMeta.instance_status === "placeholder") {
|
|
12614
|
+
return fail("NOT_EXPANDED", `Instance "${instanceDisplayId}" has not been expanded`);
|
|
12615
|
+
}
|
|
12616
|
+
const stepsResult = getInstanceStepStates(db, instanceDisplayId);
|
|
12617
|
+
if (!stepsResult.ok) {
|
|
12618
|
+
return fail("NOT_FOUND", stepsResult.error.message);
|
|
12619
|
+
}
|
|
12620
|
+
const { steps } = stepsResult.data;
|
|
12621
|
+
const incomplete = steps.filter(
|
|
12622
|
+
(s) => s.status !== "done" && s.status !== "pruned" && s.status !== "cancelled"
|
|
12623
|
+
);
|
|
12624
|
+
if (incomplete.length > 0) {
|
|
12625
|
+
return fail("INCOMPLETE_WORKFLOW", `${incomplete.length} tasks are not done/pruned`, {
|
|
12626
|
+
incomplete: incomplete.map((s) => s.taskDisplayId)
|
|
12627
|
+
});
|
|
12628
|
+
}
|
|
12629
|
+
let tasksArchived = 0;
|
|
12630
|
+
const relations = db.getRelationsFrom(instanceNote.id);
|
|
12631
|
+
const expandsToRelations = relations.filter((r) => r.type === "expands-to");
|
|
12632
|
+
const childIds = expandsToRelations.map((r) => r.targetId);
|
|
12633
|
+
const childNotes = db.getNotesByIds(childIds);
|
|
12634
|
+
for (const [, childNote] of childNotes) {
|
|
12635
|
+
if (!childNote.metadata) continue;
|
|
12636
|
+
const meta = JSON.parse(childNote.metadata);
|
|
12637
|
+
if (meta.status !== "done") {
|
|
12638
|
+
const updatedMeta = { ...meta, status: "done" };
|
|
12639
|
+
db.upsertNote({ ...childNote, metadata: JSON.stringify(updatedMeta) });
|
|
12640
|
+
}
|
|
12641
|
+
tasksArchived++;
|
|
12642
|
+
}
|
|
12643
|
+
const currentMeta = JSON.parse(instanceNote.metadata);
|
|
12644
|
+
const collapsedMeta = { ...currentMeta, instance_status: "collapsed" };
|
|
12645
|
+
db.upsertNote({ ...instanceNote, metadata: JSON.stringify(collapsedMeta) });
|
|
12646
|
+
const summaryNoteId = `${instanceDisplayId}-summary`;
|
|
12647
|
+
return ok({ summaryNoteId, tasksArchived });
|
|
12648
|
+
}
|
|
12649
|
+
|
|
12650
|
+
// src/modules/workflow/engine/templates.ts
|
|
12651
|
+
import { readFileSync as readFileSync21, existsSync as existsSync23, readdirSync as readdirSync5 } from "fs";
|
|
12652
|
+
import { join as join21, basename as basename7 } from "path";
|
|
12653
|
+
function findTemplatesDir() {
|
|
12654
|
+
const sourceDir = join21(import.meta.dirname, "..", "templates");
|
|
12655
|
+
if (existsSync23(sourceDir) && existsSync23(join21(sourceDir, "implementation-compact.md"))) {
|
|
12656
|
+
return sourceDir;
|
|
12657
|
+
}
|
|
12658
|
+
const bundledDir = join21(import.meta.dirname, "templates");
|
|
12659
|
+
if (existsSync23(bundledDir) && existsSync23(join21(bundledDir, "implementation-compact.md"))) {
|
|
12660
|
+
return bundledDir;
|
|
12661
|
+
}
|
|
12662
|
+
return sourceDir;
|
|
12663
|
+
}
|
|
12664
|
+
var TEMPLATES_DIR = findTemplatesDir();
|
|
12665
|
+
function resolveTemplate(templateName) {
|
|
12666
|
+
const normalized = templateName.endsWith(".md") ? templateName : `${templateName}.md`;
|
|
12667
|
+
const filePath = join21(TEMPLATES_DIR, normalized);
|
|
12668
|
+
if (!existsSync23(filePath)) {
|
|
12669
|
+
const available = listTemplateNames();
|
|
12670
|
+
return fail(
|
|
12671
|
+
"NOT_FOUND",
|
|
12672
|
+
`Template "${templateName}" not found. Available: ${available.join(", ")}`
|
|
12673
|
+
);
|
|
12674
|
+
}
|
|
12675
|
+
const content = readFileSync21(filePath, "utf-8");
|
|
12676
|
+
const variables = extractVariables(content);
|
|
12677
|
+
const name = basename7(normalized, ".md");
|
|
12678
|
+
return ok({ name, content, variables });
|
|
12679
|
+
}
|
|
12680
|
+
function listTemplateNames() {
|
|
12681
|
+
if (!existsSync23(TEMPLATES_DIR)) return [];
|
|
12682
|
+
return readdirSync5(TEMPLATES_DIR).filter((f) => f.endsWith(".md")).map((f) => basename7(f, ".md")).sort();
|
|
12683
|
+
}
|
|
12684
|
+
function extractVariables(content) {
|
|
12685
|
+
const matches = content.matchAll(/\{\{(\w+)\}\}/g);
|
|
12686
|
+
const vars = /* @__PURE__ */ new Set();
|
|
12687
|
+
for (const m of matches) {
|
|
12688
|
+
vars.add(m[1]);
|
|
12689
|
+
}
|
|
12690
|
+
return [...vars].sort();
|
|
12691
|
+
}
|
|
12692
|
+
function renderTemplate(templateName, params) {
|
|
12693
|
+
const resolved = resolveTemplate(templateName);
|
|
12694
|
+
if (!resolved.ok) return resolved;
|
|
12695
|
+
let rendered = resolved.data.content;
|
|
12696
|
+
for (const [key, value] of Object.entries(params)) {
|
|
12697
|
+
rendered = rendered.replaceAll(`{{${key}}}`, value);
|
|
12698
|
+
}
|
|
12699
|
+
return ok(rendered);
|
|
12700
|
+
}
|
|
12701
|
+
|
|
12702
|
+
// src/modules/workflow/engine/dispatch.ts
|
|
12703
|
+
var CATEGORY_TO_PREFIX = {
|
|
12704
|
+
implementation: "feature",
|
|
12705
|
+
feature: "feature",
|
|
12706
|
+
testing: "feature",
|
|
12707
|
+
documentation: "feature",
|
|
12708
|
+
design: "feature",
|
|
12709
|
+
configuration: "feature",
|
|
12710
|
+
migration: "feature",
|
|
12711
|
+
bug: "bug",
|
|
12712
|
+
fix: "bug",
|
|
12713
|
+
refactor: "refactor",
|
|
12714
|
+
infrastructure: "infrastructure",
|
|
12715
|
+
research: "feature",
|
|
12716
|
+
review: "feature"
|
|
12717
|
+
};
|
|
12718
|
+
function slugify2(text) {
|
|
12719
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
|
|
12720
|
+
}
|
|
12721
|
+
function buildBranchName(category, title, branchPrefix) {
|
|
12722
|
+
const prefixKey = CATEGORY_TO_PREFIX[category] ?? "feature";
|
|
12723
|
+
const prefix = branchPrefix?.[prefixKey] ?? defaultBranchPrefix(prefixKey);
|
|
12724
|
+
const slug = slugify2(title);
|
|
12725
|
+
return `${prefix}/${slug}`;
|
|
12726
|
+
}
|
|
12727
|
+
function defaultBranchPrefix(key) {
|
|
12728
|
+
const defaults = {
|
|
12729
|
+
feature: "feat",
|
|
12730
|
+
bug: "fix",
|
|
12731
|
+
refactor: "refactor",
|
|
12732
|
+
infrastructure: "infra"
|
|
12733
|
+
};
|
|
12734
|
+
return defaults[key];
|
|
12735
|
+
}
|
|
12736
|
+
function resolveStepMode(db, taskId) {
|
|
12737
|
+
const taskNotes = getPmNotes(db, "task", { display_id: taskId });
|
|
12738
|
+
if (taskNotes.length === 0) return void 0;
|
|
12739
|
+
const taskNote = taskNotes[0];
|
|
12740
|
+
if (!taskNote.metadata) return void 0;
|
|
12741
|
+
const taskMeta = JSON.parse(taskNote.metadata);
|
|
12742
|
+
const stepId = taskMeta.step_id;
|
|
12743
|
+
if (!stepId) return void 0;
|
|
12744
|
+
const incomingRelations = db.getRelationsTo(taskNote.id);
|
|
12745
|
+
const expandsToRel = incomingRelations.find((r) => r.type === "expands-to");
|
|
12746
|
+
if (!expandsToRel) return void 0;
|
|
12747
|
+
const instanceNote = db.getNoteById(expandsToRel.sourceId);
|
|
12748
|
+
if (!instanceNote?.metadata) return void 0;
|
|
12749
|
+
const instanceMeta = JSON.parse(instanceNote.metadata);
|
|
12750
|
+
const workflowId = instanceMeta.workflow_id;
|
|
12751
|
+
if (!workflowId) return void 0;
|
|
12752
|
+
const defResult = getWorkflowDefinition(db, workflowId);
|
|
12753
|
+
if (!defResult.ok) return void 0;
|
|
12754
|
+
const step = defResult.data.definition.steps.find((s) => s.id === stepId);
|
|
12755
|
+
return step?.mode;
|
|
12756
|
+
}
|
|
12757
|
+
function injectWorkflowContext(db, taskId, vars) {
|
|
12758
|
+
const taskNotes = getPmNotes(db, "task", { display_id: taskId });
|
|
12759
|
+
if (taskNotes.length === 0) return;
|
|
12760
|
+
const taskNote = taskNotes[0];
|
|
12761
|
+
if (!taskNote.metadata) return;
|
|
12762
|
+
const taskMeta = JSON.parse(taskNote.metadata);
|
|
12763
|
+
const stepId = taskMeta.step_id;
|
|
12764
|
+
if (stepId) {
|
|
12765
|
+
vars.STEP_ID = stepId;
|
|
12766
|
+
}
|
|
12767
|
+
const incomingRelations = db.getRelationsTo(taskNote.id);
|
|
12768
|
+
const expandsToRel = incomingRelations.find((r) => r.type === "expands-to");
|
|
12769
|
+
if (!expandsToRel) return;
|
|
12770
|
+
const instanceNote = db.getNoteById(expandsToRel.sourceId);
|
|
12771
|
+
if (!instanceNote?.metadata) return;
|
|
12772
|
+
const instanceMeta = JSON.parse(instanceNote.metadata);
|
|
12773
|
+
const instanceDisplayId = instanceMeta.display_id;
|
|
12774
|
+
const workflowId = instanceMeta.workflow_id;
|
|
12775
|
+
const context = instanceMeta.context;
|
|
12776
|
+
if (instanceDisplayId) {
|
|
12777
|
+
vars.INSTANCE_ID = instanceDisplayId;
|
|
12778
|
+
}
|
|
12779
|
+
if (workflowId) {
|
|
12780
|
+
vars.WORKFLOW_ID = workflowId;
|
|
12781
|
+
const defResult = getWorkflowDefinition(db, workflowId);
|
|
12782
|
+
if (defResult.ok) {
|
|
12783
|
+
vars.WORKFLOW_NAME = defResult.data.definition.name;
|
|
12784
|
+
}
|
|
12785
|
+
}
|
|
12786
|
+
if (context) {
|
|
12787
|
+
for (const [key, value] of Object.entries(context)) {
|
|
12788
|
+
vars[key] = value;
|
|
12789
|
+
const upperKey = key.toUpperCase();
|
|
12790
|
+
if (!(upperKey in vars)) {
|
|
12791
|
+
vars[upperKey] = value;
|
|
12792
|
+
}
|
|
12793
|
+
}
|
|
12794
|
+
}
|
|
12795
|
+
}
|
|
12796
|
+
async function dispatchTemplate(db, config, embedder, taskId, templateName, opts = {}) {
|
|
12797
|
+
const prefix = taskId.split("-")[0];
|
|
12798
|
+
const taskResult = getTask(db, taskId);
|
|
12799
|
+
if (!taskResult.ok) {
|
|
12800
|
+
return fail(taskResult.error.code, taskResult.error.message, taskResult.error.details);
|
|
12801
|
+
}
|
|
12802
|
+
const task = taskResult.data;
|
|
12803
|
+
const projectResult = getProject(db, prefix);
|
|
12804
|
+
const vars = {};
|
|
12805
|
+
vars.TASK_ID = taskId;
|
|
12806
|
+
vars.BRAIN_TASK_ID = taskId;
|
|
12807
|
+
vars.PROJECT_PREFIX = prefix;
|
|
12808
|
+
if (projectResult.ok) {
|
|
12809
|
+
const p = projectResult.data;
|
|
12810
|
+
vars.REPO_PATH = p.path ?? "";
|
|
12811
|
+
vars.BUILD_CMD = p.commands?.build ?? "N/A";
|
|
12812
|
+
vars.TEST_CMD = p.commands?.test ?? "N/A";
|
|
12813
|
+
vars.TYPECHECK_CMD = p.commands?.typecheck ?? "N/A";
|
|
12814
|
+
vars.LINT_CMD = p.commands?.lint ?? "N/A";
|
|
12815
|
+
vars.BASE_BRANCH = p.default_branch ?? "main";
|
|
12816
|
+
vars.REVIEW_THRESHOLD = String(p.review_threshold ?? 5);
|
|
12817
|
+
vars.BRANCH_NAME = opts.branch ?? buildBranchName(task.category, task.title ?? taskId, p.branch_prefix);
|
|
12818
|
+
} else {
|
|
12819
|
+
vars.REPO_PATH = "";
|
|
12820
|
+
vars.BUILD_CMD = "N/A";
|
|
12821
|
+
vars.TEST_CMD = "N/A";
|
|
12822
|
+
vars.TYPECHECK_CMD = "N/A";
|
|
12823
|
+
vars.LINT_CMD = "N/A";
|
|
12824
|
+
vars.BASE_BRANCH = "main";
|
|
12825
|
+
vars.REVIEW_THRESHOLD = "5";
|
|
12826
|
+
vars.BRANCH_NAME = opts.branch ?? buildBranchName(task.category, task.title ?? taskId);
|
|
12827
|
+
}
|
|
12828
|
+
vars.WORKTREE_PATH = opts.worktree ?? "";
|
|
12829
|
+
const taskNotes = getPmNotes(db, "task", { display_id: taskId });
|
|
12830
|
+
if (taskNotes.length > 0) {
|
|
12831
|
+
const body = readTaskBody(taskNotes[0]);
|
|
12832
|
+
vars.IMPLEMENTATION_INSTRUCTIONS = body;
|
|
12833
|
+
vars.TASK_DESCRIPTION = body;
|
|
12834
|
+
}
|
|
12835
|
+
if (task.claim_token) {
|
|
12836
|
+
vars.CLAIM_TOKEN = task.claim_token;
|
|
12837
|
+
}
|
|
12838
|
+
if (opts.claim && !opts.dryRun) {
|
|
12839
|
+
const claimResult = await updateTaskStatus(db, config, embedder, taskId, "claimed");
|
|
12840
|
+
if (!claimResult.ok) {
|
|
12841
|
+
return fail(claimResult.error.code, claimResult.error.message, claimResult.error.details);
|
|
12842
|
+
}
|
|
12843
|
+
const claim = generateClaim();
|
|
12844
|
+
vars.CLAIM_TOKEN = claim.token;
|
|
12845
|
+
}
|
|
12846
|
+
injectWorkflowContext(db, taskId, vars);
|
|
12847
|
+
const stepMode = resolveStepMode(db, taskId);
|
|
12848
|
+
const rendered = renderTemplate(templateName, vars);
|
|
12849
|
+
if (!rendered.ok) return rendered;
|
|
12850
|
+
const unfilledMatches = rendered.data.matchAll(/\{\{(\w+)\}\}/g);
|
|
12851
|
+
const unfilled = [...new Set([...unfilledMatches].map((m) => m[1]))].sort();
|
|
12852
|
+
return ok({
|
|
12853
|
+
rendered: rendered.data,
|
|
12854
|
+
template: templateName,
|
|
12855
|
+
taskId,
|
|
12856
|
+
variables: vars,
|
|
12857
|
+
unfilled,
|
|
12858
|
+
mode: stepMode
|
|
12859
|
+
});
|
|
12860
|
+
}
|
|
12861
|
+
|
|
12862
|
+
// src/modules/workflow/commands/workflow.ts
|
|
12863
|
+
function formatWorkflowLine(wf) {
|
|
12864
|
+
return `${wf.display_id} - ${wf.name} v${wf.version} [${wf.registration_status}]`;
|
|
12865
|
+
}
|
|
12866
|
+
function formatError2(error, json) {
|
|
12867
|
+
if (json) {
|
|
12868
|
+
return JSON.stringify({ error: true, code: error.code, message: error.message });
|
|
12869
|
+
}
|
|
12870
|
+
return `Error [${error.code}]: ${error.message}`;
|
|
12871
|
+
}
|
|
12872
|
+
function createWorkflowCommand() {
|
|
12873
|
+
const cmd = new Command44("workflow").description("Workflow definition management");
|
|
12874
|
+
cmd.command("register").description("Register a workflow definition").argument("<note-id>", "Note ID of the workflow to register").option("--json", "Output JSON").action(async (noteId, opts) => {
|
|
12875
|
+
await withBrain(async (svc) => {
|
|
12876
|
+
const result = await registerWorkflow(svc.db, svc.config, svc.embedder, noteId);
|
|
12877
|
+
if (!result.ok) {
|
|
12878
|
+
process.stderr.write(formatError2(result.error, !!opts.json) + "\n");
|
|
12879
|
+
process.exitCode = 1;
|
|
12880
|
+
return;
|
|
12881
|
+
}
|
|
12882
|
+
const meta = result.data;
|
|
12883
|
+
if (opts.json) {
|
|
12884
|
+
process.stdout.write(JSON.stringify(meta, null, 2) + "\n");
|
|
12885
|
+
} else {
|
|
12886
|
+
process.stdout.write(`Registered workflow: ${meta.name}
|
|
12887
|
+
`);
|
|
12888
|
+
process.stdout.write(` Version: ${meta.version}
|
|
12889
|
+
`);
|
|
12890
|
+
process.stdout.write(` Steps: ${meta.step_count}
|
|
12891
|
+
`);
|
|
12892
|
+
process.stdout.write(` Edges: ${meta.edge_count}
|
|
12893
|
+
`);
|
|
12894
|
+
}
|
|
12895
|
+
});
|
|
12896
|
+
});
|
|
12897
|
+
cmd.command("list").description("List registered workflows").option("--project <prefix>", "Filter by project prefix").option("--status <status>", "Filter by registration status").option("--json", "Output JSON").action(async (opts) => {
|
|
12898
|
+
await withBrain(async (svc) => {
|
|
12899
|
+
const filters = {};
|
|
12900
|
+
if (opts.project) filters.project = opts.project;
|
|
12901
|
+
if (opts.status) filters.status = opts.status;
|
|
12902
|
+
const result = listWorkflows(svc.db, filters);
|
|
12903
|
+
if (!result.ok) {
|
|
12904
|
+
process.stderr.write(formatError2(result.error, !!opts.json) + "\n");
|
|
12905
|
+
process.exitCode = 1;
|
|
12906
|
+
return;
|
|
12907
|
+
}
|
|
12908
|
+
const workflows = result.data;
|
|
12909
|
+
if (opts.json) {
|
|
12910
|
+
process.stdout.write(JSON.stringify(workflows, null, 2) + "\n");
|
|
12911
|
+
} else if (workflows.length === 0) {
|
|
12912
|
+
process.stdout.write("No workflows found.\n");
|
|
12913
|
+
} else {
|
|
12914
|
+
for (const wf of workflows) {
|
|
12915
|
+
process.stdout.write(formatWorkflowLine(wf) + "\n");
|
|
12916
|
+
}
|
|
12917
|
+
}
|
|
12918
|
+
});
|
|
12919
|
+
});
|
|
12920
|
+
cmd.command("show").description("Show workflow definition details").argument("<workflow-id>", "Workflow display ID").option("--json", "Output JSON").action(async (workflowId, opts) => {
|
|
12921
|
+
await withBrain(async (svc) => {
|
|
12922
|
+
const result = getWorkflowDefinition(svc.db, workflowId);
|
|
12923
|
+
if (!result.ok) {
|
|
12924
|
+
process.stderr.write(formatError2(result.error, !!opts.json) + "\n");
|
|
12925
|
+
process.exitCode = 1;
|
|
12926
|
+
return;
|
|
12927
|
+
}
|
|
12928
|
+
const { definition, metadata } = result.data;
|
|
12929
|
+
if (opts.json) {
|
|
12930
|
+
process.stdout.write(JSON.stringify({ metadata, definition }, null, 2) + "\n");
|
|
12931
|
+
} else {
|
|
12932
|
+
process.stdout.write(`${metadata.display_id} - ${metadata.name}
|
|
12933
|
+
`);
|
|
12934
|
+
process.stdout.write(` Version: ${metadata.version}
|
|
12935
|
+
`);
|
|
12936
|
+
process.stdout.write(` Steps: ${metadata.step_count}
|
|
12937
|
+
`);
|
|
12938
|
+
process.stdout.write(` Edges: ${metadata.edge_count}
|
|
12939
|
+
`);
|
|
12940
|
+
if (definition.steps.length > 0) {
|
|
12941
|
+
process.stdout.write("\nSteps:\n");
|
|
12942
|
+
for (const step of definition.steps) {
|
|
12943
|
+
const mode = step.mode ? ` (${step.mode})` : "";
|
|
12944
|
+
process.stdout.write(` ${step.id} - ${step.name}${mode}
|
|
12945
|
+
`);
|
|
12946
|
+
}
|
|
12947
|
+
}
|
|
12948
|
+
}
|
|
12949
|
+
});
|
|
12950
|
+
});
|
|
12951
|
+
cmd.command("status").description("Show workflow instance status").argument("<instance-id>", "Instance display ID").option("--history", "Show execution history").option("--json", "Output JSON").action(async (instanceId, opts) => {
|
|
12952
|
+
await withBrain(async (svc) => {
|
|
12953
|
+
const result = getWorkflowStatus(svc.db, instanceId);
|
|
12954
|
+
if (!result.ok) {
|
|
12955
|
+
process.stderr.write(formatError2(result.error, !!opts.json) + "\n");
|
|
12956
|
+
process.exitCode = 1;
|
|
12957
|
+
return;
|
|
12958
|
+
}
|
|
12959
|
+
const { instance: metadata, steps, progress } = result.data;
|
|
12960
|
+
if (opts.json) {
|
|
12961
|
+
process.stdout.write(JSON.stringify({ metadata, steps, progress }, null, 2) + "\n");
|
|
12962
|
+
} else {
|
|
12963
|
+
process.stdout.write(`Instance: ${instanceId}
|
|
12964
|
+
`);
|
|
12965
|
+
process.stdout.write(
|
|
12966
|
+
` Workflow: ${metadata.workflow_id} v${metadata.workflow_version}
|
|
12967
|
+
`
|
|
12968
|
+
);
|
|
12969
|
+
process.stdout.write(` Status: ${metadata.instance_status}
|
|
12970
|
+
`);
|
|
12971
|
+
process.stdout.write(
|
|
12972
|
+
` Progress: ${progress.done}/${progress.total} done, ${progress.active} active, ${progress.pending} pending, ${progress.pruned} pruned
|
|
12973
|
+
`
|
|
12974
|
+
);
|
|
12975
|
+
if (steps.length > 0) {
|
|
12976
|
+
process.stdout.write("\nSteps:\n");
|
|
12977
|
+
for (const step of steps) {
|
|
12978
|
+
process.stdout.write(` ${step.stepId} (${step.taskDisplayId}) - ${step.status}
|
|
12979
|
+
`);
|
|
12980
|
+
}
|
|
12981
|
+
}
|
|
12982
|
+
if (opts.history) {
|
|
12983
|
+
process.stdout.write("\nHistory not yet available.\n");
|
|
12984
|
+
}
|
|
12985
|
+
}
|
|
12986
|
+
});
|
|
12987
|
+
});
|
|
12988
|
+
cmd.command("dispatch").description("Fill a dispatch template with task and project metadata").argument("<task-id>", "Brain PM task display ID (e.g., SDK-02.03)").argument("<template>", "Template name (e.g., implementation-compact)").option("--branch <name>", "Override auto-generated branch name").option("--worktree <path>", "Set worktree path").option("--dry-run", "Show filled template without claiming the task").option("--claim", "Auto-claim the task before dispatch").option("--json", "Output JSON with template and metadata").action(async (taskId, templateName, opts) => {
|
|
12989
|
+
await withBrain(async (svc) => {
|
|
12990
|
+
const result = await dispatchTemplate(
|
|
12991
|
+
svc.db,
|
|
12992
|
+
svc.config,
|
|
12993
|
+
svc.embedder,
|
|
12994
|
+
taskId.toUpperCase(),
|
|
12995
|
+
templateName,
|
|
12996
|
+
{
|
|
12997
|
+
branch: opts.branch,
|
|
12998
|
+
worktree: opts.worktree,
|
|
12999
|
+
dryRun: opts.dryRun,
|
|
13000
|
+
claim: opts.claim
|
|
13001
|
+
}
|
|
13002
|
+
);
|
|
13003
|
+
if (!result.ok) {
|
|
13004
|
+
process.stderr.write(formatError2(result.error, !!opts.json) + "\n");
|
|
13005
|
+
process.exitCode = 1;
|
|
13006
|
+
return;
|
|
13007
|
+
}
|
|
13008
|
+
if (opts.json) {
|
|
13009
|
+
process.stdout.write(JSON.stringify(result.data, null, 2) + "\n");
|
|
13010
|
+
} else {
|
|
13011
|
+
if (result.data.mode === "assisted") {
|
|
13012
|
+
process.stdout.write("## Assisted Step \u2014 Coordinator Instructions\n\n");
|
|
13013
|
+
process.stdout.write(
|
|
13014
|
+
"This step requires interactive coordination. Use this prompt in your current session.\n"
|
|
13015
|
+
);
|
|
13016
|
+
process.stdout.write("Do NOT dispatch this to an autonomous agent.\n\n");
|
|
13017
|
+
process.stdout.write("---\n\n");
|
|
13018
|
+
}
|
|
13019
|
+
process.stdout.write(result.data.rendered + "\n");
|
|
13020
|
+
}
|
|
13021
|
+
});
|
|
13022
|
+
});
|
|
13023
|
+
cmd.command("gate").description("Set gate status for a workflow step").argument("<instance-id>", "Instance display ID").argument("<step-id>", "Step ID within the workflow").argument("<action>", "Gate action: pass").option("--json", "Output JSON").action(async (instanceId, stepId, action, opts) => {
|
|
13024
|
+
if (action !== "pass") {
|
|
13025
|
+
const msg = `Unknown gate action "${action}". Supported: pass`;
|
|
13026
|
+
process.stderr.write(
|
|
13027
|
+
formatError2({ code: "INVALID_ACTION", message: msg }, !!opts.json) + "\n"
|
|
13028
|
+
);
|
|
13029
|
+
process.exitCode = 1;
|
|
13030
|
+
return;
|
|
13031
|
+
}
|
|
13032
|
+
await withBrain(async (svc) => {
|
|
13033
|
+
const stepsResult = getInstanceStepStates(svc.db, instanceId);
|
|
13034
|
+
if (!stepsResult.ok) {
|
|
13035
|
+
process.stderr.write(formatError2(stepsResult.error, !!opts.json) + "\n");
|
|
13036
|
+
process.exitCode = 1;
|
|
13037
|
+
return;
|
|
13038
|
+
}
|
|
13039
|
+
const step = stepsResult.data.steps.find((s) => s.stepId === stepId);
|
|
13040
|
+
if (!step) {
|
|
13041
|
+
const msg = `Step "${stepId}" not found in instance "${instanceId}"`;
|
|
13042
|
+
process.stderr.write(
|
|
13043
|
+
formatError2({ code: "NOT_FOUND", message: msg }, !!opts.json) + "\n"
|
|
13044
|
+
);
|
|
13045
|
+
process.exitCode = 1;
|
|
13046
|
+
return;
|
|
13047
|
+
}
|
|
13048
|
+
const taskNotes = svc.db.getModuleNoteIds({ module: "pm", type: "task" });
|
|
13049
|
+
const notes = svc.db.getNotesByIds(taskNotes);
|
|
13050
|
+
let updated = false;
|
|
13051
|
+
for (const [, note] of notes) {
|
|
13052
|
+
if (!note.metadata) continue;
|
|
13053
|
+
const meta = JSON.parse(note.metadata);
|
|
13054
|
+
if (meta.display_id !== step.taskDisplayId) continue;
|
|
13055
|
+
const newMeta = { ...meta, gate_status: "passed" };
|
|
13056
|
+
svc.db.upsertNote({ ...note, metadata: JSON.stringify(newMeta) });
|
|
13057
|
+
updated = true;
|
|
13058
|
+
break;
|
|
13059
|
+
}
|
|
13060
|
+
if (!updated) {
|
|
13061
|
+
const msg = `Could not update gate for task "${step.taskDisplayId}"`;
|
|
13062
|
+
process.stderr.write(
|
|
13063
|
+
formatError2({ code: "UPDATE_FAILED", message: msg }, !!opts.json) + "\n"
|
|
13064
|
+
);
|
|
13065
|
+
process.exitCode = 1;
|
|
13066
|
+
return;
|
|
13067
|
+
}
|
|
13068
|
+
if (opts.json) {
|
|
13069
|
+
process.stdout.write(
|
|
13070
|
+
JSON.stringify(
|
|
13071
|
+
{ instance_id: instanceId, step_id: stepId, gate_status: "passed" },
|
|
13072
|
+
null,
|
|
13073
|
+
2
|
|
13074
|
+
) + "\n"
|
|
13075
|
+
);
|
|
13076
|
+
} else {
|
|
13077
|
+
process.stdout.write(`Gate passed for step "${stepId}" (${step.taskDisplayId})
|
|
13078
|
+
`);
|
|
13079
|
+
}
|
|
13080
|
+
});
|
|
13081
|
+
});
|
|
13082
|
+
return cmd;
|
|
13083
|
+
}
|
|
13084
|
+
|
|
13085
|
+
// src/modules/workflow/commands/resource.ts
|
|
13086
|
+
import { Command as Command45 } from "@commander-js/extra-typings";
|
|
13087
|
+
|
|
13088
|
+
// src/modules/workflow/engine/fast-path.ts
|
|
13089
|
+
import {
|
|
13090
|
+
existsSync as existsSync24,
|
|
13091
|
+
mkdirSync as mkdirSync17,
|
|
13092
|
+
readFileSync as readFileSync22,
|
|
13093
|
+
readdirSync as readdirSync6,
|
|
13094
|
+
renameSync as renameSync2,
|
|
13095
|
+
unlinkSync as unlinkSync7,
|
|
13096
|
+
writeFileSync as writeFileSync17
|
|
13097
|
+
} from "fs";
|
|
13098
|
+
import { createHash as createHash14, randomUUID as randomUUID8 } from "crypto";
|
|
13099
|
+
import { join as join22 } from "path";
|
|
13100
|
+
function sidecarDir(config) {
|
|
13101
|
+
return join22(config.notesDir, "modules", "workflow", "resources");
|
|
13102
|
+
}
|
|
13103
|
+
function sidecarPath(config, displayId) {
|
|
13104
|
+
return join22(sidecarDir(config), `${displayId}.json`);
|
|
13105
|
+
}
|
|
13106
|
+
function toFastPathResource(resource) {
|
|
13107
|
+
return {
|
|
13108
|
+
id: resource.display_id,
|
|
13109
|
+
type: resource.resource_type,
|
|
13110
|
+
project: resource.project,
|
|
13111
|
+
status: resource.status,
|
|
13112
|
+
data: resource.data,
|
|
13113
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13114
|
+
};
|
|
13115
|
+
}
|
|
13116
|
+
function buildResourceMarkdown(resource) {
|
|
13117
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
13118
|
+
const id = resource.display_id.toLowerCase() + "-resource";
|
|
13119
|
+
const dataJson = JSON.stringify(resource.data);
|
|
13120
|
+
const lines2 = [
|
|
13121
|
+
"---",
|
|
13122
|
+
`id: ${id}`,
|
|
13123
|
+
`title: "Resource ${resource.display_id}"`,
|
|
13124
|
+
"type: resource",
|
|
13125
|
+
"tier: fast",
|
|
13126
|
+
"module: workflow",
|
|
13127
|
+
`display_id: ${resource.display_id}`,
|
|
13128
|
+
`resource_type: ${resource.resource_type}`,
|
|
13129
|
+
`project: ${resource.project}`,
|
|
13130
|
+
`status: ${resource.status}`,
|
|
13131
|
+
`data: '${dataJson}'`,
|
|
13132
|
+
`created: ${now}`,
|
|
13133
|
+
`modified: ${now}`,
|
|
13134
|
+
"---",
|
|
13135
|
+
"",
|
|
13136
|
+
`# Resource ${resource.display_id}`,
|
|
13137
|
+
"",
|
|
13138
|
+
`Type: ${resource.resource_type}`,
|
|
13139
|
+
`Project: ${resource.project}`,
|
|
13140
|
+
""
|
|
13141
|
+
];
|
|
13142
|
+
return lines2.join("\n");
|
|
13143
|
+
}
|
|
13144
|
+
function writeSidecarAtomic(config, resource) {
|
|
13145
|
+
const dir = sidecarDir(config);
|
|
13146
|
+
if (!existsSync24(dir)) {
|
|
13147
|
+
mkdirSync17(dir, { recursive: true });
|
|
13148
|
+
}
|
|
13149
|
+
const tmpPath = join22(dir, `.${resource.id}-${randomUUID8().slice(0, 8)}.tmp`);
|
|
13150
|
+
const finalPath = sidecarPath(config, resource.id);
|
|
13151
|
+
writeFileSync17(tmpPath, JSON.stringify(resource, null, 2), "utf-8");
|
|
13152
|
+
renameSync2(tmpPath, finalPath);
|
|
13153
|
+
}
|
|
13154
|
+
async function writeResource(db, config, embedder, resource) {
|
|
13155
|
+
const fastPathData = toFastPathResource(resource);
|
|
13156
|
+
try {
|
|
13157
|
+
writeSidecarAtomic(config, fastPathData);
|
|
13158
|
+
} catch {
|
|
13159
|
+
return fail("SIDECAR_WRITE_FAILED", `Failed to write sidecar for ${resource.display_id}`);
|
|
13160
|
+
}
|
|
13161
|
+
try {
|
|
13162
|
+
const markdown = buildResourceMarkdown(resource);
|
|
13163
|
+
const noteDir = join22(config.notesDir, "modules", "workflow");
|
|
13164
|
+
if (!existsSync24(noteDir)) {
|
|
13165
|
+
mkdirSync17(noteDir, { recursive: true });
|
|
13166
|
+
}
|
|
13167
|
+
const filePath = join22(noteDir, `${resource.display_id}.md`);
|
|
13168
|
+
writeFileSync17(filePath, markdown, "utf-8");
|
|
13169
|
+
const hash = createHash14("sha256").update(markdown).digest("hex");
|
|
13170
|
+
await indexSingleFile(db, embedder, filePath, markdown, hash, Date.now());
|
|
13171
|
+
} catch {
|
|
13172
|
+
const sc = sidecarPath(config, resource.display_id);
|
|
13173
|
+
if (existsSync24(sc)) {
|
|
13174
|
+
unlinkSync7(sc);
|
|
13175
|
+
}
|
|
13176
|
+
return fail("DB_WRITE_FAILED", `Failed to index resource note for ${resource.display_id}`);
|
|
13177
|
+
}
|
|
13178
|
+
return ok(fastPathData);
|
|
13179
|
+
}
|
|
13180
|
+
async function updateResourceSidecar(db, config, embedder, resourceId, updates) {
|
|
13181
|
+
const existing = readResourceFastPath(config.notesDir, resourceId);
|
|
13182
|
+
if (!existing) {
|
|
13183
|
+
return fail("NOT_FOUND", `Resource "${resourceId}" not found`);
|
|
13184
|
+
}
|
|
13185
|
+
const updated = {
|
|
13186
|
+
...existing,
|
|
13187
|
+
type: updates.resource_type ?? existing.type,
|
|
13188
|
+
project: updates.project ?? existing.project,
|
|
13189
|
+
status: updates.status ?? existing.status,
|
|
13190
|
+
data: updates.data ?? existing.data,
|
|
13191
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13192
|
+
};
|
|
13193
|
+
try {
|
|
13194
|
+
writeSidecarAtomic(config, updated);
|
|
13195
|
+
} catch {
|
|
13196
|
+
return fail("SIDECAR_WRITE_FAILED", `Failed to update sidecar for ${resourceId}`);
|
|
13197
|
+
}
|
|
13198
|
+
return ok(updated);
|
|
13199
|
+
}
|
|
13200
|
+
function readResourceFastPath(notesDir, displayId) {
|
|
13201
|
+
const filePath = join22(notesDir, "modules", "workflow", "resources", `${displayId}.json`);
|
|
13202
|
+
if (!existsSync24(filePath)) {
|
|
13203
|
+
return null;
|
|
13204
|
+
}
|
|
13205
|
+
try {
|
|
13206
|
+
return JSON.parse(readFileSync22(filePath, "utf-8"));
|
|
13207
|
+
} catch {
|
|
13208
|
+
process.stderr.write(`fast-path: corrupt JSON in ${filePath}
|
|
13209
|
+
`);
|
|
13210
|
+
return null;
|
|
13211
|
+
}
|
|
13212
|
+
}
|
|
13213
|
+
|
|
13214
|
+
// src/modules/workflow/data/resource-ops.ts
|
|
13215
|
+
async function createResource(db, config, embedder, resource) {
|
|
13216
|
+
return writeResource(db, config, embedder, resource);
|
|
13217
|
+
}
|
|
13218
|
+
function listResources(db, filters) {
|
|
13219
|
+
const noteIds = db.getModuleNoteIds({ module: "workflow", type: "resource" });
|
|
13220
|
+
const notes = db.getNotesByIds(noteIds);
|
|
13221
|
+
const resources = [];
|
|
13222
|
+
for (const [, note] of notes) {
|
|
13223
|
+
if (!note.metadata) continue;
|
|
13224
|
+
const meta = JSON.parse(note.metadata);
|
|
13225
|
+
if (filters?.type && meta.resource_type !== filters.type) continue;
|
|
13226
|
+
if (filters?.project && meta.project !== filters.project) continue;
|
|
13227
|
+
if (filters?.status && meta.status !== filters.status) continue;
|
|
13228
|
+
resources.push({
|
|
13229
|
+
id: meta.display_id,
|
|
13230
|
+
type: meta.resource_type,
|
|
13231
|
+
project: meta.project,
|
|
13232
|
+
status: meta.status,
|
|
13233
|
+
data: typeof meta.data === "string" ? JSON.parse(meta.data) : meta.data ?? {},
|
|
13234
|
+
updatedAt: note.modifiedAt ?? note.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
13235
|
+
});
|
|
13236
|
+
}
|
|
13237
|
+
return ok(resources);
|
|
13238
|
+
}
|
|
13239
|
+
function getResource(db, resourceId) {
|
|
13240
|
+
const noteIds = db.getModuleNoteIds({ module: "workflow", type: "resource" });
|
|
13241
|
+
const notes = db.getNotesByIds(noteIds);
|
|
13242
|
+
for (const [, note] of notes) {
|
|
13243
|
+
if (!note.metadata) continue;
|
|
13244
|
+
const meta = JSON.parse(note.metadata);
|
|
13245
|
+
if (meta.display_id !== resourceId) continue;
|
|
13246
|
+
return ok({
|
|
13247
|
+
id: meta.display_id,
|
|
13248
|
+
type: meta.resource_type,
|
|
13249
|
+
project: meta.project,
|
|
13250
|
+
status: meta.status,
|
|
13251
|
+
data: typeof meta.data === "string" ? JSON.parse(meta.data) : meta.data ?? {},
|
|
13252
|
+
updatedAt: note.modifiedAt ?? note.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
13253
|
+
});
|
|
13254
|
+
}
|
|
13255
|
+
return fail("NOT_FOUND", `Resource "${resourceId}" not found`);
|
|
13256
|
+
}
|
|
13257
|
+
async function releaseResource(db, config, embedder, resourceId) {
|
|
13258
|
+
return updateResourceSidecar(db, config, embedder, resourceId, { status: "released" });
|
|
13259
|
+
}
|
|
13260
|
+
|
|
13261
|
+
// src/modules/workflow/commands/resource.ts
|
|
13262
|
+
function parseDataPairs(raw) {
|
|
13263
|
+
const result = {};
|
|
13264
|
+
for (const pair of raw.split(",")) {
|
|
13265
|
+
const eqIdx = pair.indexOf("=");
|
|
13266
|
+
if (eqIdx === -1) continue;
|
|
13267
|
+
const key = pair.slice(0, eqIdx).trim();
|
|
13268
|
+
const val = pair.slice(eqIdx + 1).trim();
|
|
13269
|
+
result[key] = val;
|
|
13270
|
+
}
|
|
13271
|
+
return result;
|
|
13272
|
+
}
|
|
13273
|
+
function createResourceCommand() {
|
|
13274
|
+
const cmd = new Command45("resource").description("Workflow resource management");
|
|
13275
|
+
cmd.command("add").description("Add a resource to a workflow").requiredOption("--type <type>", "Resource type (worktree|branch|ownership|wip-slot)").requiredOption("--project <prefix>", "Project prefix").option("--data <pairs>", "Comma-separated key=val data pairs").option("--id <id>", "Custom display ID").option("--json", "Output JSON").action(async (opts) => {
|
|
13276
|
+
await withBrain(async (svc) => {
|
|
13277
|
+
const displayId = opts.id ?? `RES-${Date.now().toString(36).toUpperCase()}`;
|
|
13278
|
+
const resource = {
|
|
13279
|
+
display_id: displayId,
|
|
13280
|
+
resource_type: opts.type,
|
|
13281
|
+
project: opts.project,
|
|
13282
|
+
status: "active",
|
|
13283
|
+
data: opts.data ? parseDataPairs(opts.data) : {}
|
|
13284
|
+
};
|
|
13285
|
+
const result = await createResource(svc.db, svc.config, svc.embedder, resource);
|
|
13286
|
+
if (!result.ok) {
|
|
13287
|
+
process.stderr.write(`Error: ${result.error.message}
|
|
13288
|
+
`);
|
|
13289
|
+
process.exitCode = 1;
|
|
13290
|
+
return;
|
|
13291
|
+
}
|
|
13292
|
+
if (opts.json) {
|
|
13293
|
+
process.stdout.write(JSON.stringify(result.data, null, 2) + "\n");
|
|
13294
|
+
} else {
|
|
13295
|
+
process.stdout.write(`Created resource ${result.data.id} (${result.data.type})
|
|
13296
|
+
`);
|
|
13297
|
+
}
|
|
13298
|
+
});
|
|
13299
|
+
});
|
|
13300
|
+
cmd.command("list").description("List workflow resources").option("--type <type>", "Filter by resource type").option("--project <prefix>", "Filter by project").option("--json", "Output JSON").action(async (opts) => {
|
|
13301
|
+
await withBrain(async (svc) => {
|
|
13302
|
+
const result = listResources(svc.db, {
|
|
13303
|
+
type: opts.type,
|
|
13304
|
+
project: opts.project
|
|
13305
|
+
});
|
|
13306
|
+
if (!result.ok) {
|
|
13307
|
+
process.stderr.write(`Error: ${result.error.message}
|
|
13308
|
+
`);
|
|
13309
|
+
process.exitCode = 1;
|
|
13310
|
+
return;
|
|
13311
|
+
}
|
|
13312
|
+
if (opts.json) {
|
|
13313
|
+
process.stdout.write(JSON.stringify(result.data, null, 2) + "\n");
|
|
13314
|
+
} else if (result.data.length === 0) {
|
|
13315
|
+
process.stdout.write("No resources found.\n");
|
|
13316
|
+
} else {
|
|
13317
|
+
for (const r of result.data) {
|
|
13318
|
+
process.stdout.write(`${r.id} ${r.type} ${r.project} ${r.status}
|
|
13319
|
+
`);
|
|
13320
|
+
}
|
|
13321
|
+
}
|
|
13322
|
+
});
|
|
13323
|
+
});
|
|
13324
|
+
cmd.command("show").description("Show resource details").argument("<id>", "Resource display ID").option("--json", "Output JSON").action(async (id, opts) => {
|
|
13325
|
+
await withBrain(async (svc) => {
|
|
13326
|
+
const result = getResource(svc.db, id);
|
|
13327
|
+
if (!result.ok) {
|
|
13328
|
+
process.stderr.write(`Error: ${result.error.message}
|
|
13329
|
+
`);
|
|
13330
|
+
process.exitCode = 1;
|
|
13331
|
+
return;
|
|
13332
|
+
}
|
|
13333
|
+
if (opts.json) {
|
|
13334
|
+
process.stdout.write(JSON.stringify(result.data, null, 2) + "\n");
|
|
13335
|
+
} else {
|
|
13336
|
+
const r = result.data;
|
|
13337
|
+
process.stdout.write(`ID: ${r.id}
|
|
13338
|
+
`);
|
|
13339
|
+
process.stdout.write(`Type: ${r.type}
|
|
13340
|
+
`);
|
|
13341
|
+
process.stdout.write(`Project: ${r.project}
|
|
13342
|
+
`);
|
|
13343
|
+
process.stdout.write(`Status: ${r.status}
|
|
13344
|
+
`);
|
|
13345
|
+
process.stdout.write(`Data: ${JSON.stringify(r.data)}
|
|
13346
|
+
`);
|
|
13347
|
+
process.stdout.write(`Updated: ${r.updatedAt}
|
|
13348
|
+
`);
|
|
13349
|
+
}
|
|
13350
|
+
});
|
|
13351
|
+
});
|
|
13352
|
+
cmd.command("release").description("Release a workflow resource").argument("<id>", "Resource display ID").option("--json", "Output JSON").action(async (id, opts) => {
|
|
13353
|
+
await withBrain(async (svc) => {
|
|
13354
|
+
const result = await releaseResource(svc.db, svc.config, svc.embedder, id);
|
|
13355
|
+
if (!result.ok) {
|
|
13356
|
+
process.stderr.write(`Error: ${result.error.message}
|
|
13357
|
+
`);
|
|
13358
|
+
process.exitCode = 1;
|
|
13359
|
+
return;
|
|
13360
|
+
}
|
|
13361
|
+
if (opts.json) {
|
|
13362
|
+
process.stdout.write(JSON.stringify(result.data, null, 2) + "\n");
|
|
13363
|
+
} else {
|
|
13364
|
+
process.stdout.write(`Released resource ${result.data.id}
|
|
13365
|
+
`);
|
|
13366
|
+
}
|
|
13367
|
+
});
|
|
13368
|
+
});
|
|
13369
|
+
return cmd;
|
|
13370
|
+
}
|
|
13371
|
+
|
|
13372
|
+
// src/modules/workflow/commands/lifecycle.ts
|
|
13373
|
+
import { Command as Command46 } from "@commander-js/extra-typings";
|
|
13374
|
+
|
|
13375
|
+
// src/modules/workflow/engine/gates.ts
|
|
13376
|
+
import { existsSync as existsSync25 } from "fs";
|
|
13377
|
+
import { execSync as execSync4 } from "child_process";
|
|
13378
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
13379
|
+
async function evaluateTaskComplete(gate, db) {
|
|
13380
|
+
const noteIds = db.getModuleNoteIds({ module: "pm", type: "task" });
|
|
13381
|
+
if (noteIds.length === 0) return false;
|
|
13382
|
+
const notes = db.getNotesByIds(noteIds);
|
|
13383
|
+
for (const [, note] of notes) {
|
|
13384
|
+
if (!note.metadata) continue;
|
|
13385
|
+
const meta = JSON.parse(note.metadata);
|
|
13386
|
+
if (meta.display_id === gate.target && meta.status === "done") {
|
|
13387
|
+
return true;
|
|
13388
|
+
}
|
|
13389
|
+
}
|
|
13390
|
+
return false;
|
|
13391
|
+
}
|
|
13392
|
+
function evaluateCliPass(gate) {
|
|
13393
|
+
const command = gate.command ?? gate.target ?? "true";
|
|
13394
|
+
const timeout = gate.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
13395
|
+
try {
|
|
13396
|
+
execSync4(command, { timeout, stdio: "ignore" });
|
|
13397
|
+
return true;
|
|
13398
|
+
} catch {
|
|
13399
|
+
return false;
|
|
13400
|
+
}
|
|
13401
|
+
}
|
|
13402
|
+
async function evaluateGate(gate, db, _config, taskMetadata) {
|
|
13403
|
+
switch (gate.type) {
|
|
13404
|
+
case "task-complete": {
|
|
13405
|
+
const passed = await evaluateTaskComplete(gate, db);
|
|
13406
|
+
return { gate, passed, reason: passed ? void 0 : `Task "${gate.target}" not done` };
|
|
13407
|
+
}
|
|
13408
|
+
case "file-exists": {
|
|
13409
|
+
const passed = gate.target ? existsSync25(gate.target) : false;
|
|
13410
|
+
return { gate, passed, reason: passed ? void 0 : `File "${gate.target}" not found` };
|
|
13411
|
+
}
|
|
13412
|
+
case "cli-pass": {
|
|
13413
|
+
const passed = evaluateCliPass(gate);
|
|
13414
|
+
return { gate, passed, reason: passed ? void 0 : `Command failed` };
|
|
13415
|
+
}
|
|
13416
|
+
case "human-approval":
|
|
13417
|
+
return { gate, passed: false, reason: "Human approval required" };
|
|
13418
|
+
case "approval": {
|
|
13419
|
+
const passed = taskMetadata?.gate_status === "passed";
|
|
13420
|
+
return {
|
|
13421
|
+
gate,
|
|
13422
|
+
passed,
|
|
13423
|
+
reason: passed ? void 0 : "Approval gate not yet passed"
|
|
13424
|
+
};
|
|
13425
|
+
}
|
|
13426
|
+
case "iteration": {
|
|
13427
|
+
const max = gate.maxIterations ?? Infinity;
|
|
13428
|
+
const count = taskMetadata?.iteration_count ?? 0;
|
|
13429
|
+
const passed = count < max;
|
|
13430
|
+
return {
|
|
13431
|
+
gate,
|
|
13432
|
+
passed,
|
|
13433
|
+
reason: passed ? void 0 : `Iteration limit reached (${count}/${max})`
|
|
13434
|
+
};
|
|
13435
|
+
}
|
|
13436
|
+
case "custom": {
|
|
13437
|
+
const passed = evaluateCliPass(gate);
|
|
13438
|
+
return { gate, passed, reason: passed ? void 0 : `Custom command failed` };
|
|
13439
|
+
}
|
|
13440
|
+
}
|
|
13441
|
+
}
|
|
13442
|
+
async function evaluateGates(gates, db, config, taskMetadata) {
|
|
13443
|
+
if (gates.length === 0) {
|
|
13444
|
+
return { allPassed: true, results: [] };
|
|
13445
|
+
}
|
|
13446
|
+
const results = [];
|
|
13447
|
+
for (const gate of gates) {
|
|
13448
|
+
const result = await evaluateGate(gate, db, config, taskMetadata);
|
|
13449
|
+
results.push(result);
|
|
13450
|
+
}
|
|
13451
|
+
const allPassed = results.every((r) => r.passed);
|
|
13452
|
+
return { allPassed, results };
|
|
13453
|
+
}
|
|
13454
|
+
|
|
13455
|
+
// src/modules/workflow/engine/lifecycle.ts
|
|
13456
|
+
async function advanceWorkflow(db, config, instanceDisplayId) {
|
|
13457
|
+
const instanceResult = getInstanceByDisplayId(db, instanceDisplayId);
|
|
13458
|
+
if (!instanceResult.ok) return instanceResult;
|
|
13459
|
+
const { note: instanceNote } = instanceResult.data;
|
|
13460
|
+
const stepsResult = getInstanceStepStates(db, instanceDisplayId);
|
|
13461
|
+
if (!stepsResult.ok) return stepsResult;
|
|
13462
|
+
const { steps } = stepsResult.data;
|
|
13463
|
+
if (steps.length === 0) {
|
|
13464
|
+
return ok({ advanced: [], pruned: [], warnings: [], completed: true });
|
|
13465
|
+
}
|
|
13466
|
+
const definition = resolveDefinition(db, instanceNote);
|
|
13467
|
+
if (!definition) {
|
|
13468
|
+
return fail("DEFINITION_NOT_FOUND", "Could not resolve workflow definition");
|
|
13469
|
+
}
|
|
13470
|
+
const statusMap = new Map(steps.map((s) => [s.stepId, s.status]));
|
|
13471
|
+
const taskDisplayIdMap = new Map(steps.map((s) => [s.stepId, s.taskDisplayId]));
|
|
13472
|
+
const allStepIds = steps.map((s) => s.stepId);
|
|
13473
|
+
const readySteps = buildReadySet(allStepIds, steps, definition);
|
|
13474
|
+
const advanced = await findAdvancedSteps(
|
|
13475
|
+
allStepIds,
|
|
13476
|
+
statusMap,
|
|
13477
|
+
taskDisplayIdMap,
|
|
13478
|
+
definition,
|
|
13479
|
+
readySteps,
|
|
13480
|
+
db,
|
|
13481
|
+
config
|
|
13482
|
+
);
|
|
13483
|
+
const { pruned, warnings } = pruneUnreachableSteps(
|
|
13484
|
+
db,
|
|
13485
|
+
allStepIds,
|
|
13486
|
+
statusMap,
|
|
13487
|
+
taskDisplayIdMap,
|
|
13488
|
+
definition,
|
|
13489
|
+
readySteps,
|
|
13490
|
+
advanced
|
|
13491
|
+
);
|
|
13492
|
+
const prunedSet = new Set(pruned);
|
|
13493
|
+
const completed = allStepIds.every((stepId) => {
|
|
13494
|
+
const status = statusMap.get(stepId);
|
|
13495
|
+
if (status === "pruned" || status === "cancelled" || prunedSet.has(stepId)) return true;
|
|
13496
|
+
return readySteps.has(stepId);
|
|
13497
|
+
});
|
|
13498
|
+
if (completed) {
|
|
13499
|
+
updateInstanceStatus(db, instanceNote.id, "completed");
|
|
13500
|
+
}
|
|
13501
|
+
return ok({ advanced, pruned, warnings, completed });
|
|
13502
|
+
}
|
|
13503
|
+
function buildReadySet(allStepIds, steps, definition) {
|
|
13504
|
+
const ready = /* @__PURE__ */ new Set();
|
|
13505
|
+
for (const s of steps) {
|
|
13506
|
+
if (s.status === "done") {
|
|
13507
|
+
ready.add(s.stepId);
|
|
13508
|
+
}
|
|
13509
|
+
}
|
|
13510
|
+
const unconditionalEdges = definition.edges.filter((e) => !e.condition);
|
|
13511
|
+
for (const stepId of allStepIds) {
|
|
13512
|
+
const preds = getPredecessors(stepId, unconditionalEdges);
|
|
13513
|
+
if (preds.length === 0) {
|
|
13514
|
+
ready.add(stepId);
|
|
13515
|
+
}
|
|
13516
|
+
}
|
|
13517
|
+
return ready;
|
|
13518
|
+
}
|
|
13519
|
+
async function findAdvancedSteps(allStepIds, statusMap, taskDisplayIdMap, definition, readySteps, db, config) {
|
|
13520
|
+
const advanced = [];
|
|
13521
|
+
const unconditional = definition.edges.filter((e) => !e.condition);
|
|
13522
|
+
for (const stepId of allStepIds) {
|
|
13523
|
+
const status = statusMap.get(stepId);
|
|
13524
|
+
if (status === "done" || status === "pruned" || status === "cancelled") continue;
|
|
13525
|
+
const preds = getPredecessors(stepId, unconditional);
|
|
13526
|
+
if (preds.length === 0) continue;
|
|
13527
|
+
const allPredsReady = preds.every((p) => readySteps.has(p));
|
|
13528
|
+
if (!allPredsReady) continue;
|
|
13529
|
+
const stepDef = definition.steps.find((s) => s.id === stepId);
|
|
13530
|
+
const gates = stepDef?.gates ?? [];
|
|
13531
|
+
if (gates.length > 0) {
|
|
13532
|
+
const taskMeta = lookupTaskMetadata(db, taskDisplayIdMap.get(stepId));
|
|
13533
|
+
const gateResult = await evaluateGates(gates, db, config, taskMeta);
|
|
13534
|
+
if (!gateResult.allPassed) continue;
|
|
13535
|
+
}
|
|
13536
|
+
advanced.push(stepId);
|
|
13537
|
+
}
|
|
13538
|
+
return advanced;
|
|
13539
|
+
}
|
|
13540
|
+
function pruneUnreachableSteps(db, allStepIds, statusMap, taskDisplayIdMap, definition, readySteps, advanced) {
|
|
13541
|
+
const pruned = [];
|
|
13542
|
+
const warnings = [];
|
|
13543
|
+
const unreachable = getUnreachableSteps([...readySteps], definition.edges, allStepIds);
|
|
13544
|
+
const conditionalTargets = new Set(definition.edges.filter((e) => e.condition).map((e) => e.to));
|
|
13545
|
+
for (const stepId of allStepIds) {
|
|
13546
|
+
if (unreachable.includes(stepId)) continue;
|
|
13547
|
+
if (!conditionalTargets.has(stepId)) continue;
|
|
13548
|
+
const status = statusMap.get(stepId);
|
|
13549
|
+
if (status === "done" || status === "pruned" || status === "cancelled") continue;
|
|
13550
|
+
const incoming = definition.edges.filter((e) => e.to === stepId);
|
|
13551
|
+
const allConditional = incoming.every((e) => e.condition);
|
|
13552
|
+
if (!allConditional) continue;
|
|
13553
|
+
const allSourcesReady = incoming.every((e) => readySteps.has(e.from));
|
|
13554
|
+
if (allSourcesReady && !advanced.includes(stepId)) {
|
|
13555
|
+
unreachable.push(stepId);
|
|
13556
|
+
}
|
|
13557
|
+
}
|
|
13558
|
+
for (const stepId of unreachable) {
|
|
13559
|
+
const status = statusMap.get(stepId);
|
|
13560
|
+
const displayId = taskDisplayIdMap.get(stepId);
|
|
13561
|
+
if (status === "pending" || status === "blocked") {
|
|
13562
|
+
pruneTask(db, displayId);
|
|
13563
|
+
pruned.push(stepId);
|
|
13564
|
+
} else if (status === "claimed" || status === "in-progress") {
|
|
13565
|
+
warnings.push(`Step "${stepId}" (${displayId}) should be pruned but is ${status}`);
|
|
13566
|
+
}
|
|
13567
|
+
}
|
|
13568
|
+
return { pruned, warnings };
|
|
13569
|
+
}
|
|
13570
|
+
function lookupTaskMetadata(db, taskDisplayId) {
|
|
13571
|
+
if (!taskDisplayId) return void 0;
|
|
13572
|
+
const noteIds = db.getModuleNoteIds({ module: "pm", type: "task" });
|
|
13573
|
+
const notes = db.getNotesByIds(noteIds);
|
|
13574
|
+
for (const [, note] of notes) {
|
|
13575
|
+
if (!note.metadata) continue;
|
|
13576
|
+
const meta = JSON.parse(note.metadata);
|
|
13577
|
+
if (meta.display_id === taskDisplayId) return meta;
|
|
13578
|
+
}
|
|
13579
|
+
return void 0;
|
|
13580
|
+
}
|
|
13581
|
+
function resolveDefinition(db, instanceNote) {
|
|
13582
|
+
const relations = db.getRelationsFrom(instanceNote.id);
|
|
13583
|
+
const instanceOfRel = relations.find((r) => r.type === "instance-of");
|
|
13584
|
+
if (!instanceOfRel) return null;
|
|
13585
|
+
const defNote = db.getNoteById(instanceOfRel.targetId);
|
|
13586
|
+
if (!defNote) return null;
|
|
13587
|
+
const chunk = db.getFirstChunkForNote(defNote.id);
|
|
13588
|
+
if (!chunk) return null;
|
|
13589
|
+
try {
|
|
13590
|
+
const jsonMatch = chunk.content.match(/({[\s\S]*})/);
|
|
13591
|
+
return JSON.parse(jsonMatch ? jsonMatch[1] : chunk.content);
|
|
13592
|
+
} catch {
|
|
13593
|
+
return null;
|
|
13594
|
+
}
|
|
13595
|
+
}
|
|
13596
|
+
function pruneTask(db, targetDisplayId) {
|
|
13597
|
+
const noteIds = db.getModuleNoteIds({ module: "pm", type: "task" });
|
|
13598
|
+
const notes = db.getNotesByIds(noteIds);
|
|
13599
|
+
for (const [, note] of notes) {
|
|
13600
|
+
if (!note.metadata) continue;
|
|
13601
|
+
const meta = JSON.parse(note.metadata);
|
|
13602
|
+
if (meta.display_id === targetDisplayId) {
|
|
13603
|
+
const updated = { ...meta, status: "pruned" };
|
|
13604
|
+
db.upsertNote({ ...note, metadata: JSON.stringify(updated) });
|
|
13605
|
+
break;
|
|
13606
|
+
}
|
|
13607
|
+
}
|
|
13608
|
+
}
|
|
13609
|
+
function updateInstanceStatus(db, noteId, status) {
|
|
13610
|
+
const note = db.getNoteById(noteId);
|
|
13611
|
+
if (!note?.metadata) return;
|
|
13612
|
+
const meta = JSON.parse(note.metadata);
|
|
13613
|
+
const updated = { ...meta, instance_status: status };
|
|
13614
|
+
db.upsertNote({ ...note, metadata: JSON.stringify(updated) });
|
|
13615
|
+
}
|
|
13616
|
+
|
|
13617
|
+
// src/modules/workflow/commands/lifecycle.ts
|
|
13618
|
+
function formatError3(error, json) {
|
|
13619
|
+
if (json) {
|
|
13620
|
+
return JSON.stringify({ error: true, code: error.code, message: error.message });
|
|
13621
|
+
}
|
|
13622
|
+
return `Error [${error.code}]: ${error.message}`;
|
|
13623
|
+
}
|
|
13624
|
+
function parseContextPairs(pairs) {
|
|
13625
|
+
const context = {};
|
|
13626
|
+
for (const pair of pairs) {
|
|
13627
|
+
const eqIndex = pair.indexOf("=");
|
|
13628
|
+
if (eqIndex === -1) {
|
|
13629
|
+
throw new Error(`Invalid context pair: "${pair}" (expected key=value)`);
|
|
13630
|
+
}
|
|
13631
|
+
context[pair.slice(0, eqIndex)] = pair.slice(eqIndex + 1);
|
|
13632
|
+
}
|
|
13633
|
+
return context;
|
|
13634
|
+
}
|
|
13635
|
+
function createLifecycleCommands() {
|
|
13636
|
+
const add = new Command46("add").description("Add a workflow instance").argument("<workflow-id>", "Workflow definition ID to instantiate").requiredOption("--project <prefix>", "Project prefix for the instance").option("--context <key=value...>", "Context key=value pairs", []).option("--json", "Output JSON").action(async (workflowId, opts) => {
|
|
13637
|
+
let context;
|
|
13638
|
+
try {
|
|
13639
|
+
context = parseContextPairs(opts.context);
|
|
13640
|
+
} catch (e) {
|
|
13641
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
13642
|
+
if (opts.json) {
|
|
13643
|
+
process.stderr.write(
|
|
13644
|
+
JSON.stringify({ error: true, code: "INVALID_CONTEXT", message: msg }) + "\n"
|
|
13645
|
+
);
|
|
13646
|
+
} else {
|
|
13647
|
+
process.stderr.write(`Error [INVALID_CONTEXT]: ${msg}
|
|
13648
|
+
`);
|
|
13649
|
+
}
|
|
13650
|
+
process.exitCode = 1;
|
|
13651
|
+
return;
|
|
13652
|
+
}
|
|
13653
|
+
await withBrain(async (svc) => {
|
|
13654
|
+
const result = await instantiateWorkflow(
|
|
13655
|
+
svc.db,
|
|
13656
|
+
svc.config,
|
|
13657
|
+
svc.embedder,
|
|
13658
|
+
workflowId,
|
|
13659
|
+
opts.project,
|
|
13660
|
+
context
|
|
13661
|
+
);
|
|
13662
|
+
if (!result.ok) {
|
|
13663
|
+
process.stderr.write(formatError3(result.error, !!opts.json) + "\n");
|
|
13664
|
+
process.exitCode = 1;
|
|
13665
|
+
return;
|
|
13666
|
+
}
|
|
13667
|
+
const data = result.data;
|
|
13668
|
+
if (opts.json) {
|
|
13669
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
13670
|
+
} else {
|
|
13671
|
+
process.stdout.write(`Created workflow instance ${data.display_id}
|
|
13672
|
+
`);
|
|
13673
|
+
process.stdout.write(` Workflow: ${data.workflow_id} v${data.workflow_version}
|
|
13674
|
+
`);
|
|
13675
|
+
process.stdout.write(` Status: ${data.instance_status}
|
|
13676
|
+
`);
|
|
13677
|
+
}
|
|
13678
|
+
});
|
|
13679
|
+
});
|
|
13680
|
+
const expand = new Command46("expand").description("Expand a workflow instance into tasks").argument("<instance-id>", "Instance display ID to expand").option("--json", "Output JSON").action(async (instanceId, opts) => {
|
|
13681
|
+
await withBrain(async (svc) => {
|
|
13682
|
+
const result = await expandWorkflow(svc.db, svc.config, svc.embedder, instanceId);
|
|
13683
|
+
if (!result.ok) {
|
|
13684
|
+
process.stderr.write(formatError3(result.error, !!opts.json) + "\n");
|
|
13685
|
+
process.exitCode = 1;
|
|
13686
|
+
return;
|
|
13687
|
+
}
|
|
13688
|
+
const data = result.data;
|
|
13689
|
+
if (opts.json) {
|
|
13690
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
13691
|
+
} else {
|
|
13692
|
+
process.stdout.write(
|
|
13693
|
+
`Expanded: ${data.tasksCreated} tasks created, ${data.edges} edges
|
|
13694
|
+
`
|
|
13695
|
+
);
|
|
13696
|
+
}
|
|
13697
|
+
});
|
|
13698
|
+
});
|
|
13699
|
+
const advance = new Command46("advance").description("Advance a workflow to the next step").argument("<instance-id>", "Instance display ID to advance").option("--json", "Output JSON").action(async (instanceId, opts) => {
|
|
13700
|
+
await withBrain(async (svc) => {
|
|
13701
|
+
const result = await advanceWorkflow(svc.db, svc.config, instanceId);
|
|
13702
|
+
if (!result.ok) {
|
|
13703
|
+
process.stderr.write(formatError3(result.error, !!opts.json) + "\n");
|
|
13704
|
+
process.exitCode = 1;
|
|
13705
|
+
return;
|
|
13706
|
+
}
|
|
13707
|
+
const data = result.data;
|
|
13708
|
+
if (opts.json) {
|
|
13709
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
13710
|
+
} else {
|
|
13711
|
+
const advancedStr = data.advanced.length > 0 ? data.advanced.join(", ") : "(none)";
|
|
13712
|
+
const prunedStr = data.pruned.length > 0 ? data.pruned.join(", ") : "(none)";
|
|
13713
|
+
process.stdout.write(`Advanced: ${advancedStr}
|
|
13714
|
+
`);
|
|
13715
|
+
process.stdout.write(`Pruned: ${prunedStr}
|
|
13716
|
+
`);
|
|
13717
|
+
if (data.warnings.length > 0) {
|
|
13718
|
+
process.stdout.write(`Warnings: ${data.warnings.join("; ")}
|
|
13719
|
+
`);
|
|
13720
|
+
}
|
|
13721
|
+
process.stdout.write(`Completed: ${data.completed}
|
|
13722
|
+
`);
|
|
13723
|
+
}
|
|
13724
|
+
});
|
|
13725
|
+
});
|
|
13726
|
+
const next = new Command46("next").description("Advance a workflow and dispatch the next step(s)").argument("<instance-id>", "Instance display ID to advance").option("--json", "Output JSON").action(async (instanceId, opts) => {
|
|
13727
|
+
await withBrain(async (svc) => {
|
|
13728
|
+
const advResult = await advanceWorkflow(svc.db, svc.config, instanceId);
|
|
13729
|
+
if (!advResult.ok) {
|
|
13730
|
+
process.stderr.write(formatError3(advResult.error, !!opts.json) + "\n");
|
|
13731
|
+
process.exitCode = 1;
|
|
13732
|
+
return;
|
|
13733
|
+
}
|
|
13734
|
+
const { advanced, pruned, warnings, completed } = advResult.data;
|
|
13735
|
+
const stepsResult = getInstanceStepStates(svc.db, instanceId);
|
|
13736
|
+
const stepToTask = /* @__PURE__ */ new Map();
|
|
13737
|
+
if (stepsResult.ok) {
|
|
13738
|
+
for (const step of stepsResult.data.steps) {
|
|
13739
|
+
stepToTask.set(step.stepId, step.taskDisplayId);
|
|
13740
|
+
}
|
|
13741
|
+
}
|
|
13742
|
+
const instanceResult = getInstanceByDisplayId(svc.db, instanceId);
|
|
13743
|
+
const stepDefs = /* @__PURE__ */ new Map();
|
|
13744
|
+
if (instanceResult.ok) {
|
|
13745
|
+
const workflowId = instanceResult.data.metadata.workflow_id;
|
|
13746
|
+
const defResult = getWorkflowDefinition(svc.db, workflowId);
|
|
13747
|
+
if (defResult.ok) {
|
|
13748
|
+
for (const step of defResult.data.definition.steps) {
|
|
13749
|
+
stepDefs.set(step.id, { template: step.template, mode: step.mode });
|
|
13750
|
+
}
|
|
13751
|
+
}
|
|
13752
|
+
}
|
|
13753
|
+
const dispatched = [];
|
|
13754
|
+
const dispatchErrors = [];
|
|
13755
|
+
for (const stepId of advanced) {
|
|
13756
|
+
const taskId = stepToTask.get(stepId);
|
|
13757
|
+
if (!taskId) continue;
|
|
13758
|
+
const stepDef = stepDefs.get(stepId);
|
|
13759
|
+
const templateName = stepDef?.template;
|
|
13760
|
+
if (!templateName) continue;
|
|
13761
|
+
const isAssisted = stepDef?.mode === "assisted";
|
|
13762
|
+
const dispatchResult = await dispatchTemplate(
|
|
13763
|
+
svc.db,
|
|
13764
|
+
svc.config,
|
|
13765
|
+
svc.embedder,
|
|
13766
|
+
taskId,
|
|
13767
|
+
templateName,
|
|
13768
|
+
{ claim: !isAssisted }
|
|
13769
|
+
);
|
|
13770
|
+
if (!dispatchResult.ok) {
|
|
13771
|
+
dispatchErrors.push({
|
|
13772
|
+
stepId,
|
|
13773
|
+
error: dispatchResult.error.message
|
|
13774
|
+
});
|
|
13775
|
+
continue;
|
|
13776
|
+
}
|
|
13777
|
+
dispatched.push({ stepId, taskId, template: templateName });
|
|
13778
|
+
}
|
|
13779
|
+
if (opts.json) {
|
|
13780
|
+
process.stdout.write(
|
|
13781
|
+
JSON.stringify(
|
|
13782
|
+
{
|
|
13783
|
+
advanced,
|
|
13784
|
+
pruned,
|
|
13785
|
+
warnings,
|
|
13786
|
+
completed,
|
|
13787
|
+
dispatched,
|
|
13788
|
+
errors: dispatchErrors
|
|
13789
|
+
},
|
|
13790
|
+
null,
|
|
13791
|
+
2
|
|
13792
|
+
) + "\n"
|
|
13793
|
+
);
|
|
13794
|
+
} else {
|
|
13795
|
+
const advancedStr = advanced.length > 0 ? advanced.join(", ") : "(none)";
|
|
13796
|
+
const prunedStr = pruned.length > 0 ? pruned.join(", ") : "(none)";
|
|
13797
|
+
process.stdout.write(`Advanced: ${advancedStr}
|
|
13798
|
+
`);
|
|
13799
|
+
process.stdout.write(`Pruned: ${prunedStr}
|
|
13800
|
+
`);
|
|
13801
|
+
if (warnings.length > 0) {
|
|
13802
|
+
process.stdout.write(`Warnings: ${warnings.join("; ")}
|
|
13803
|
+
`);
|
|
13804
|
+
}
|
|
13805
|
+
if (completed) {
|
|
13806
|
+
process.stdout.write(`Workflow completed.
|
|
13807
|
+
`);
|
|
13808
|
+
}
|
|
13809
|
+
if (dispatched.length > 0) {
|
|
13810
|
+
process.stdout.write(`
|
|
13811
|
+
Dispatched steps:
|
|
13812
|
+
`);
|
|
13813
|
+
for (const d of dispatched) {
|
|
13814
|
+
process.stdout.write(` ${d.stepId} \u2192 ${d.taskId} (${d.template})
|
|
13815
|
+
`);
|
|
13816
|
+
}
|
|
13817
|
+
}
|
|
13818
|
+
if (dispatchErrors.length > 0) {
|
|
13819
|
+
process.stdout.write(`
|
|
13820
|
+
Dispatch errors:
|
|
13821
|
+
`);
|
|
13822
|
+
for (const e of dispatchErrors) {
|
|
13823
|
+
process.stdout.write(` ${e.stepId}: ${e.error}
|
|
13824
|
+
`);
|
|
13825
|
+
}
|
|
13826
|
+
}
|
|
13827
|
+
if (dispatched.length > 0) {
|
|
13828
|
+
const firstTask = dispatched[0];
|
|
13829
|
+
const rendered = await dispatchTemplate(
|
|
13830
|
+
svc.db,
|
|
13831
|
+
svc.config,
|
|
13832
|
+
svc.embedder,
|
|
13833
|
+
firstTask.taskId,
|
|
13834
|
+
firstTask.template,
|
|
13835
|
+
{ dryRun: true }
|
|
13836
|
+
);
|
|
13837
|
+
if (rendered.ok) {
|
|
13838
|
+
if (rendered.data.mode === "assisted") {
|
|
13839
|
+
process.stdout.write(`
|
|
13840
|
+
## Assisted Step \u2014 Coordinator Instructions
|
|
13841
|
+
|
|
13842
|
+
`);
|
|
13843
|
+
process.stdout.write(
|
|
13844
|
+
"This step requires interactive coordination. Use this prompt in your current session.\n"
|
|
13845
|
+
);
|
|
13846
|
+
process.stdout.write("Do NOT dispatch this to an autonomous agent.\n\n");
|
|
13847
|
+
process.stdout.write("---\n\n");
|
|
13848
|
+
} else {
|
|
13849
|
+
process.stdout.write(`
|
|
13850
|
+
--- Dispatch prompt (${firstTask.stepId}) ---
|
|
13851
|
+
`);
|
|
13852
|
+
}
|
|
13853
|
+
process.stdout.write(rendered.data.rendered + "\n");
|
|
13854
|
+
}
|
|
13855
|
+
}
|
|
13856
|
+
}
|
|
13857
|
+
});
|
|
13858
|
+
});
|
|
13859
|
+
const run = new Command46("run").description("Instantiate, expand, and dispatch the first step(s) of a workflow").argument("<definition-id>", "Workflow definition ID to run").requiredOption("--project <prefix>", "Project prefix for the instance").option("--param <key=value...>", "Parameter key=value pairs", []).option("--template <name>", "Override dispatch template for first step(s)").option("--json", "Output JSON").action(async (definitionId, opts) => {
|
|
13860
|
+
let params;
|
|
13861
|
+
try {
|
|
13862
|
+
params = parseContextPairs(opts.param);
|
|
13863
|
+
} catch (e) {
|
|
13864
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
13865
|
+
process.stderr.write(
|
|
13866
|
+
formatError3({ code: "INVALID_PARAM", message: msg }, !!opts.json) + "\n"
|
|
13867
|
+
);
|
|
13868
|
+
process.exitCode = 1;
|
|
13869
|
+
return;
|
|
13870
|
+
}
|
|
13871
|
+
await withBrain(async (svc) => {
|
|
13872
|
+
const instResult = await instantiateWorkflow(
|
|
13873
|
+
svc.db,
|
|
13874
|
+
svc.config,
|
|
13875
|
+
svc.embedder,
|
|
13876
|
+
definitionId,
|
|
13877
|
+
opts.project,
|
|
13878
|
+
params
|
|
13879
|
+
);
|
|
13880
|
+
if (!instResult.ok) {
|
|
13881
|
+
process.stderr.write(formatError3(instResult.error, !!opts.json) + "\n");
|
|
13882
|
+
process.exitCode = 1;
|
|
13883
|
+
return;
|
|
13884
|
+
}
|
|
13885
|
+
const instanceId = instResult.data.display_id;
|
|
13886
|
+
const expandResult = await expandWorkflow(svc.db, svc.config, svc.embedder, instanceId);
|
|
13887
|
+
if (!expandResult.ok) {
|
|
13888
|
+
process.stderr.write(formatError3(expandResult.error, !!opts.json) + "\n");
|
|
13889
|
+
process.exitCode = 1;
|
|
13890
|
+
return;
|
|
13891
|
+
}
|
|
13892
|
+
const defResult = getWorkflowDefinition(svc.db, definitionId);
|
|
13893
|
+
if (!defResult.ok) {
|
|
13894
|
+
process.stderr.write(formatError3(defResult.error, !!opts.json) + "\n");
|
|
13895
|
+
process.exitCode = 1;
|
|
13896
|
+
return;
|
|
13897
|
+
}
|
|
13898
|
+
const { definition } = defResult.data;
|
|
13899
|
+
const unconditionalEdges = definition.edges.filter((e) => !e.condition);
|
|
13900
|
+
const hasIncoming = new Set(unconditionalEdges.map((e) => e.to));
|
|
13901
|
+
const entrySteps = definition.steps.filter((s) => !hasIncoming.has(s.id));
|
|
13902
|
+
const stepsResult = getInstanceStepStates(svc.db, instanceId);
|
|
13903
|
+
const stepToTask = /* @__PURE__ */ new Map();
|
|
13904
|
+
if (stepsResult.ok) {
|
|
13905
|
+
for (const step of stepsResult.data.steps) {
|
|
13906
|
+
stepToTask.set(step.stepId, step.taskDisplayId);
|
|
13907
|
+
}
|
|
13908
|
+
}
|
|
13909
|
+
const dispatched = [];
|
|
13910
|
+
const dispatchErrors = [];
|
|
13911
|
+
for (const step of entrySteps) {
|
|
13912
|
+
const taskId = stepToTask.get(step.id);
|
|
13913
|
+
if (!taskId) continue;
|
|
13914
|
+
const templateName = opts.template ?? step.template;
|
|
13915
|
+
if (!templateName) {
|
|
13916
|
+
dispatchErrors.push({
|
|
13917
|
+
stepId: step.id,
|
|
13918
|
+
error: `No template specified (use --template or define template in step)`
|
|
13919
|
+
});
|
|
13920
|
+
continue;
|
|
13921
|
+
}
|
|
13922
|
+
const isAssisted = step.mode === "assisted";
|
|
13923
|
+
const dispatchResult = await dispatchTemplate(
|
|
13924
|
+
svc.db,
|
|
13925
|
+
svc.config,
|
|
13926
|
+
svc.embedder,
|
|
13927
|
+
taskId,
|
|
13928
|
+
templateName,
|
|
13929
|
+
{ claim: !isAssisted }
|
|
13930
|
+
);
|
|
13931
|
+
if (!dispatchResult.ok) {
|
|
13932
|
+
dispatchErrors.push({
|
|
13933
|
+
stepId: step.id,
|
|
13934
|
+
error: dispatchResult.error.message
|
|
13935
|
+
});
|
|
13936
|
+
continue;
|
|
13937
|
+
}
|
|
13938
|
+
dispatched.push({ stepId: step.id, taskId, template: templateName });
|
|
13939
|
+
}
|
|
13940
|
+
if (opts.json) {
|
|
13941
|
+
process.stdout.write(
|
|
13942
|
+
JSON.stringify(
|
|
13943
|
+
{
|
|
13944
|
+
instance_id: instanceId,
|
|
13945
|
+
workflow_id: instResult.data.workflow_id,
|
|
13946
|
+
workflow_version: instResult.data.workflow_version,
|
|
13947
|
+
tasks_created: expandResult.data.tasksCreated,
|
|
13948
|
+
edges: expandResult.data.edges,
|
|
13949
|
+
dispatched,
|
|
13950
|
+
errors: dispatchErrors
|
|
13951
|
+
},
|
|
13952
|
+
null,
|
|
13953
|
+
2
|
|
13954
|
+
) + "\n"
|
|
13955
|
+
);
|
|
13956
|
+
} else {
|
|
13957
|
+
process.stdout.write(`Instance: ${instanceId}
|
|
13958
|
+
`);
|
|
13959
|
+
process.stdout.write(
|
|
13960
|
+
` Workflow: ${instResult.data.workflow_id} v${instResult.data.workflow_version}
|
|
13961
|
+
`
|
|
13962
|
+
);
|
|
13963
|
+
process.stdout.write(
|
|
13964
|
+
` Expanded: ${expandResult.data.tasksCreated} tasks, ${expandResult.data.edges} edges
|
|
13965
|
+
`
|
|
13966
|
+
);
|
|
13967
|
+
if (dispatched.length > 0) {
|
|
13968
|
+
process.stdout.write(`
|
|
13969
|
+
Dispatched steps:
|
|
13970
|
+
`);
|
|
13971
|
+
for (const d of dispatched) {
|
|
13972
|
+
process.stdout.write(` ${d.stepId} \u2192 ${d.taskId} (${d.template})
|
|
13973
|
+
`);
|
|
13974
|
+
}
|
|
13975
|
+
}
|
|
13976
|
+
if (dispatchErrors.length > 0) {
|
|
13977
|
+
process.stdout.write(`
|
|
13978
|
+
Dispatch errors:
|
|
13979
|
+
`);
|
|
13980
|
+
for (const e of dispatchErrors) {
|
|
13981
|
+
process.stdout.write(` ${e.stepId}: ${e.error}
|
|
13982
|
+
`);
|
|
13983
|
+
}
|
|
13984
|
+
}
|
|
13985
|
+
if (dispatched.length > 0) {
|
|
13986
|
+
const firstTask = dispatched[0];
|
|
13987
|
+
const rendered = await dispatchTemplate(
|
|
13988
|
+
svc.db,
|
|
13989
|
+
svc.config,
|
|
13990
|
+
svc.embedder,
|
|
13991
|
+
firstTask.taskId,
|
|
13992
|
+
firstTask.template,
|
|
13993
|
+
{ dryRun: true }
|
|
13994
|
+
);
|
|
13995
|
+
if (rendered.ok) {
|
|
13996
|
+
if (rendered.data.mode === "assisted") {
|
|
13997
|
+
process.stdout.write(`
|
|
13998
|
+
## Assisted Step \u2014 Coordinator Instructions
|
|
13999
|
+
|
|
14000
|
+
`);
|
|
14001
|
+
process.stdout.write(
|
|
14002
|
+
"This step requires interactive coordination. Use this prompt in your current session.\n"
|
|
14003
|
+
);
|
|
14004
|
+
process.stdout.write("Do NOT dispatch this to an autonomous agent.\n\n");
|
|
14005
|
+
process.stdout.write("---\n\n");
|
|
14006
|
+
} else {
|
|
14007
|
+
process.stdout.write(`
|
|
14008
|
+
--- Dispatch prompt (${firstTask.stepId}) ---
|
|
14009
|
+
`);
|
|
14010
|
+
}
|
|
14011
|
+
process.stdout.write(rendered.data.rendered + "\n");
|
|
14012
|
+
}
|
|
14013
|
+
}
|
|
14014
|
+
}
|
|
14015
|
+
});
|
|
14016
|
+
});
|
|
14017
|
+
return [add, expand, advance, next, run];
|
|
14018
|
+
}
|
|
14019
|
+
|
|
14020
|
+
// src/modules/workflow/commands/collapse.ts
|
|
14021
|
+
import { Command as Command47 } from "@commander-js/extra-typings";
|
|
14022
|
+
function createCollapseCommand() {
|
|
14023
|
+
const cmd = new Command47("collapse").description("Collapse a completed workflow instance").argument("<instance-id>", "Workflow instance display ID").option("--json", "Output JSON").action(async (instanceId, opts) => {
|
|
14024
|
+
await withBrain(async (svc) => {
|
|
14025
|
+
const result = await collapseWorkflow(svc.db, svc.config, svc.embedder, instanceId);
|
|
14026
|
+
if (!result.ok) {
|
|
14027
|
+
const msg = opts.json ? JSON.stringify(result.error, null, 2) : `Error: ${result.error.message}`;
|
|
14028
|
+
process.stderr.write(msg + "\n");
|
|
14029
|
+
process.exitCode = 1;
|
|
14030
|
+
return;
|
|
14031
|
+
}
|
|
14032
|
+
if (opts.json) {
|
|
14033
|
+
process.stdout.write(JSON.stringify(result.data, null, 2) + "\n");
|
|
14034
|
+
} else {
|
|
14035
|
+
process.stdout.write(`Collapsed workflow instance
|
|
14036
|
+
`);
|
|
14037
|
+
process.stdout.write(` Summary: ${result.data.summaryNoteId}
|
|
14038
|
+
`);
|
|
14039
|
+
process.stdout.write(` Tasks archived: ${result.data.tasksArchived}
|
|
14040
|
+
`);
|
|
14041
|
+
}
|
|
14042
|
+
});
|
|
14043
|
+
});
|
|
14044
|
+
return cmd;
|
|
14045
|
+
}
|
|
14046
|
+
|
|
14047
|
+
// src/modules/workflow/index.ts
|
|
14048
|
+
var workflowModule = {
|
|
14049
|
+
name: "workflow",
|
|
14050
|
+
version: "1.0.0",
|
|
14051
|
+
description: "Workflow definition, instantiation, and lifecycle management",
|
|
14052
|
+
register(ctx) {
|
|
14053
|
+
ctx.registerNoteType({
|
|
14054
|
+
name: "workflow",
|
|
14055
|
+
description: "Workflow definition with steps and edges",
|
|
14056
|
+
tier: "slow",
|
|
14057
|
+
schema: {
|
|
14058
|
+
type: "object",
|
|
14059
|
+
properties: {
|
|
14060
|
+
display_id: { type: "string", description: "Display identifier" },
|
|
14061
|
+
name: { type: "string", description: "Workflow name" },
|
|
14062
|
+
version: { type: "number", description: "Workflow version number" },
|
|
14063
|
+
registration_status: {
|
|
14064
|
+
type: "string",
|
|
14065
|
+
enum: ["registered", "draft", "archived"],
|
|
14066
|
+
description: "Registration status"
|
|
14067
|
+
},
|
|
14068
|
+
step_count: { type: "number", description: "Number of steps in the workflow" },
|
|
14069
|
+
edge_count: { type: "number", description: "Number of edges in the workflow" }
|
|
14070
|
+
},
|
|
14071
|
+
required: ["name", "version", "registration_status"]
|
|
14072
|
+
}
|
|
14073
|
+
});
|
|
14074
|
+
ctx.registerNoteType({
|
|
14075
|
+
name: "resource",
|
|
14076
|
+
description: "Resource bound to a workflow instance",
|
|
14077
|
+
tier: "fast",
|
|
14078
|
+
schema: {
|
|
14079
|
+
type: "object",
|
|
14080
|
+
properties: {
|
|
14081
|
+
display_id: { type: "string", description: "Display identifier" },
|
|
14082
|
+
resource_type: { type: "string", description: "Type of resource" },
|
|
14083
|
+
project: { type: "string", description: "Associated project prefix" },
|
|
14084
|
+
status: {
|
|
14085
|
+
type: "string",
|
|
14086
|
+
enum: ["active", "released", "expired"],
|
|
14087
|
+
description: "Resource status"
|
|
14088
|
+
},
|
|
14089
|
+
data: { type: "string", description: "Resource data payload" }
|
|
14090
|
+
},
|
|
14091
|
+
required: ["resource_type", "status"]
|
|
14092
|
+
}
|
|
14093
|
+
});
|
|
14094
|
+
ctx.registerRelationType({
|
|
14095
|
+
name: "instance-of",
|
|
14096
|
+
description: "Workflow instance is an instance of a workflow definition"
|
|
14097
|
+
});
|
|
14098
|
+
ctx.registerRelationType({
|
|
14099
|
+
name: "expands-to",
|
|
14100
|
+
description: "Workflow step expands to sub-steps"
|
|
14101
|
+
});
|
|
14102
|
+
ctx.registerRelationType({
|
|
14103
|
+
name: "iteration-of",
|
|
14104
|
+
description: "Workflow instance is an iteration of a previous instance"
|
|
14105
|
+
});
|
|
14106
|
+
ctx.registerExtractionStrategy({ shouldExtract: () => false });
|
|
14107
|
+
ctx.registerFilter({ visibility: "private" });
|
|
14108
|
+
const wfCmd = createWorkflowCommand();
|
|
14109
|
+
wfCmd.addCommand(createResourceCommand());
|
|
14110
|
+
for (const cmd of createLifecycleCommands()) {
|
|
14111
|
+
wfCmd.addCommand(cmd);
|
|
14112
|
+
}
|
|
14113
|
+
wfCmd.addCommand(createCollapseCommand());
|
|
14114
|
+
ctx.registerCommand(wfCmd);
|
|
14115
|
+
}
|
|
14116
|
+
};
|
|
14117
|
+
|
|
14118
|
+
// src/commands/instances.ts
|
|
14119
|
+
import { Command as Command48 } from "@commander-js/extra-typings";
|
|
14120
|
+
var listSubcommand = new Command48("list").description("List all known brain instances").option("--json", "output as JSON").option("--prune", "remove stale entries (paths that no longer exist)").action((opts) => {
|
|
11872
14121
|
const globalDir = GLOBAL_BRAIN_DIR;
|
|
11873
14122
|
if (opts.prune) {
|
|
11874
14123
|
const pruned = pruneStaleInstances(globalDir);
|
|
@@ -11901,10 +14150,10 @@ Local instances (${instances.length}):
|
|
|
11901
14150
|
}
|
|
11902
14151
|
}
|
|
11903
14152
|
});
|
|
11904
|
-
var instancesCommand = new
|
|
14153
|
+
var instancesCommand = new Command48("instances").description("Manage brain instances").addCommand(listSubcommand);
|
|
11905
14154
|
|
|
11906
14155
|
// src/cli.ts
|
|
11907
|
-
var program = new
|
|
14156
|
+
var program = new Command49().name("brain").description("Developer second brain with hybrid RAG search").version(createRequire(import.meta.url)("../package.json").version).option("--global", "Force use of global brain instance").option("--instance <path>", "Use specific brain instance at path");
|
|
11908
14157
|
program.addCommand(initCommand);
|
|
11909
14158
|
program.addCommand(indexCommand);
|
|
11910
14159
|
program.addCommand(searchCommand);
|
|
@@ -11931,7 +14180,7 @@ program.addCommand(resetCommand);
|
|
|
11931
14180
|
program.addCommand(notesCommand);
|
|
11932
14181
|
program.addCommand(instancesCommand);
|
|
11933
14182
|
async function main() {
|
|
11934
|
-
const { registry } = await loadModules({ modules: [pmModule] });
|
|
14183
|
+
const { registry } = await loadModules({ modules: [pmModule, workflowModule] });
|
|
11935
14184
|
for (const { command } of registry.getCommands()) {
|
|
11936
14185
|
program.addCommand(command);
|
|
11937
14186
|
}
|