@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/dist/cli.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  saveConfig,
12
12
  withBrain,
13
13
  withDb
14
- } from "./chunk-HNC656YT.js";
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-KSJZ7CMP.js";
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-AJKFX2TM.js";
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 Command46 } from "@commander-js/extra-typings";
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 basename7 = context.sourcePath.split("/").pop()?.replace(/\.[^.]+$/, "") ?? "unknown";
2374
- const slug = slugify(basename7);
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(config.notesDir, {
2480
- sourcePath: filePath,
2481
- format,
2482
- lineCount,
2483
- tier1Items: tier1.items,
2484
- tier2Items,
2485
- lowConfidenceRegions: [],
2486
- remainderContent: tier2Remainder
2487
- }, registry);
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: readdirSync5, statSync: statSync5 } = await import("fs");
3644
- const { join: join21 } = await import("path");
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 = readdirSync5(notesDir, { recursive: true });
3653
+ const entries = readdirSync7(notesDir, { recursive: true });
3649
3654
  for (const entry of entries) {
3650
- const full = join21(notesDir, entry);
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: existsSync22 } = await import("fs");
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 (!existsSync22(path)) {
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 Command44 } from "@commander-js/extra-typings";
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/modules/pm/errors.ts
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
- return { ok: false, error: pmError(code, message, details) };
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 ok(asInt);
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 ok(parsed.workstream);
4088
+ return ok2(parsed.workstream);
4068
4089
  }
4069
- return fail(
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 fail("NOT_FOUND", `No PM notes found. Run "brain pm list" to check projects.`);
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 ok(note.id);
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 fail("NOT_FOUND", `No PM note found with display ID "${displayId}".${hint}`);
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 fail(
4164
+ return fail2(
4144
4165
  "NOT_FOUND",
4145
4166
  `Project "${upper}" not found. Available projects: ${available.sort().join(", ")}.`
4146
4167
  );
4147
4168
  }
4148
- return fail("NOT_FOUND", `Project "${upper}" not found. No projects exist yet.`);
4169
+ return fail2("NOT_FOUND", `Project "${upper}" not found. No projects exist yet.`);
4149
4170
  }
4150
- return ok(upper);
4171
+ return ok2(upper);
4151
4172
  }
4152
4173
  const active = getActiveProject(db);
4153
- if (active) return ok(active);
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 ok(prefix);
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 fail(
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 fail("INVALID_INPUT", 'No projects found. Run "brain pm onboard <name>" to create one.');
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 ok(active);
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 ok(prefix);
4206
+ return ok2(prefix);
4186
4207
  }
4187
- return ok(null);
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 fail(
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 fail("PROJECT_EXISTS", `Project with prefix "${input.prefix}" already exists`);
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 ok(metadata);
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 ok(projects);
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 fail("NOT_FOUND", `Project "${prefix}" not found.${availableProjectsList(db)}`);
4366
+ return fail2("NOT_FOUND", `Project "${prefix}" not found.${availableProjectsList(db)}`);
4307
4367
  }
4308
4368
  const meta = JSON.parse(notes[0].metadata);
4309
- return ok(projectMetaFromRecord(meta));
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 fail("NOT_FOUND", `Project "${prefix}" not found.${availableProjectsList(db)}`);
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 fail("NOT_FOUND", `Project file not found at "${filePath}"`);
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 ok(projectMetaFromRecord(meta));
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 fail("NOT_FOUND", `Project "${prefix}" not found.${availableProjectsList(db)}`);
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 fail(
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 ok(void 0);
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 quoted = value.includes(" ") ? `"${value}"` : value;
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 ok(void 0);
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 fail("INVALID_TRANSITION", message, { from, to });
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) => statusByDisplay.get(dep) === "done");
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 dfs(node) {
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
- dfs(neighbor);
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
- dfs(node);
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((t) => t.status !== "done" && t.status !== "cancelled");
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 fail("NOT_FOUND", `Task "${taskDisplayId}" not found`);
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 ok({
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 ok({
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 fail("NOT_FOUND", `Project "${prefix}" not found`);
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 ok({
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 fail("NOT_FOUND", `Workstream "${wsDisplayId}" not found`);
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 ok({
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 fail("NOT_FOUND", `Project "${input.project}" not found`);
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 ok(metadata);
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 ok(workstreams);
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 fail("NOT_FOUND", `Workstream "${displayId}" not found`);
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 ok(workstreamMetaFromRecord(meta, description));
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 fail("NOT_FOUND", `Workstream "${displayId}" not found`);
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 fail("NOT_FOUND", `Workstream file not found at "${filePath}"`);
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 ok(workstreamMetaFromRecord(meta));
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 fail("NOT_FOUND", `Workstream "${displayId}" not found`);
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 fail("INVALID_INPUT", `Invalid workstream display ID "${displayId}"`);
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 fail(
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 ok(void 0);
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 fail("NOT_FOUND", `Project "${input.project}" not found`);
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 fail("NOT_FOUND", `Workstream "${wsDisplayId}" not found`);
5478
+ return fail2("NOT_FOUND", `Workstream "${wsDisplayId}" not found`);
5376
5479
  }
5377
5480
  if (!input.description?.trim()) {
5378
- return fail(
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 fail("NOT_FOUND", `Dependency "${depDisplayId}" not found`);
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 ok(metadata);
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 ok(cleaned);
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 fail("NOT_FOUND", `Task "${displayId}" not found.${hint}`);
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 ok({ ...taskMeta, virtualStates });
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 fail("NOT_FOUND", `Task "${displayId}" not found`);
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 fail(
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 fail("NOT_FOUND", `Task file not found at "${filePath}"`);
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 ok(taskMetaFromRecord(refreshedMeta));
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 fail("NOT_FOUND", `Task "${displayId}" not found`);
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 fail("NOT_FOUND", `Task file not found at "${filePath}"`);
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 ok(taskMetaFromRecord(refreshedMeta));
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 fail("NOT_FOUND", `Task "${displayId}" not found`);
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 fail(
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 ok(void 0);
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 fail(
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 fail("NOT_FOUND", `Project "${prefix}" not found.`);
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 ok({
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 ok(void 0);
6577
+ return ok2(void 0);
6475
6578
  }
6476
- return fail("INVALID_CLAIM_TOKEN", "Claim token does not match", {
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("INVALID_INPUT", 'No projects found. Run "brain pm onboard <name>" to create one.'),
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 redirectMsg = checkNamespaceMismatch(id.toUpperCase(), "task");
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
- const taskResult = getTask(svc.db, id.toUpperCase());
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
- id.toUpperCase(),
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 fail("NOT_FOUND", `Task "${displayId}" not found`);
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 fail("NOT_FOUND", `Task file not found at "${filePath}"`);
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 fail("NOT_FOUND", `Project "${input.project}" not found`);
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 fail("NOT_FOUND", `Source task "${input.sourceTask}" not found`);
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 fail("NOT_FOUND", `Impact target "${impactId}" not found`);
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 ok(metadata);
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 ok(decisions);
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 fail("NOT_FOUND", `Decision "${displayId}" not found`);
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 ok({ ...decisionMeta, content });
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 fail("NOT_FOUND", `Decision "${oldDisplayId}" not found`);
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 fail("NOT_FOUND", `Decision file not found at "${oldFilePath}"`);
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 ok({ old: oldDecision, new: newResult.data });
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 fail("NOT_FOUND", `Project "${input.project}" not found`);
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 fail("NOT_FOUND", `Task "${input.task}" not found`);
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 ok(metadata);
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 fail("NOT_FOUND", `No prompts found for task "${taskDisplayId}"`);
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 fail("NOT_FOUND", `Prompt version ${version} not found for task "${taskDisplayId}"`);
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 ok({ ...promptMetaFromRecord(targetMeta), content });
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 ok(prompts);
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 fail("NOT_FOUND", `No prompts found for task "${taskDisplayId}"`);
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 ok(prompts);
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 ok([]);
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 > promptIndexedAt) {
7695
+ if (decIndexedAt >= promptIndexedAt) {
7537
7696
  stale.push(promptMetaFromRecord(promptMeta));
7538
7697
  break;
7539
7698
  }
7540
7699
  }
7541
7700
  }
7542
- return ok(stale);
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) => statusMap.get(d) === "done");
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
- if (statusMap.get(dep) === "cancelled") {
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: "cancelled",
7656
- reason: `Depends on cancelled task ${dep}`
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 fail("NOT_FOUND", `Could not list workstreams: ${wsResult.error.message}`);
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 ok(ws.number);
8095
+ if (name.toLowerCase().includes(lower)) return ok2(ws.number);
7933
8096
  }
7934
- return fail(
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
- { error: true, code: "INVALID_INPUT", message: "--workstream requires --project when multiple projects exist" },
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 fail("INVALID_INPUT", "Capture content cannot be empty");
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 ok(record);
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 ok(captures);
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 fail("NOT_FOUND", `Capture "${captureNoteId}" not found`);
9411
+ return fail2("NOT_FOUND", `Capture "${captureNoteId}" not found`);
9245
9412
  }
9246
9413
  if (note.type !== "capture" || note.module !== "pm") {
9247
- return fail("INVALID_INPUT", `Note "${captureNoteId}" is not a PM capture`);
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 fail("INVALID_INPUT", `Capture "${captureNoteId}" has already been processed`);
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 ok(taskResult.data);
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 fail("INVALID_INPUT", "Import data must be a JSON object");
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 fail("INVALID_INPUT", 'Import data must have a non-empty "name" field');
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 fail("INVALID_INPUT", 'Import data must have a non-empty "prefix" field');
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 fail("INVALID_INPUT", '"workstreams" must be an array');
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 fail("INVALID_INPUT", `Workstream at index ${i} must have a non-empty "name" field`);
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 fail("INVALID_INPUT", `Workstream "${ws.name}" tasks must be an array`);
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 fail(
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 = fail(
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
- `Worktrees: ${worktreeInfo.used}/${worktreeInfo.max} in use
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 Command39 } from "@commander-js/extra-typings";
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 Command39("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) => {
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: [{ label: "Database accessible", passed: dbCheck.passed, error: dbCheck.error }],
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 Command40 } from "@commander-js/extra-typings";
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 Command40("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) => {
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 Command41 } from "@commander-js/extra-typings";
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 fail(
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 fail(
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 ok(manifest);
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 Command41("onboard");
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 Command42 } from "@commander-js/extra-typings";
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 Command42("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) => {
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 Command43 } from "@commander-js/extra-typings";
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 fail("NOT_FOUND", `Source task "${sourceDisplayId}" not found`);
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 fail(wsResult.error.code, wsResult.error.message, wsResult.error.details);
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 fail(taskResult.error.code, taskResult.error.message, taskResult.error.details);
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
- return ok({
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 Command43("review").description("PR review lifecycle commands");
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-MO7LSFOT.js");
11990
+ const { resolveUnknownCommand } = await import("./command-resolution-EJ6LTC2Z.js");
11773
11991
  let resolution;
11774
11992
  try {
11775
- const { withBrain: withBrain2 } = await import("./brain-service-4ETWBOIO.js");
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 Command44("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) => {
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 Command44("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) => {
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 Command44("tasks").description('Task management (alias for "task")').helpOption(false).allowUnknownOption().allowExcessArguments(true).action(async () => {
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 Command44("workstreams").description('Workstream management (alias for "workstream")').helpOption(false).allowUnknownOption().allowExcessArguments(true).action(async () => {
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 Command44("projects").description('Project management (alias for "project")').helpOption(false).allowUnknownOption().allowExcessArguments(true).action(async () => {
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 Command44("ls").description('List projects (alias for "list")').helpOption(false).allowUnknownOption().allowExcessArguments(true).action(async () => {
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/instances.ts
11870
- import { Command as Command45 } from "@commander-js/extra-typings";
11871
- var listSubcommand = new Command45("list").description("List all known brain instances").option("--json", "output as JSON").option("--prune", "remove stale entries (paths that no longer exist)").action((opts) => {
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 Command45("instances").description("Manage brain instances").addCommand(listSubcommand);
14153
+ var instancesCommand = new Command48("instances").description("Manage brain instances").addCommand(listSubcommand);
11905
14154
 
11906
14155
  // src/cli.ts
11907
- var program = new Command46().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");
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
  }