@cleocode/cleo 2026.3.73 → 2026.3.74

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/index.js CHANGED
@@ -9,6 +9,55 @@ var __export = (target, all) => {
9
9
  __defProp(target, name2, { get: all[name2], enumerable: true });
10
10
  };
11
11
 
12
+ // packages/cleo/src/cli/field-context.ts
13
+ import {
14
+ resolveFieldExtraction
15
+ } from "@cleocode/lafs-protocol";
16
+ function setFieldContext(ctx) {
17
+ currentContext = ctx;
18
+ }
19
+ function getFieldContext() {
20
+ return currentContext;
21
+ }
22
+ function resolveFieldContext(opts) {
23
+ const input = {
24
+ fieldFlag: typeof opts["field"] === "string" ? opts["field"] : void 0,
25
+ fieldsFlag: typeof opts["fields"] === "string" ? opts["fields"] : void 0,
26
+ mviFlag: typeof opts["mvi"] === "string" ? opts["mvi"] : void 0
27
+ };
28
+ return resolveFieldExtraction(input);
29
+ }
30
+ var currentContext;
31
+ var init_field_context = __esm({
32
+ "packages/cleo/src/cli/field-context.ts"() {
33
+ "use strict";
34
+ currentContext = {
35
+ mvi: "standard",
36
+ mviSource: "default",
37
+ expectsCustomMvi: false
38
+ };
39
+ }
40
+ });
41
+
42
+ // packages/cleo/src/cli/format-context.ts
43
+ function setFormatContext(resolution) {
44
+ currentResolution = resolution;
45
+ }
46
+ function getFormatContext() {
47
+ return currentResolution;
48
+ }
49
+ var currentResolution;
50
+ var init_format_context = __esm({
51
+ "packages/cleo/src/cli/format-context.ts"() {
52
+ "use strict";
53
+ currentResolution = {
54
+ format: "json",
55
+ source: "default",
56
+ quiet: false
57
+ };
58
+ }
59
+ });
60
+
12
61
  // packages/contracts/src/errors.ts
13
62
  function normalizeError(error40, fallbackMessage = "An unexpected error occurred") {
14
63
  if (error40 instanceof Error) {
@@ -632,6 +681,13 @@ var init_discovery = __esm({
632
681
  });
633
682
 
634
683
  // packages/core/src/logger.ts
684
+ var logger_exports = {};
685
+ __export(logger_exports, {
686
+ closeLogger: () => closeLogger,
687
+ getLogDir: () => getLogDir,
688
+ getLogger: () => getLogger,
689
+ initLogger: () => initLogger
690
+ });
635
691
  import { existsSync as existsSync2 } from "node:fs";
636
692
  import { dirname, join as join3 } from "node:path";
637
693
  import pino from "pino";
@@ -45251,8 +45307,8 @@ var init_checksum = __esm({
45251
45307
  });
45252
45308
 
45253
45309
  // packages/core/src/migration/logger.ts
45254
- var logger_exports = {};
45255
- __export(logger_exports, {
45310
+ var logger_exports2 = {};
45311
+ __export(logger_exports2, {
45256
45312
  MigrationLogger: () => MigrationLogger,
45257
45313
  createMigrationLogger: () => createMigrationLogger,
45258
45314
  getLatestMigrationLog: () => getLatestMigrationLog,
@@ -48205,6 +48261,903 @@ var init_transfer = __esm({
48205
48261
  }
48206
48262
  });
48207
48263
 
48264
+ // packages/core/src/task-work/index.ts
48265
+ var task_work_exports = {};
48266
+ __export(task_work_exports, {
48267
+ currentTask: () => currentTask,
48268
+ getTaskHistory: () => getTaskHistory,
48269
+ getWorkHistory: () => getWorkHistory,
48270
+ startTask: () => startTask,
48271
+ stopTask: () => stopTask
48272
+ });
48273
+ async function currentTask(cwd, accessor) {
48274
+ const acc = accessor ?? await getAccessor(cwd);
48275
+ const focus = await acc.getMetaValue("focus_state");
48276
+ return {
48277
+ currentTask: focus?.currentTask ?? null,
48278
+ currentPhase: focus?.currentPhase ?? null,
48279
+ sessionNote: focus?.sessionNote ?? null,
48280
+ nextAction: focus?.nextAction ?? null
48281
+ };
48282
+ }
48283
+ async function startTask(taskId, cwd, accessor) {
48284
+ if (!taskId) {
48285
+ throw new CleoError(2 /* INVALID_INPUT */, "Task ID is required");
48286
+ }
48287
+ const acc = accessor ?? await getAccessor(cwd);
48288
+ const task = await acc.loadSingleTask(taskId);
48289
+ if (!task) {
48290
+ throw new CleoError(4 /* NOT_FOUND */, `Task not found: ${taskId}`, {
48291
+ fix: `Use 'cleo find "${taskId}"' to search`
48292
+ });
48293
+ }
48294
+ const { tasks: allTasks } = await acc.queryTasks({});
48295
+ const unresolvedDeps = getUnresolvedDeps(taskId, allTasks);
48296
+ if (unresolvedDeps.length > 0) {
48297
+ throw new CleoError(
48298
+ 5 /* DEPENDENCY_ERROR */,
48299
+ `Task ${taskId} is blocked by unresolved dependencies: ${unresolvedDeps.join(", ")}`,
48300
+ {
48301
+ fix: `Complete blockers first: ${unresolvedDeps.map((d) => `cleo complete ${d}`).join(", ")}`
48302
+ }
48303
+ );
48304
+ }
48305
+ const focus = await acc.getMetaValue("focus_state") ?? {};
48306
+ const previousTask = focus.currentTask ?? null;
48307
+ focus.currentTask = taskId;
48308
+ focus.currentPhase = task.phase ?? null;
48309
+ const noteEntry = {
48310
+ note: `Started work on ${taskId}: ${task.title}`,
48311
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
48312
+ };
48313
+ if (!focus.sessionNotes) {
48314
+ focus.sessionNotes = [];
48315
+ }
48316
+ focus.sessionNotes.push(noteEntry);
48317
+ await acc.setMetaValue("focus_state", focus);
48318
+ await logOperation(
48319
+ "task_start",
48320
+ taskId,
48321
+ {
48322
+ previousTask,
48323
+ title: task.title
48324
+ },
48325
+ accessor
48326
+ );
48327
+ const { hooks: hooks2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
48328
+ hooks2.dispatch("PreToolUse", cwd ?? process.cwd(), {
48329
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
48330
+ taskId,
48331
+ taskTitle: task.title
48332
+ }).catch(() => {
48333
+ });
48334
+ return {
48335
+ taskId,
48336
+ taskTitle: task.title,
48337
+ previousTask
48338
+ };
48339
+ }
48340
+ async function stopTask(cwd, accessor) {
48341
+ const acc = accessor ?? await getAccessor(cwd);
48342
+ const focus = await acc.getMetaValue("focus_state");
48343
+ const previousTask = focus?.currentTask ?? null;
48344
+ if (!focus) {
48345
+ return { previousTask: null };
48346
+ }
48347
+ const taskId = focus.currentTask;
48348
+ const task = taskId ? await acc.loadSingleTask(taskId) : void 0;
48349
+ focus.currentTask = null;
48350
+ focus.nextAction = null;
48351
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
48352
+ if (taskId && task) {
48353
+ const { hooks: hooks2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
48354
+ hooks2.dispatch("PostToolUse", cwd ?? process.cwd(), {
48355
+ timestamp: now2,
48356
+ taskId,
48357
+ taskTitle: task.title,
48358
+ status: "done"
48359
+ }).catch(() => {
48360
+ });
48361
+ }
48362
+ await acc.setMetaValue("focus_state", focus);
48363
+ await logOperation(
48364
+ "task_stop",
48365
+ previousTask ?? "none",
48366
+ {
48367
+ previousTask
48368
+ },
48369
+ accessor
48370
+ );
48371
+ return { previousTask };
48372
+ }
48373
+ async function getWorkHistory(cwd, accessor) {
48374
+ const acc = accessor ?? await getAccessor(cwd);
48375
+ const focus = await acc.getMetaValue("focus_state");
48376
+ const notes = focus?.sessionNotes ?? [];
48377
+ const history = [];
48378
+ for (const note of notes) {
48379
+ const match = note.note.match(/^(?:Focus set to|Started work on) (T\d+)/);
48380
+ if (match) {
48381
+ history.push({
48382
+ taskId: match[1],
48383
+ timestamp: note.timestamp
48384
+ });
48385
+ }
48386
+ }
48387
+ return history.reverse();
48388
+ }
48389
+ var getTaskHistory;
48390
+ var init_task_work = __esm({
48391
+ "packages/core/src/task-work/index.ts"() {
48392
+ "use strict";
48393
+ init_src();
48394
+ init_errors3();
48395
+ init_data_accessor();
48396
+ init_add();
48397
+ init_dependency_check();
48398
+ init_handlers();
48399
+ getTaskHistory = getWorkHistory;
48400
+ }
48401
+ });
48402
+
48403
+ // packages/core/src/tasks/complete.ts
48404
+ var complete_exports = {};
48405
+ __export(complete_exports, {
48406
+ completeTask: () => completeTask
48407
+ });
48408
+ function isVerificationGate(value) {
48409
+ return VERIFICATION_GATES.has(value);
48410
+ }
48411
+ async function loadCompletionEnforcement(cwd) {
48412
+ const isTest = !!process.env.VITEST;
48413
+ const config2 = await loadConfig(cwd);
48414
+ const acceptance = config2.enforcement?.acceptance;
48415
+ const verificationCfg = config2.verification;
48416
+ const acceptanceMode = acceptance?.mode ?? (isTest ? "off" : "block");
48417
+ const acceptanceRequiredForPriorities = acceptance?.requiredForPriorities ?? (isTest ? [] : ["critical", "high", "medium", "low"]);
48418
+ const rawVerificationEnabled = await getRawConfigValue("verification.enabled", cwd);
48419
+ const verificationEnabled = rawVerificationEnabled !== void 0 ? rawVerificationEnabled : !isTest;
48420
+ const verificationRequiredGates = (verificationCfg?.requiredGates ?? []).filter(isVerificationGate).length > 0 ? (verificationCfg?.requiredGates ?? []).filter(isVerificationGate) : DEFAULT_VERIFICATION_REQUIRED_GATES;
48421
+ const verificationMaxRounds = verificationCfg?.maxRounds ?? 5;
48422
+ const lifecycleMode = config2.lifecycle?.mode ?? (isTest ? "off" : "strict");
48423
+ return {
48424
+ acceptanceMode,
48425
+ acceptanceRequiredForPriorities,
48426
+ verificationEnabled,
48427
+ verificationRequiredGates,
48428
+ verificationMaxRounds,
48429
+ lifecycleMode
48430
+ };
48431
+ }
48432
+ async function completeTask(options, cwd, accessor) {
48433
+ const acc = accessor ?? await getAccessor(cwd);
48434
+ const task = await acc.loadSingleTask(options.taskId);
48435
+ if (!task) {
48436
+ throw new CleoError(4 /* NOT_FOUND */, `Task not found: ${options.taskId}`, {
48437
+ fix: `Use 'cleo find "${options.taskId}"' to search`
48438
+ });
48439
+ }
48440
+ await requireActiveSession("tasks.complete", cwd);
48441
+ const enforcement = await loadCompletionEnforcement(cwd);
48442
+ if (task.status === "done") {
48443
+ throw new CleoError(17 /* TASK_COMPLETED */, `Task ${options.taskId} is already completed`);
48444
+ }
48445
+ if (task.depends?.length) {
48446
+ const deps = await acc.loadTasks(task.depends);
48447
+ const incompleteDeps = deps.filter((d) => d.status !== "done" && d.status !== "cancelled").map((d) => d.id);
48448
+ if (incompleteDeps.length > 0) {
48449
+ throw new CleoError(
48450
+ 5 /* DEPENDENCY_ERROR */,
48451
+ `Task ${options.taskId} has incomplete dependencies: ${incompleteDeps.join(", ")}`,
48452
+ {
48453
+ fix: `Complete dependencies first: ${incompleteDeps.map((d) => `cleo complete ${d}`).join(", ")}`
48454
+ }
48455
+ );
48456
+ }
48457
+ }
48458
+ const acceptanceEnforcement = await createAcceptanceEnforcement(cwd);
48459
+ const completionValidation = acceptanceEnforcement.validateCompletion(task);
48460
+ if (!completionValidation.valid) {
48461
+ throw new CleoError(
48462
+ completionValidation.exitCode ?? 6 /* VALIDATION_ERROR */,
48463
+ completionValidation.error,
48464
+ { fix: completionValidation.fix }
48465
+ );
48466
+ }
48467
+ if (enforcement.verificationEnabled && task.type !== "epic") {
48468
+ if (!task.verification) {
48469
+ throw new CleoError(
48470
+ 40 /* VERIFICATION_INIT_FAILED */,
48471
+ `Task ${options.taskId} is missing verification metadata`,
48472
+ {
48473
+ fix: `Initialize verification for ${options.taskId} before completion`
48474
+ }
48475
+ );
48476
+ }
48477
+ if (task.verification.round > enforcement.verificationMaxRounds) {
48478
+ throw new CleoError(
48479
+ 44 /* MAX_ROUNDS_EXCEEDED */,
48480
+ `Task ${options.taskId} exceeded verification max rounds (${enforcement.verificationMaxRounds})`,
48481
+ {
48482
+ fix: `Review failure log and resolve blockers before retrying completion`
48483
+ }
48484
+ );
48485
+ }
48486
+ const missingRequiredGates = enforcement.verificationRequiredGates.filter(
48487
+ (gate) => task.verification?.gates?.[gate] !== true
48488
+ );
48489
+ if (missingRequiredGates.length > 0 || task.verification.passed !== true) {
48490
+ const exitCode = enforcement.lifecycleMode === "strict" ? 80 /* LIFECYCLE_GATE_FAILED */ : 45 /* GATE_DEPENDENCY */;
48491
+ throw new CleoError(
48492
+ exitCode,
48493
+ `Task ${options.taskId} failed verification gates: ${missingRequiredGates.join(", ") || "verification.passed=false"}`,
48494
+ {
48495
+ fix: `Set required verification gates before completion: ${enforcement.verificationRequiredGates.join(", ")}`
48496
+ }
48497
+ );
48498
+ }
48499
+ }
48500
+ const children = await acc.getChildren(options.taskId);
48501
+ const incompleteChildren = children.filter(
48502
+ (c) => c.status !== "done" && c.status !== "cancelled"
48503
+ );
48504
+ if (incompleteChildren.length > 0 && task.type === "epic") {
48505
+ if (!task.noAutoComplete) {
48506
+ throw new CleoError(
48507
+ 16 /* HAS_CHILDREN */,
48508
+ `Epic ${options.taskId} has ${incompleteChildren.length} incomplete children: ${incompleteChildren.map((c) => c.id).join(", ")}`,
48509
+ {
48510
+ fix: `Complete children first or use 'cleo update ${options.taskId} --no-auto-complete'`
48511
+ }
48512
+ );
48513
+ }
48514
+ }
48515
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
48516
+ const before = { ...task };
48517
+ task.status = "done";
48518
+ task.completedAt = now2;
48519
+ task.updatedAt = now2;
48520
+ if (options.notes) {
48521
+ const timestampedNote = `${(/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC")}: ${options.notes}`;
48522
+ if (!task.notes) task.notes = [];
48523
+ task.notes.push(timestampedNote);
48524
+ }
48525
+ if (options.changeset) {
48526
+ if (!task.notes) task.notes = [];
48527
+ task.notes.push(`Changeset: ${options.changeset}`);
48528
+ }
48529
+ const autoCompleted = [];
48530
+ const autoCompletedTasks = [];
48531
+ if (task.parentId) {
48532
+ const parent = await acc.loadSingleTask(task.parentId);
48533
+ if (parent && parent.type === "epic" && !parent.noAutoComplete) {
48534
+ const siblings = await acc.getChildren(parent.id);
48535
+ const allDone = siblings.every(
48536
+ (c) => c.id === task.id || c.status === "done" || c.status === "cancelled"
48537
+ );
48538
+ if (allDone) {
48539
+ parent.status = "done";
48540
+ parent.completedAt = now2;
48541
+ parent.updatedAt = now2;
48542
+ autoCompleted.push(parent.id);
48543
+ autoCompletedTasks.push(parent);
48544
+ }
48545
+ }
48546
+ }
48547
+ await acc.transaction(async (tx) => {
48548
+ await tx.upsertSingleTask(task);
48549
+ for (const parentTask of autoCompletedTasks) {
48550
+ await tx.upsertSingleTask(parentTask);
48551
+ }
48552
+ await tx.appendLog({
48553
+ id: `log-${Math.floor(Date.now() / 1e3)}-${(await import("node:crypto")).randomBytes(3).toString("hex")}`,
48554
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
48555
+ action: "task_completed",
48556
+ taskId: options.taskId,
48557
+ actor: "system",
48558
+ details: { title: task.title, previousStatus: before.status },
48559
+ before: null,
48560
+ after: { title: task.title, previousStatus: before.status }
48561
+ });
48562
+ });
48563
+ const dependents = await acc.getDependents(options.taskId);
48564
+ const unblockedTasks = [];
48565
+ for (const dep of dependents) {
48566
+ if (dep.status === "done" || dep.status === "cancelled") continue;
48567
+ if (dep.depends?.length) {
48568
+ const depDeps = await acc.loadTasks(dep.depends);
48569
+ const stillUnresolved = depDeps.filter(
48570
+ (d) => d.id !== options.taskId && d.status !== "done" && d.status !== "cancelled"
48571
+ );
48572
+ if (stillUnresolved.length === 0) {
48573
+ unblockedTasks.push({ id: dep.id, title: dep.title });
48574
+ }
48575
+ } else {
48576
+ unblockedTasks.push({ id: dep.id, title: dep.title });
48577
+ }
48578
+ }
48579
+ Promise.resolve().then(() => (init_auto_extract(), auto_extract_exports)).then(
48580
+ ({ extractTaskCompletionMemory: extractTaskCompletionMemory2 }) => extractTaskCompletionMemory2(cwd ?? process.cwd(), task)
48581
+ ).catch(() => {
48582
+ });
48583
+ return {
48584
+ task,
48585
+ ...autoCompleted.length > 0 && { autoCompleted },
48586
+ ...unblockedTasks.length > 0 && { unblockedTasks }
48587
+ };
48588
+ }
48589
+ var DEFAULT_VERIFICATION_REQUIRED_GATES, VERIFICATION_GATES;
48590
+ var init_complete = __esm({
48591
+ "packages/core/src/tasks/complete.ts"() {
48592
+ "use strict";
48593
+ init_src();
48594
+ init_config();
48595
+ init_errors3();
48596
+ init_session_enforcement();
48597
+ init_data_accessor();
48598
+ init_enforcement();
48599
+ DEFAULT_VERIFICATION_REQUIRED_GATES = [
48600
+ "implemented",
48601
+ "testsPassed",
48602
+ "qaPassed",
48603
+ "securityPassed",
48604
+ "documented"
48605
+ ];
48606
+ VERIFICATION_GATES = /* @__PURE__ */ new Set([
48607
+ "implemented",
48608
+ "testsPassed",
48609
+ "qaPassed",
48610
+ "cleanupDone",
48611
+ "securityPassed",
48612
+ "documented"
48613
+ ]);
48614
+ }
48615
+ });
48616
+
48617
+ // packages/core/src/tasks/update.ts
48618
+ var update_exports = {};
48619
+ __export(update_exports, {
48620
+ updateTask: () => updateTask
48621
+ });
48622
+ function hasNonStatusDoneFields(options) {
48623
+ return NON_STATUS_DONE_FIELDS.some((field) => options[field] !== void 0);
48624
+ }
48625
+ async function updateTask(options, cwd, accessor) {
48626
+ const acc = accessor ?? await getAccessor(cwd);
48627
+ const task = await acc.loadSingleTask(options.taskId);
48628
+ if (!task) {
48629
+ throw new CleoError(4 /* NOT_FOUND */, `Task not found: ${options.taskId}`, {
48630
+ fix: `Use 'cleo find "${options.taskId}"' to search`
48631
+ });
48632
+ }
48633
+ await requireActiveSession("tasks.update", cwd);
48634
+ const changes = [];
48635
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
48636
+ const isStatusOnlyDoneTransition = options.status === "done" && task.status !== "done" && !hasNonStatusDoneFields(options);
48637
+ if (isStatusOnlyDoneTransition) {
48638
+ const result = await completeTask({ taskId: options.taskId }, cwd, accessor);
48639
+ return { task: result.task, changes: ["status"] };
48640
+ }
48641
+ if (options.status === "done" && task.status !== "done") {
48642
+ throw new CleoError(
48643
+ 6 /* VALIDATION_ERROR */,
48644
+ "status=done must use complete flow; do not combine with other update fields",
48645
+ {
48646
+ fix: `Run 'cleo complete ${options.taskId}' first, then apply additional updates with 'cleo update ${options.taskId} ...'`
48647
+ }
48648
+ );
48649
+ }
48650
+ const enforcement = await createAcceptanceEnforcement(cwd);
48651
+ const updateValidation = enforcement.validateUpdate(task, { acceptance: options.acceptance });
48652
+ if (!updateValidation.valid) {
48653
+ throw new CleoError(
48654
+ updateValidation.exitCode ?? 6 /* VALIDATION_ERROR */,
48655
+ updateValidation.error,
48656
+ { fix: updateValidation.fix }
48657
+ );
48658
+ }
48659
+ if (options.title !== void 0) {
48660
+ validateTitle(options.title);
48661
+ task.title = options.title;
48662
+ changes.push("title");
48663
+ }
48664
+ if (options.status !== void 0) {
48665
+ validateStatus(options.status);
48666
+ const oldStatus = task.status;
48667
+ task.status = options.status;
48668
+ changes.push("status");
48669
+ if (options.status === "done" && oldStatus !== "done") {
48670
+ task.completedAt = now2;
48671
+ }
48672
+ if (options.status === "cancelled" && oldStatus !== "cancelled") {
48673
+ task.cancelledAt = now2;
48674
+ }
48675
+ }
48676
+ if (options.priority !== void 0) {
48677
+ const normalizedPriority = normalizePriority(options.priority);
48678
+ task.priority = normalizedPriority;
48679
+ changes.push("priority");
48680
+ }
48681
+ if (options.type !== void 0) {
48682
+ validateTaskType(options.type);
48683
+ task.type = options.type;
48684
+ changes.push("type");
48685
+ }
48686
+ if (options.size !== void 0) {
48687
+ validateSize(options.size);
48688
+ task.size = options.size;
48689
+ changes.push("size");
48690
+ }
48691
+ if (options.phase !== void 0) {
48692
+ task.phase = options.phase;
48693
+ changes.push("phase");
48694
+ }
48695
+ if (options.description !== void 0) {
48696
+ task.description = options.description;
48697
+ changes.push("description");
48698
+ }
48699
+ if (options.labels !== void 0) {
48700
+ if (options.labels.length) validateLabels(options.labels);
48701
+ task.labels = options.labels;
48702
+ changes.push("labels");
48703
+ }
48704
+ if (options.addLabels?.length) {
48705
+ validateLabels(options.addLabels);
48706
+ const existing = new Set(task.labels ?? []);
48707
+ for (const l of options.addLabels) existing.add(l.trim());
48708
+ task.labels = [...existing];
48709
+ changes.push("labels");
48710
+ }
48711
+ if (options.removeLabels?.length) {
48712
+ const toRemove = new Set(options.removeLabels.map((l) => l.trim()));
48713
+ task.labels = (task.labels ?? []).filter((l) => !toRemove.has(l));
48714
+ changes.push("labels");
48715
+ }
48716
+ if (options.depends !== void 0) {
48717
+ task.depends = options.depends;
48718
+ changes.push("depends");
48719
+ }
48720
+ if (options.addDepends?.length) {
48721
+ const existing = new Set(task.depends ?? []);
48722
+ for (const d of options.addDepends) existing.add(d.trim());
48723
+ task.depends = [...existing];
48724
+ changes.push("depends");
48725
+ }
48726
+ if (options.removeDepends?.length) {
48727
+ const toRemove = new Set(options.removeDepends.map((d) => d.trim()));
48728
+ task.depends = (task.depends ?? []).filter((d) => !toRemove.has(d));
48729
+ changes.push("depends");
48730
+ }
48731
+ if (options.notes !== void 0) {
48732
+ const timestampedNote = `${(/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC")}: ${options.notes}`;
48733
+ if (!task.notes) task.notes = [];
48734
+ task.notes.push(timestampedNote);
48735
+ changes.push("notes");
48736
+ }
48737
+ if (options.acceptance !== void 0) {
48738
+ task.acceptance = options.acceptance;
48739
+ changes.push("acceptance");
48740
+ }
48741
+ if (options.files !== void 0) {
48742
+ task.files = options.files;
48743
+ changes.push("files");
48744
+ }
48745
+ if (options.blockedBy !== void 0) {
48746
+ task.blockedBy = options.blockedBy;
48747
+ changes.push("blockedBy");
48748
+ }
48749
+ if (options.noAutoComplete !== void 0) {
48750
+ task.noAutoComplete = options.noAutoComplete;
48751
+ changes.push("noAutoComplete");
48752
+ }
48753
+ if (options.pipelineStage !== void 0) {
48754
+ validatePipelineTransition(task.pipelineStage, options.pipelineStage);
48755
+ if (task.type === "epic" && task.pipelineStage) {
48756
+ await validateEpicStageAdvancement(
48757
+ {
48758
+ epicId: task.id,
48759
+ currentStage: task.pipelineStage,
48760
+ newStage: options.pipelineStage
48761
+ },
48762
+ acc,
48763
+ cwd
48764
+ );
48765
+ }
48766
+ if (task.type !== "epic") {
48767
+ const epicAncestor = task.parentId ? await findEpicAncestor(task.parentId, acc) : null;
48768
+ const directParent = task.parentId ? await acc.loadSingleTask(task.parentId) : null;
48769
+ const epicToCheck = directParent?.type === "epic" ? directParent : epicAncestor;
48770
+ if (epicToCheck) {
48771
+ await validateChildStageCeiling(
48772
+ { childStage: options.pipelineStage, epicId: epicToCheck.id },
48773
+ acc,
48774
+ cwd
48775
+ );
48776
+ }
48777
+ }
48778
+ task.pipelineStage = options.pipelineStage;
48779
+ changes.push("pipelineStage");
48780
+ }
48781
+ if (options.parentId !== void 0) {
48782
+ const newParentId = options.parentId || null;
48783
+ const currentParentId = task.parentId ?? null;
48784
+ if (newParentId !== currentParentId) {
48785
+ const originalType = task.type;
48786
+ if (!newParentId) {
48787
+ task.parentId = null;
48788
+ if (task.type === "subtask") task.type = "task";
48789
+ changes.push("parentId");
48790
+ if (task.type !== originalType) changes.push("type");
48791
+ } else {
48792
+ const newParent = await acc.loadSingleTask(newParentId);
48793
+ if (!newParent) {
48794
+ throw new CleoError(10 /* PARENT_NOT_FOUND */, `Parent task ${newParentId} not found`);
48795
+ }
48796
+ if (newParent.type === "subtask") {
48797
+ throw new CleoError(
48798
+ 13 /* INVALID_PARENT_TYPE */,
48799
+ `Cannot parent under subtask '${newParentId}'`
48800
+ );
48801
+ }
48802
+ const subtree = await acc.getSubtree(options.taskId);
48803
+ if (subtree.some((t) => t.id === newParentId)) {
48804
+ throw new CleoError(
48805
+ 14 /* CIRCULAR_REFERENCE */,
48806
+ `Moving '${options.taskId}' under '${newParentId}' would create a circular reference`
48807
+ );
48808
+ }
48809
+ const ancestors = await acc.getAncestorChain(newParentId);
48810
+ const parentDepth = ancestors.length;
48811
+ const config2 = await loadConfig(cwd);
48812
+ const policy = resolveHierarchyPolicy(config2);
48813
+ if (parentDepth + 1 >= policy.maxDepth) {
48814
+ throw new CleoError(
48815
+ 11 /* DEPTH_EXCEEDED */,
48816
+ `Maximum nesting depth ${policy.maxDepth} would be exceeded`
48817
+ );
48818
+ }
48819
+ task.parentId = newParentId;
48820
+ const newDepth = parentDepth + 1;
48821
+ if (newDepth === 1) task.type = "task";
48822
+ else if (newDepth >= 2) task.type = "subtask";
48823
+ changes.push("parentId");
48824
+ if (task.type !== originalType) changes.push("type");
48825
+ }
48826
+ }
48827
+ }
48828
+ if (changes.length === 0) {
48829
+ throw new CleoError(102 /* NO_CHANGE */, "No changes specified");
48830
+ }
48831
+ task.updatedAt = now2;
48832
+ await acc.transaction(async (tx) => {
48833
+ await tx.upsertSingleTask(task);
48834
+ await tx.appendLog({
48835
+ id: `log-${Math.floor(Date.now() / 1e3)}-${(await import("node:crypto")).randomBytes(3).toString("hex")}`,
48836
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
48837
+ action: "task_updated",
48838
+ taskId: options.taskId,
48839
+ actor: "system",
48840
+ details: { changes, title: task.title },
48841
+ before: null,
48842
+ after: { changes, title: task.title }
48843
+ });
48844
+ });
48845
+ return { task, changes };
48846
+ }
48847
+ var NON_STATUS_DONE_FIELDS;
48848
+ var init_update2 = __esm({
48849
+ "packages/core/src/tasks/update.ts"() {
48850
+ "use strict";
48851
+ init_src();
48852
+ init_config();
48853
+ init_errors3();
48854
+ init_session_enforcement();
48855
+ init_data_accessor();
48856
+ init_add();
48857
+ init_complete();
48858
+ init_enforcement();
48859
+ init_epic_enforcement();
48860
+ init_hierarchy_policy();
48861
+ init_pipeline_stage();
48862
+ NON_STATUS_DONE_FIELDS = [
48863
+ "title",
48864
+ "priority",
48865
+ "type",
48866
+ "size",
48867
+ "phase",
48868
+ "description",
48869
+ "labels",
48870
+ "addLabels",
48871
+ "removeLabels",
48872
+ "depends",
48873
+ "addDepends",
48874
+ "removeDepends",
48875
+ "notes",
48876
+ "acceptance",
48877
+ "files",
48878
+ "blockedBy",
48879
+ "parentId",
48880
+ "noAutoComplete",
48881
+ "pipelineStage"
48882
+ ];
48883
+ }
48884
+ });
48885
+
48886
+ // packages/core/src/nexus/workspace.ts
48887
+ function checkRateLimit(agentId) {
48888
+ const now2 = Date.now();
48889
+ const entry = rateLimitCounters.get(agentId);
48890
+ if (!entry || now2 - entry.windowStart > RATE_LIMIT_WINDOW_MS) {
48891
+ rateLimitCounters.set(agentId, { count: 1, windowStart: now2 });
48892
+ return;
48893
+ }
48894
+ entry.count++;
48895
+ if (entry.count > RATE_LIMIT_MAX_OPS) {
48896
+ throw new CleoError(
48897
+ 1 /* GENERAL_ERROR */,
48898
+ `Agent '${agentId}' exceeded rate limit: ${RATE_LIMIT_MAX_OPS} routing ops per ${RATE_LIMIT_WINDOW_MS / 1e3}s`
48899
+ );
48900
+ }
48901
+ }
48902
+ async function loadProjectACL(projectPath) {
48903
+ try {
48904
+ const { loadConfig: loadConfig4 } = await Promise.resolve().then(() => (init_config(), config_exports));
48905
+ const config2 = await loadConfig4(projectPath);
48906
+ const agents = config2?.authorizedAgents;
48907
+ if (Array.isArray(agents) && agents.length > 0) {
48908
+ return { authorizedAgents: agents };
48909
+ }
48910
+ } catch {
48911
+ }
48912
+ return DEFAULT_ACL;
48913
+ }
48914
+ function isAuthorized(acl, agentId) {
48915
+ if (acl.authorizedAgents.includes("*")) return true;
48916
+ return acl.authorizedAgents.includes(agentId);
48917
+ }
48918
+ function parseDirective(message) {
48919
+ const content = message.content;
48920
+ const verbMatch = content.match(/^\/(\w+)/);
48921
+ if (!verbMatch) return null;
48922
+ const verb = verbMatch[1];
48923
+ const taskRefs = [];
48924
+ const pattern = new RegExp(TASK_REF_PATTERN.source, "g");
48925
+ for (const m of content.matchAll(pattern)) {
48926
+ taskRefs.push(`T${m[1]}`);
48927
+ }
48928
+ const metaRefs = message.metadata?.taskRefs;
48929
+ if (Array.isArray(metaRefs)) {
48930
+ for (const ref of metaRefs) {
48931
+ if (typeof ref === "string" && !taskRefs.includes(ref)) {
48932
+ taskRefs.push(ref);
48933
+ }
48934
+ }
48935
+ }
48936
+ if (taskRefs.length === 0) return null;
48937
+ return {
48938
+ verb,
48939
+ taskRefs,
48940
+ agentId: message.from,
48941
+ messageId: message.id,
48942
+ timestamp: message.timestamp
48943
+ };
48944
+ }
48945
+ async function routeDirective(directive) {
48946
+ checkRateLimit(directive.agentId);
48947
+ const results = [];
48948
+ const operation = VERB_TO_OPERATION[directive.verb];
48949
+ if (!operation) {
48950
+ return results;
48951
+ }
48952
+ const projects = await nexusList();
48953
+ for (const taskRef of directive.taskRefs) {
48954
+ const result = await routeSingleTask(taskRef, directive, operation, projects);
48955
+ results.push(result);
48956
+ }
48957
+ return results;
48958
+ }
48959
+ async function routeSingleTask(taskId, directive, operation, projects) {
48960
+ let targetProject = null;
48961
+ let targetAccessor = null;
48962
+ for (const project of projects) {
48963
+ try {
48964
+ const acc = await getAccessor(project.path);
48965
+ const { tasks: tasks2 } = await acc.queryTasks({});
48966
+ const task = tasks2.find((t) => t.id === taskId);
48967
+ if (task) {
48968
+ targetProject = project;
48969
+ targetAccessor = acc;
48970
+ break;
48971
+ }
48972
+ } catch {
48973
+ }
48974
+ }
48975
+ if (!targetProject || !targetAccessor) {
48976
+ return {
48977
+ success: false,
48978
+ project: "unknown",
48979
+ projectPath: "",
48980
+ taskId,
48981
+ operation,
48982
+ error: `Task ${taskId} not found in any registered project`
48983
+ };
48984
+ }
48985
+ const acl = await loadProjectACL(targetProject.path);
48986
+ if (!isAuthorized(acl, directive.agentId)) {
48987
+ return {
48988
+ success: false,
48989
+ project: targetProject.name,
48990
+ projectPath: targetProject.path,
48991
+ taskId,
48992
+ operation,
48993
+ error: `Agent '${directive.agentId}' not authorized to mutate project '${targetProject.name}'`
48994
+ };
48995
+ }
48996
+ try {
48997
+ await executeOperation(operation, taskId, targetProject.path, targetAccessor, directive);
48998
+ await logRouteAudit(directive, targetProject.name, taskId, operation, true);
48999
+ return {
49000
+ success: true,
49001
+ project: targetProject.name,
49002
+ projectPath: targetProject.path,
49003
+ taskId,
49004
+ operation
49005
+ };
49006
+ } catch (err) {
49007
+ const errorMsg = err instanceof Error ? err.message : String(err);
49008
+ await logRouteAudit(directive, targetProject.name, taskId, operation, false, errorMsg);
49009
+ return {
49010
+ success: false,
49011
+ project: targetProject.name,
49012
+ projectPath: targetProject.path,
49013
+ taskId,
49014
+ operation,
49015
+ error: errorMsg
49016
+ };
49017
+ }
49018
+ }
49019
+ async function executeOperation(operation, taskId, projectPath, accessor, directive) {
49020
+ switch (operation) {
49021
+ case "tasks.start": {
49022
+ const { startTask: startTask3 } = await Promise.resolve().then(() => (init_task_work(), task_work_exports));
49023
+ await startTask3(taskId, projectPath, accessor);
49024
+ break;
49025
+ }
49026
+ case "tasks.complete": {
49027
+ const { completeTask: completeTask2 } = await Promise.resolve().then(() => (init_complete(), complete_exports));
49028
+ await completeTask2(
49029
+ { taskId, notes: `Completed via Conduit directive from ${directive.agentId}` },
49030
+ projectPath,
49031
+ accessor
49032
+ );
49033
+ break;
49034
+ }
49035
+ case "tasks.stop": {
49036
+ const { stopTask: stopTask3 } = await Promise.resolve().then(() => (init_task_work(), task_work_exports));
49037
+ await stopTask3(projectPath, accessor);
49038
+ break;
49039
+ }
49040
+ case "tasks.update": {
49041
+ const { updateTask: updateTask3 } = await Promise.resolve().then(() => (init_update2(), update_exports));
49042
+ await updateTask3(
49043
+ { taskId, notes: `Marked blocked via Conduit directive from ${directive.agentId}` },
49044
+ projectPath,
49045
+ accessor
49046
+ );
49047
+ break;
49048
+ }
49049
+ }
49050
+ }
49051
+ async function logRouteAudit(directive, projectName, taskId, operation, success2, error40) {
49052
+ try {
49053
+ const { getLogger: getLogger2 } = await Promise.resolve().then(() => (init_logger(), logger_exports));
49054
+ const log11 = getLogger2("nexus.route");
49055
+ const level = success2 ? "info" : "warn";
49056
+ log11[level](
49057
+ {
49058
+ directive: directive.verb,
49059
+ agentId: directive.agentId,
49060
+ messageId: directive.messageId,
49061
+ project: projectName,
49062
+ taskId,
49063
+ operation,
49064
+ success: success2,
49065
+ error: error40
49066
+ },
49067
+ `Conduit directive routed: ${directive.verb} ${taskId} \u2192 ${projectName} (${success2 ? "OK" : "FAILED"})`
49068
+ );
49069
+ } catch {
49070
+ }
49071
+ }
49072
+ async function workspaceStatus() {
49073
+ const projects = await nexusList();
49074
+ const summaries = [];
49075
+ const totals = { pending: 0, active: 0, done: 0, total: 0 };
49076
+ for (const project of projects) {
49077
+ try {
49078
+ const acc = await getAccessor(project.path);
49079
+ const { tasks: tasks2 } = await acc.queryTasks({});
49080
+ const counts2 = {
49081
+ pending: tasks2.filter((t) => t.status === "pending").length,
49082
+ active: tasks2.filter((t) => t.status === "active").length,
49083
+ done: tasks2.filter((t) => t.status === "done").length,
49084
+ total: tasks2.length
49085
+ };
49086
+ summaries.push({
49087
+ name: project.name,
49088
+ path: project.path,
49089
+ counts: counts2,
49090
+ health: project.healthStatus,
49091
+ lastSync: project.lastSync
49092
+ });
49093
+ totals.pending += counts2.pending;
49094
+ totals.active += counts2.active;
49095
+ totals.done += counts2.done;
49096
+ totals.total += counts2.total;
49097
+ } catch {
49098
+ summaries.push({
49099
+ name: project.name,
49100
+ path: project.path,
49101
+ counts: { pending: 0, active: 0, done: 0, total: 0 },
49102
+ health: "unreachable",
49103
+ lastSync: project.lastSync
49104
+ });
49105
+ }
49106
+ }
49107
+ return {
49108
+ projectCount: projects.length,
49109
+ projects: summaries,
49110
+ totals,
49111
+ computedAt: (/* @__PURE__ */ new Date()).toISOString()
49112
+ };
49113
+ }
49114
+ async function workspaceAgents() {
49115
+ const projects = await nexusList();
49116
+ const agents = [];
49117
+ for (const project of projects) {
49118
+ try {
49119
+ const { listAgentInstances: listAgentInstances2 } = await Promise.resolve().then(() => (init_registry2(), registry_exports2));
49120
+ const instances = await listAgentInstances2(void 0, project.path);
49121
+ for (const inst of instances) {
49122
+ agents.push({
49123
+ agentId: inst.id,
49124
+ agentType: inst.agentType,
49125
+ status: inst.status,
49126
+ project: project.name,
49127
+ taskId: inst.taskId ?? null,
49128
+ lastHeartbeat: inst.lastHeartbeat
49129
+ });
49130
+ }
49131
+ } catch {
49132
+ }
49133
+ }
49134
+ return agents;
49135
+ }
49136
+ var RATE_LIMIT_WINDOW_MS, RATE_LIMIT_MAX_OPS, rateLimitCounters, DEFAULT_ACL, TASK_REF_PATTERN, VERB_TO_OPERATION;
49137
+ var init_workspace = __esm({
49138
+ "packages/core/src/nexus/workspace.ts"() {
49139
+ "use strict";
49140
+ init_src();
49141
+ init_errors3();
49142
+ init_data_accessor();
49143
+ init_registry3();
49144
+ RATE_LIMIT_WINDOW_MS = 6e4;
49145
+ RATE_LIMIT_MAX_OPS = 100;
49146
+ rateLimitCounters = /* @__PURE__ */ new Map();
49147
+ DEFAULT_ACL = { authorizedAgents: ["*"] };
49148
+ TASK_REF_PATTERN = /\bT(\d+)\b/g;
49149
+ VERB_TO_OPERATION = {
49150
+ claim: "tasks.start",
49151
+ done: "tasks.complete",
49152
+ complete: "tasks.complete",
49153
+ blocked: "tasks.update",
49154
+ // Update status to blocked
49155
+ start: "tasks.start",
49156
+ stop: "tasks.stop"
49157
+ };
49158
+ }
49159
+ });
49160
+
48208
49161
  // packages/core/src/nexus/index.ts
48209
49162
  var nexus_exports = {};
48210
49163
  __export(nexus_exports, {
@@ -48239,6 +49192,7 @@ __export(nexus_exports, {
48239
49192
  nexusSyncAll: () => nexusSyncAll,
48240
49193
  nexusUnregister: () => nexusUnregister,
48241
49194
  orphanDetection: () => orphanDetection,
49195
+ parseDirective: () => parseDirective,
48242
49196
  parseQuery: () => parseQuery,
48243
49197
  permissionLevel: () => permissionLevel,
48244
49198
  previewTransfer: () => previewTransfer,
@@ -48249,10 +49203,13 @@ __export(nexus_exports, {
48249
49203
  resolveCrossDeps: () => resolveCrossDeps,
48250
49204
  resolveProjectPath: () => resolveProjectPath2,
48251
49205
  resolveTask: () => resolveTask,
49206
+ routeDirective: () => routeDirective,
48252
49207
  searchAcrossProjects: () => searchAcrossProjects,
48253
49208
  setPermission: () => setPermission,
48254
49209
  syncGitignore: () => syncGitignore,
48255
- validateSyntax: () => validateSyntax
49210
+ validateSyntax: () => validateSyntax,
49211
+ workspaceAgents: () => workspaceAgents,
49212
+ workspaceStatus: () => workspaceStatus
48256
49213
  });
48257
49214
  var init_nexus = __esm({
48258
49215
  "packages/core/src/nexus/index.ts"() {
@@ -48265,6 +49222,7 @@ var init_nexus = __esm({
48265
49222
  init_registry3();
48266
49223
  init_sharing();
48267
49224
  init_transfer();
49225
+ init_workspace();
48268
49226
  }
48269
49227
  });
48270
49228
 
@@ -49606,676 +50564,197 @@ async function advancePhase(force = false, _cwd, accessor) {
49606
50564
  6 /* VALIDATION_ERROR */,
49607
50565
  `Cannot advance - ${criticalTasks.length} critical task(s) remain in phase '${currentSlug}'`
49608
50566
  );
49609
- }
49610
- const totalTasks = phaseTasks.length;
49611
- const completionPercent = totalTasks > 0 ? Math.floor((totalTasks - incompleteTasks.length) * 100 / totalTasks) : 0;
49612
- const threshold = 90;
49613
- if (completionPercent < threshold && !force) {
49614
- throw new CleoError(
49615
- 6 /* VALIDATION_ERROR */,
49616
- `Cannot advance - ${incompleteTasks.length} incomplete task(s) in phase '${currentSlug}' (${completionPercent}% complete, threshold: ${threshold}%)`,
49617
- { fix: "Use --force to override" }
49618
- );
49619
- }
49620
- }
49621
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
49622
- currentPhase.status = "completed";
49623
- currentPhase.completedAt = now2;
49624
- const nextPhase = phases[nextSlug];
49625
- nextPhase.status = "active";
49626
- nextPhase.startedAt = now2;
49627
- const updatedMeta = meta;
49628
- updatedMeta.currentPhase = nextSlug;
49629
- const currentPhaseTaskCount = phaseTasks.length;
49630
- const { tasks: nextPhaseTasks } = await accessor.queryTasks({ phase: nextSlug });
49631
- const nextPhaseTaskCount = nextPhaseTasks.length;
49632
- addPhaseHistoryEntryToMeta(
49633
- updatedMeta,
49634
- currentSlug,
49635
- "completed",
49636
- null,
49637
- "Phase completed via advance",
49638
- currentPhaseTaskCount
49639
- );
49640
- addPhaseHistoryEntryToMeta(
49641
- updatedMeta,
49642
- nextSlug,
49643
- "started",
49644
- currentSlug,
49645
- `Phase started via advance from ${currentSlug}`,
49646
- nextPhaseTaskCount
49647
- );
49648
- await accessor.setMetaValue("project_meta", updatedMeta);
49649
- return {
49650
- previousPhase: currentSlug,
49651
- currentPhase: nextSlug,
49652
- forced: force
49653
- };
49654
- }
49655
- async function renamePhase(oldName, newName, _cwd, accessor) {
49656
- const meta = await accessor.getMetaValue("project_meta");
49657
- const phases = meta?.phases ?? {};
49658
- if (!phases[oldName]) {
49659
- throw new CleoError(4 /* NOT_FOUND */, `Phase '${oldName}' does not exist`);
49660
- }
49661
- if (phases[newName]) {
49662
- throw new CleoError(101 /* ALREADY_EXISTS */, `Phase '${newName}' already exists`);
49663
- }
49664
- if (!/^[a-z][a-z0-9-]*$/.test(newName)) {
49665
- throw new CleoError(2 /* INVALID_INPUT */, `Invalid phase name '${newName}'`);
49666
- }
49667
- phases[newName] = phases[oldName];
49668
- delete phases[oldName];
49669
- const { tasks: oldPhaseTasks } = await accessor.queryTasks({ phase: oldName });
49670
- let tasksUpdated = 0;
49671
- for (const task of oldPhaseTasks) {
49672
- task.phase = newName;
49673
- await accessor.upsertSingleTask(task);
49674
- tasksUpdated++;
49675
- }
49676
- const updatedMeta = meta;
49677
- let currentPhaseUpdated = false;
49678
- if (updatedMeta.currentPhase === oldName) {
49679
- updatedMeta.currentPhase = newName;
49680
- currentPhaseUpdated = true;
49681
- }
49682
- await accessor.setMetaValue("project_meta", updatedMeta);
49683
- const focus = await accessor.getMetaValue("focus_state");
49684
- if (focus?.currentPhase === oldName) {
49685
- focus.currentPhase = newName;
49686
- await accessor.setMetaValue("focus_state", focus);
49687
- }
49688
- return { oldName, newName, tasksUpdated, currentPhaseUpdated };
49689
- }
49690
- async function deletePhase(slug, options = {}, _cwd, accessor) {
49691
- const meta = await accessor.getMetaValue("project_meta");
49692
- const phases = meta?.phases ?? {};
49693
- if (!phases[slug]) {
49694
- throw new CleoError(4 /* NOT_FOUND */, `Phase '${slug}' does not exist`);
49695
- }
49696
- if (meta?.currentPhase === slug) {
49697
- throw new CleoError(
49698
- 6 /* VALIDATION_ERROR */,
49699
- `Cannot delete current project phase '${slug}'. Use 'phase set' to change phase first`
49700
- );
49701
- }
49702
- const { tasks: phaseTasks } = await accessor.queryTasks({ phase: slug });
49703
- if (phaseTasks.length > 0 && !options.reassignTo) {
49704
- throw new CleoError(
49705
- 6 /* VALIDATION_ERROR */,
49706
- `Cannot delete '${slug}': ${phaseTasks.length} tasks would be orphaned. Use --reassign-to <phase>`
49707
- );
49708
- }
49709
- if (!options.force) {
49710
- throw new CleoError(2 /* INVALID_INPUT */, "Phase deletion requires --force flag for safety");
49711
- }
49712
- if (options.reassignTo) {
49713
- if (!phases[options.reassignTo]) {
49714
- throw new CleoError(
49715
- 4 /* NOT_FOUND */,
49716
- `Reassignment target phase '${options.reassignTo}' does not exist`
49717
- );
49718
- }
49719
- }
49720
- let tasksReassigned = 0;
49721
- if (options.reassignTo) {
49722
- for (const task of phaseTasks) {
49723
- task.phase = options.reassignTo;
49724
- await accessor.upsertSingleTask(task);
49725
- tasksReassigned++;
49726
- }
49727
- }
49728
- delete phases[slug];
49729
- await accessor.setMetaValue("project_meta", meta);
49730
- return {
49731
- deletedPhase: slug,
49732
- tasksReassigned,
49733
- reassignedTo: options.reassignTo ?? null
49734
- };
49735
- }
49736
- function addPhaseHistoryEntryToMeta(meta, phase, transitionType, fromPhase, reason, taskCount) {
49737
- if (!meta.phaseHistory) {
49738
- meta.phaseHistory = [];
49739
- }
49740
- meta.phaseHistory.push({
49741
- phase,
49742
- transitionType,
49743
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
49744
- taskCount,
49745
- fromPhase,
49746
- reason
49747
- });
49748
- }
49749
- var init_phases = __esm({
49750
- "packages/core/src/phases/index.ts"() {
49751
- "use strict";
49752
- init_src();
49753
- init_errors3();
49754
- init_add();
49755
- }
49756
- });
49757
-
49758
- // packages/core/src/pipeline/phase.ts
49759
- async function listPhases2(projectRoot, accessor) {
49760
- try {
49761
- const result = await listPhases(projectRoot, accessor);
49762
- return { success: true, data: result };
49763
- } catch (err) {
49764
- const message = err instanceof Error ? err.message : String(err);
49765
- return {
49766
- success: false,
49767
- error: { code: "E_PHASE_LIST_FAILED", message }
49768
- };
49769
- }
49770
- }
49771
- async function showPhase2(projectRoot, phaseId, accessor) {
49772
- try {
49773
- const result = await showPhase(phaseId, projectRoot, accessor);
49774
- return { success: true, data: result };
49775
- } catch (err) {
49776
- const message = err instanceof Error ? err.message : String(err);
49777
- return {
49778
- success: false,
49779
- error: { code: "E_PHASE_SHOW_FAILED", message }
49780
- };
49781
- }
49782
- }
49783
- var init_phase = __esm({
49784
- "packages/core/src/pipeline/phase.ts"() {
49785
- "use strict";
49786
- init_phases();
49787
- }
49788
- });
49789
-
49790
- // packages/core/src/pipeline/index.ts
49791
- var pipeline_exports2 = {};
49792
- __export(pipeline_exports2, {
49793
- listPhases: () => listPhases2,
49794
- showPhase: () => showPhase2
49795
- });
49796
- var init_pipeline2 = __esm({
49797
- "packages/core/src/pipeline/index.ts"() {
49798
- "use strict";
49799
- init_phase();
49800
- }
49801
- });
49802
-
49803
- // packages/core/src/tasks/complete.ts
49804
- function isVerificationGate(value) {
49805
- return VERIFICATION_GATES.has(value);
49806
- }
49807
- async function loadCompletionEnforcement(cwd) {
49808
- const isTest = !!process.env.VITEST;
49809
- const config2 = await loadConfig(cwd);
49810
- const acceptance = config2.enforcement?.acceptance;
49811
- const verificationCfg = config2.verification;
49812
- const acceptanceMode = acceptance?.mode ?? (isTest ? "off" : "block");
49813
- const acceptanceRequiredForPriorities = acceptance?.requiredForPriorities ?? (isTest ? [] : ["critical", "high", "medium", "low"]);
49814
- const rawVerificationEnabled = await getRawConfigValue("verification.enabled", cwd);
49815
- const verificationEnabled = rawVerificationEnabled !== void 0 ? rawVerificationEnabled : !isTest;
49816
- const verificationRequiredGates = (verificationCfg?.requiredGates ?? []).filter(isVerificationGate).length > 0 ? (verificationCfg?.requiredGates ?? []).filter(isVerificationGate) : DEFAULT_VERIFICATION_REQUIRED_GATES;
49817
- const verificationMaxRounds = verificationCfg?.maxRounds ?? 5;
49818
- const lifecycleMode = config2.lifecycle?.mode ?? (isTest ? "off" : "strict");
49819
- return {
49820
- acceptanceMode,
49821
- acceptanceRequiredForPriorities,
49822
- verificationEnabled,
49823
- verificationRequiredGates,
49824
- verificationMaxRounds,
49825
- lifecycleMode
49826
- };
49827
- }
49828
- async function completeTask(options, cwd, accessor) {
49829
- const acc = accessor ?? await getAccessor(cwd);
49830
- const task = await acc.loadSingleTask(options.taskId);
49831
- if (!task) {
49832
- throw new CleoError(4 /* NOT_FOUND */, `Task not found: ${options.taskId}`, {
49833
- fix: `Use 'cleo find "${options.taskId}"' to search`
49834
- });
49835
- }
49836
- await requireActiveSession("tasks.complete", cwd);
49837
- const enforcement = await loadCompletionEnforcement(cwd);
49838
- if (task.status === "done") {
49839
- throw new CleoError(17 /* TASK_COMPLETED */, `Task ${options.taskId} is already completed`);
49840
- }
49841
- if (task.depends?.length) {
49842
- const deps = await acc.loadTasks(task.depends);
49843
- const incompleteDeps = deps.filter((d) => d.status !== "done" && d.status !== "cancelled").map((d) => d.id);
49844
- if (incompleteDeps.length > 0) {
49845
- throw new CleoError(
49846
- 5 /* DEPENDENCY_ERROR */,
49847
- `Task ${options.taskId} has incomplete dependencies: ${incompleteDeps.join(", ")}`,
49848
- {
49849
- fix: `Complete dependencies first: ${incompleteDeps.map((d) => `cleo complete ${d}`).join(", ")}`
49850
- }
49851
- );
49852
- }
49853
- }
49854
- const acceptanceEnforcement = await createAcceptanceEnforcement(cwd);
49855
- const completionValidation = acceptanceEnforcement.validateCompletion(task);
49856
- if (!completionValidation.valid) {
49857
- throw new CleoError(
49858
- completionValidation.exitCode ?? 6 /* VALIDATION_ERROR */,
49859
- completionValidation.error,
49860
- { fix: completionValidation.fix }
49861
- );
49862
- }
49863
- if (enforcement.verificationEnabled && task.type !== "epic") {
49864
- if (!task.verification) {
49865
- throw new CleoError(
49866
- 40 /* VERIFICATION_INIT_FAILED */,
49867
- `Task ${options.taskId} is missing verification metadata`,
49868
- {
49869
- fix: `Initialize verification for ${options.taskId} before completion`
49870
- }
49871
- );
49872
- }
49873
- if (task.verification.round > enforcement.verificationMaxRounds) {
49874
- throw new CleoError(
49875
- 44 /* MAX_ROUNDS_EXCEEDED */,
49876
- `Task ${options.taskId} exceeded verification max rounds (${enforcement.verificationMaxRounds})`,
49877
- {
49878
- fix: `Review failure log and resolve blockers before retrying completion`
49879
- }
49880
- );
49881
- }
49882
- const missingRequiredGates = enforcement.verificationRequiredGates.filter(
49883
- (gate) => task.verification?.gates?.[gate] !== true
49884
- );
49885
- if (missingRequiredGates.length > 0 || task.verification.passed !== true) {
49886
- const exitCode = enforcement.lifecycleMode === "strict" ? 80 /* LIFECYCLE_GATE_FAILED */ : 45 /* GATE_DEPENDENCY */;
49887
- throw new CleoError(
49888
- exitCode,
49889
- `Task ${options.taskId} failed verification gates: ${missingRequiredGates.join(", ") || "verification.passed=false"}`,
49890
- {
49891
- fix: `Set required verification gates before completion: ${enforcement.verificationRequiredGates.join(", ")}`
49892
- }
49893
- );
49894
- }
49895
- }
49896
- const children = await acc.getChildren(options.taskId);
49897
- const incompleteChildren = children.filter(
49898
- (c) => c.status !== "done" && c.status !== "cancelled"
49899
- );
49900
- if (incompleteChildren.length > 0 && task.type === "epic") {
49901
- if (!task.noAutoComplete) {
49902
- throw new CleoError(
49903
- 16 /* HAS_CHILDREN */,
49904
- `Epic ${options.taskId} has ${incompleteChildren.length} incomplete children: ${incompleteChildren.map((c) => c.id).join(", ")}`,
49905
- {
49906
- fix: `Complete children first or use 'cleo update ${options.taskId} --no-auto-complete'`
49907
- }
49908
- );
49909
- }
49910
- }
49911
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
49912
- const before = { ...task };
49913
- task.status = "done";
49914
- task.completedAt = now2;
49915
- task.updatedAt = now2;
49916
- if (options.notes) {
49917
- const timestampedNote = `${(/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC")}: ${options.notes}`;
49918
- if (!task.notes) task.notes = [];
49919
- task.notes.push(timestampedNote);
49920
- }
49921
- if (options.changeset) {
49922
- if (!task.notes) task.notes = [];
49923
- task.notes.push(`Changeset: ${options.changeset}`);
49924
- }
49925
- const autoCompleted = [];
49926
- const autoCompletedTasks = [];
49927
- if (task.parentId) {
49928
- const parent = await acc.loadSingleTask(task.parentId);
49929
- if (parent && parent.type === "epic" && !parent.noAutoComplete) {
49930
- const siblings = await acc.getChildren(parent.id);
49931
- const allDone = siblings.every(
49932
- (c) => c.id === task.id || c.status === "done" || c.status === "cancelled"
49933
- );
49934
- if (allDone) {
49935
- parent.status = "done";
49936
- parent.completedAt = now2;
49937
- parent.updatedAt = now2;
49938
- autoCompleted.push(parent.id);
49939
- autoCompletedTasks.push(parent);
49940
- }
49941
- }
49942
- }
49943
- await acc.transaction(async (tx) => {
49944
- await tx.upsertSingleTask(task);
49945
- for (const parentTask of autoCompletedTasks) {
49946
- await tx.upsertSingleTask(parentTask);
49947
- }
49948
- await tx.appendLog({
49949
- id: `log-${Math.floor(Date.now() / 1e3)}-${(await import("node:crypto")).randomBytes(3).toString("hex")}`,
49950
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
49951
- action: "task_completed",
49952
- taskId: options.taskId,
49953
- actor: "system",
49954
- details: { title: task.title, previousStatus: before.status },
49955
- before: null,
49956
- after: { title: task.title, previousStatus: before.status }
49957
- });
49958
- });
49959
- const dependents = await acc.getDependents(options.taskId);
49960
- const unblockedTasks = [];
49961
- for (const dep of dependents) {
49962
- if (dep.status === "done" || dep.status === "cancelled") continue;
49963
- if (dep.depends?.length) {
49964
- const depDeps = await acc.loadTasks(dep.depends);
49965
- const stillUnresolved = depDeps.filter(
49966
- (d) => d.id !== options.taskId && d.status !== "done" && d.status !== "cancelled"
49967
- );
49968
- if (stillUnresolved.length === 0) {
49969
- unblockedTasks.push({ id: dep.id, title: dep.title });
49970
- }
49971
- } else {
49972
- unblockedTasks.push({ id: dep.id, title: dep.title });
49973
- }
49974
- }
49975
- Promise.resolve().then(() => (init_auto_extract(), auto_extract_exports)).then(
49976
- ({ extractTaskCompletionMemory: extractTaskCompletionMemory2 }) => extractTaskCompletionMemory2(cwd ?? process.cwd(), task)
49977
- ).catch(() => {
49978
- });
49979
- return {
49980
- task,
49981
- ...autoCompleted.length > 0 && { autoCompleted },
49982
- ...unblockedTasks.length > 0 && { unblockedTasks }
49983
- };
49984
- }
49985
- var DEFAULT_VERIFICATION_REQUIRED_GATES, VERIFICATION_GATES;
49986
- var init_complete = __esm({
49987
- "packages/core/src/tasks/complete.ts"() {
49988
- "use strict";
49989
- init_src();
49990
- init_config();
49991
- init_errors3();
49992
- init_session_enforcement();
49993
- init_data_accessor();
49994
- init_enforcement();
49995
- DEFAULT_VERIFICATION_REQUIRED_GATES = [
49996
- "implemented",
49997
- "testsPassed",
49998
- "qaPassed",
49999
- "securityPassed",
50000
- "documented"
50001
- ];
50002
- VERIFICATION_GATES = /* @__PURE__ */ new Set([
50003
- "implemented",
50004
- "testsPassed",
50005
- "qaPassed",
50006
- "cleanupDone",
50007
- "securityPassed",
50008
- "documented"
50009
- ]);
50010
- }
50011
- });
50012
-
50013
- // packages/core/src/tasks/update.ts
50014
- var update_exports = {};
50015
- __export(update_exports, {
50016
- updateTask: () => updateTask
50017
- });
50018
- function hasNonStatusDoneFields(options) {
50019
- return NON_STATUS_DONE_FIELDS.some((field) => options[field] !== void 0);
50020
- }
50021
- async function updateTask(options, cwd, accessor) {
50022
- const acc = accessor ?? await getAccessor(cwd);
50023
- const task = await acc.loadSingleTask(options.taskId);
50024
- if (!task) {
50025
- throw new CleoError(4 /* NOT_FOUND */, `Task not found: ${options.taskId}`, {
50026
- fix: `Use 'cleo find "${options.taskId}"' to search`
50027
- });
50028
- }
50029
- await requireActiveSession("tasks.update", cwd);
50030
- const changes = [];
50031
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
50032
- const isStatusOnlyDoneTransition = options.status === "done" && task.status !== "done" && !hasNonStatusDoneFields(options);
50033
- if (isStatusOnlyDoneTransition) {
50034
- const result = await completeTask({ taskId: options.taskId }, cwd, accessor);
50035
- return { task: result.task, changes: ["status"] };
50036
- }
50037
- if (options.status === "done" && task.status !== "done") {
50038
- throw new CleoError(
50039
- 6 /* VALIDATION_ERROR */,
50040
- "status=done must use complete flow; do not combine with other update fields",
50041
- {
50042
- fix: `Run 'cleo complete ${options.taskId}' first, then apply additional updates with 'cleo update ${options.taskId} ...'`
50043
- }
50044
- );
50045
- }
50046
- const enforcement = await createAcceptanceEnforcement(cwd);
50047
- const updateValidation = enforcement.validateUpdate(task, { acceptance: options.acceptance });
50048
- if (!updateValidation.valid) {
50049
- throw new CleoError(
50050
- updateValidation.exitCode ?? 6 /* VALIDATION_ERROR */,
50051
- updateValidation.error,
50052
- { fix: updateValidation.fix }
50053
- );
50054
- }
50055
- if (options.title !== void 0) {
50056
- validateTitle(options.title);
50057
- task.title = options.title;
50058
- changes.push("title");
50059
- }
50060
- if (options.status !== void 0) {
50061
- validateStatus(options.status);
50062
- const oldStatus = task.status;
50063
- task.status = options.status;
50064
- changes.push("status");
50065
- if (options.status === "done" && oldStatus !== "done") {
50066
- task.completedAt = now2;
50067
- }
50068
- if (options.status === "cancelled" && oldStatus !== "cancelled") {
50069
- task.cancelledAt = now2;
50070
- }
50071
- }
50072
- if (options.priority !== void 0) {
50073
- const normalizedPriority = normalizePriority(options.priority);
50074
- task.priority = normalizedPriority;
50075
- changes.push("priority");
50076
- }
50077
- if (options.type !== void 0) {
50078
- validateTaskType(options.type);
50079
- task.type = options.type;
50080
- changes.push("type");
50081
- }
50082
- if (options.size !== void 0) {
50083
- validateSize(options.size);
50084
- task.size = options.size;
50085
- changes.push("size");
50086
- }
50087
- if (options.phase !== void 0) {
50088
- task.phase = options.phase;
50089
- changes.push("phase");
50090
- }
50091
- if (options.description !== void 0) {
50092
- task.description = options.description;
50093
- changes.push("description");
50094
- }
50095
- if (options.labels !== void 0) {
50096
- if (options.labels.length) validateLabels(options.labels);
50097
- task.labels = options.labels;
50098
- changes.push("labels");
50567
+ }
50568
+ const totalTasks = phaseTasks.length;
50569
+ const completionPercent = totalTasks > 0 ? Math.floor((totalTasks - incompleteTasks.length) * 100 / totalTasks) : 0;
50570
+ const threshold = 90;
50571
+ if (completionPercent < threshold && !force) {
50572
+ throw new CleoError(
50573
+ 6 /* VALIDATION_ERROR */,
50574
+ `Cannot advance - ${incompleteTasks.length} incomplete task(s) in phase '${currentSlug}' (${completionPercent}% complete, threshold: ${threshold}%)`,
50575
+ { fix: "Use --force to override" }
50576
+ );
50577
+ }
50099
50578
  }
50100
- if (options.addLabels?.length) {
50101
- validateLabels(options.addLabels);
50102
- const existing = new Set(task.labels ?? []);
50103
- for (const l of options.addLabels) existing.add(l.trim());
50104
- task.labels = [...existing];
50105
- changes.push("labels");
50579
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
50580
+ currentPhase.status = "completed";
50581
+ currentPhase.completedAt = now2;
50582
+ const nextPhase = phases[nextSlug];
50583
+ nextPhase.status = "active";
50584
+ nextPhase.startedAt = now2;
50585
+ const updatedMeta = meta;
50586
+ updatedMeta.currentPhase = nextSlug;
50587
+ const currentPhaseTaskCount = phaseTasks.length;
50588
+ const { tasks: nextPhaseTasks } = await accessor.queryTasks({ phase: nextSlug });
50589
+ const nextPhaseTaskCount = nextPhaseTasks.length;
50590
+ addPhaseHistoryEntryToMeta(
50591
+ updatedMeta,
50592
+ currentSlug,
50593
+ "completed",
50594
+ null,
50595
+ "Phase completed via advance",
50596
+ currentPhaseTaskCount
50597
+ );
50598
+ addPhaseHistoryEntryToMeta(
50599
+ updatedMeta,
50600
+ nextSlug,
50601
+ "started",
50602
+ currentSlug,
50603
+ `Phase started via advance from ${currentSlug}`,
50604
+ nextPhaseTaskCount
50605
+ );
50606
+ await accessor.setMetaValue("project_meta", updatedMeta);
50607
+ return {
50608
+ previousPhase: currentSlug,
50609
+ currentPhase: nextSlug,
50610
+ forced: force
50611
+ };
50612
+ }
50613
+ async function renamePhase(oldName, newName, _cwd, accessor) {
50614
+ const meta = await accessor.getMetaValue("project_meta");
50615
+ const phases = meta?.phases ?? {};
50616
+ if (!phases[oldName]) {
50617
+ throw new CleoError(4 /* NOT_FOUND */, `Phase '${oldName}' does not exist`);
50106
50618
  }
50107
- if (options.removeLabels?.length) {
50108
- const toRemove = new Set(options.removeLabels.map((l) => l.trim()));
50109
- task.labels = (task.labels ?? []).filter((l) => !toRemove.has(l));
50110
- changes.push("labels");
50619
+ if (phases[newName]) {
50620
+ throw new CleoError(101 /* ALREADY_EXISTS */, `Phase '${newName}' already exists`);
50111
50621
  }
50112
- if (options.depends !== void 0) {
50113
- task.depends = options.depends;
50114
- changes.push("depends");
50622
+ if (!/^[a-z][a-z0-9-]*$/.test(newName)) {
50623
+ throw new CleoError(2 /* INVALID_INPUT */, `Invalid phase name '${newName}'`);
50115
50624
  }
50116
- if (options.addDepends?.length) {
50117
- const existing = new Set(task.depends ?? []);
50118
- for (const d of options.addDepends) existing.add(d.trim());
50119
- task.depends = [...existing];
50120
- changes.push("depends");
50625
+ phases[newName] = phases[oldName];
50626
+ delete phases[oldName];
50627
+ const { tasks: oldPhaseTasks } = await accessor.queryTasks({ phase: oldName });
50628
+ let tasksUpdated = 0;
50629
+ for (const task of oldPhaseTasks) {
50630
+ task.phase = newName;
50631
+ await accessor.upsertSingleTask(task);
50632
+ tasksUpdated++;
50121
50633
  }
50122
- if (options.removeDepends?.length) {
50123
- const toRemove = new Set(options.removeDepends.map((d) => d.trim()));
50124
- task.depends = (task.depends ?? []).filter((d) => !toRemove.has(d));
50125
- changes.push("depends");
50634
+ const updatedMeta = meta;
50635
+ let currentPhaseUpdated = false;
50636
+ if (updatedMeta.currentPhase === oldName) {
50637
+ updatedMeta.currentPhase = newName;
50638
+ currentPhaseUpdated = true;
50126
50639
  }
50127
- if (options.notes !== void 0) {
50128
- const timestampedNote = `${(/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC")}: ${options.notes}`;
50129
- if (!task.notes) task.notes = [];
50130
- task.notes.push(timestampedNote);
50131
- changes.push("notes");
50640
+ await accessor.setMetaValue("project_meta", updatedMeta);
50641
+ const focus = await accessor.getMetaValue("focus_state");
50642
+ if (focus?.currentPhase === oldName) {
50643
+ focus.currentPhase = newName;
50644
+ await accessor.setMetaValue("focus_state", focus);
50132
50645
  }
50133
- if (options.acceptance !== void 0) {
50134
- task.acceptance = options.acceptance;
50135
- changes.push("acceptance");
50646
+ return { oldName, newName, tasksUpdated, currentPhaseUpdated };
50647
+ }
50648
+ async function deletePhase(slug, options = {}, _cwd, accessor) {
50649
+ const meta = await accessor.getMetaValue("project_meta");
50650
+ const phases = meta?.phases ?? {};
50651
+ if (!phases[slug]) {
50652
+ throw new CleoError(4 /* NOT_FOUND */, `Phase '${slug}' does not exist`);
50136
50653
  }
50137
- if (options.files !== void 0) {
50138
- task.files = options.files;
50139
- changes.push("files");
50654
+ if (meta?.currentPhase === slug) {
50655
+ throw new CleoError(
50656
+ 6 /* VALIDATION_ERROR */,
50657
+ `Cannot delete current project phase '${slug}'. Use 'phase set' to change phase first`
50658
+ );
50140
50659
  }
50141
- if (options.blockedBy !== void 0) {
50142
- task.blockedBy = options.blockedBy;
50143
- changes.push("blockedBy");
50660
+ const { tasks: phaseTasks } = await accessor.queryTasks({ phase: slug });
50661
+ if (phaseTasks.length > 0 && !options.reassignTo) {
50662
+ throw new CleoError(
50663
+ 6 /* VALIDATION_ERROR */,
50664
+ `Cannot delete '${slug}': ${phaseTasks.length} tasks would be orphaned. Use --reassign-to <phase>`
50665
+ );
50144
50666
  }
50145
- if (options.noAutoComplete !== void 0) {
50146
- task.noAutoComplete = options.noAutoComplete;
50147
- changes.push("noAutoComplete");
50667
+ if (!options.force) {
50668
+ throw new CleoError(2 /* INVALID_INPUT */, "Phase deletion requires --force flag for safety");
50148
50669
  }
50149
- if (options.pipelineStage !== void 0) {
50150
- validatePipelineTransition(task.pipelineStage, options.pipelineStage);
50151
- if (task.type === "epic" && task.pipelineStage) {
50152
- await validateEpicStageAdvancement(
50153
- {
50154
- epicId: task.id,
50155
- currentStage: task.pipelineStage,
50156
- newStage: options.pipelineStage
50157
- },
50158
- acc,
50159
- cwd
50670
+ if (options.reassignTo) {
50671
+ if (!phases[options.reassignTo]) {
50672
+ throw new CleoError(
50673
+ 4 /* NOT_FOUND */,
50674
+ `Reassignment target phase '${options.reassignTo}' does not exist`
50160
50675
  );
50161
50676
  }
50162
- if (task.type !== "epic") {
50163
- const epicAncestor = task.parentId ? await findEpicAncestor(task.parentId, acc) : null;
50164
- const directParent = task.parentId ? await acc.loadSingleTask(task.parentId) : null;
50165
- const epicToCheck = directParent?.type === "epic" ? directParent : epicAncestor;
50166
- if (epicToCheck) {
50167
- await validateChildStageCeiling(
50168
- { childStage: options.pipelineStage, epicId: epicToCheck.id },
50169
- acc,
50170
- cwd
50171
- );
50172
- }
50173
- }
50174
- task.pipelineStage = options.pipelineStage;
50175
- changes.push("pipelineStage");
50176
50677
  }
50177
- if (options.parentId !== void 0) {
50178
- const newParentId = options.parentId || null;
50179
- const currentParentId = task.parentId ?? null;
50180
- if (newParentId !== currentParentId) {
50181
- const originalType = task.type;
50182
- if (!newParentId) {
50183
- task.parentId = null;
50184
- if (task.type === "subtask") task.type = "task";
50185
- changes.push("parentId");
50186
- if (task.type !== originalType) changes.push("type");
50187
- } else {
50188
- const newParent = await acc.loadSingleTask(newParentId);
50189
- if (!newParent) {
50190
- throw new CleoError(10 /* PARENT_NOT_FOUND */, `Parent task ${newParentId} not found`);
50191
- }
50192
- if (newParent.type === "subtask") {
50193
- throw new CleoError(
50194
- 13 /* INVALID_PARENT_TYPE */,
50195
- `Cannot parent under subtask '${newParentId}'`
50196
- );
50197
- }
50198
- const subtree = await acc.getSubtree(options.taskId);
50199
- if (subtree.some((t) => t.id === newParentId)) {
50200
- throw new CleoError(
50201
- 14 /* CIRCULAR_REFERENCE */,
50202
- `Moving '${options.taskId}' under '${newParentId}' would create a circular reference`
50203
- );
50204
- }
50205
- const ancestors = await acc.getAncestorChain(newParentId);
50206
- const parentDepth = ancestors.length;
50207
- const config2 = await loadConfig(cwd);
50208
- const policy = resolveHierarchyPolicy(config2);
50209
- if (parentDepth + 1 >= policy.maxDepth) {
50210
- throw new CleoError(
50211
- 11 /* DEPTH_EXCEEDED */,
50212
- `Maximum nesting depth ${policy.maxDepth} would be exceeded`
50213
- );
50214
- }
50215
- task.parentId = newParentId;
50216
- const newDepth = parentDepth + 1;
50217
- if (newDepth === 1) task.type = "task";
50218
- else if (newDepth >= 2) task.type = "subtask";
50219
- changes.push("parentId");
50220
- if (task.type !== originalType) changes.push("type");
50221
- }
50678
+ let tasksReassigned = 0;
50679
+ if (options.reassignTo) {
50680
+ for (const task of phaseTasks) {
50681
+ task.phase = options.reassignTo;
50682
+ await accessor.upsertSingleTask(task);
50683
+ tasksReassigned++;
50222
50684
  }
50223
50685
  }
50224
- if (changes.length === 0) {
50225
- throw new CleoError(102 /* NO_CHANGE */, "No changes specified");
50686
+ delete phases[slug];
50687
+ await accessor.setMetaValue("project_meta", meta);
50688
+ return {
50689
+ deletedPhase: slug,
50690
+ tasksReassigned,
50691
+ reassignedTo: options.reassignTo ?? null
50692
+ };
50693
+ }
50694
+ function addPhaseHistoryEntryToMeta(meta, phase, transitionType, fromPhase, reason, taskCount) {
50695
+ if (!meta.phaseHistory) {
50696
+ meta.phaseHistory = [];
50226
50697
  }
50227
- task.updatedAt = now2;
50228
- await acc.transaction(async (tx) => {
50229
- await tx.upsertSingleTask(task);
50230
- await tx.appendLog({
50231
- id: `log-${Math.floor(Date.now() / 1e3)}-${(await import("node:crypto")).randomBytes(3).toString("hex")}`,
50232
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
50233
- action: "task_updated",
50234
- taskId: options.taskId,
50235
- actor: "system",
50236
- details: { changes, title: task.title },
50237
- before: null,
50238
- after: { changes, title: task.title }
50239
- });
50698
+ meta.phaseHistory.push({
50699
+ phase,
50700
+ transitionType,
50701
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
50702
+ taskCount,
50703
+ fromPhase,
50704
+ reason
50240
50705
  });
50241
- return { task, changes };
50242
50706
  }
50243
- var NON_STATUS_DONE_FIELDS;
50244
- var init_update2 = __esm({
50245
- "packages/core/src/tasks/update.ts"() {
50707
+ var init_phases = __esm({
50708
+ "packages/core/src/phases/index.ts"() {
50246
50709
  "use strict";
50247
50710
  init_src();
50248
- init_config();
50249
50711
  init_errors3();
50250
- init_session_enforcement();
50251
- init_data_accessor();
50252
50712
  init_add();
50253
- init_complete();
50254
- init_enforcement();
50255
- init_epic_enforcement();
50256
- init_hierarchy_policy();
50257
- init_pipeline_stage();
50258
- NON_STATUS_DONE_FIELDS = [
50259
- "title",
50260
- "priority",
50261
- "type",
50262
- "size",
50263
- "phase",
50264
- "description",
50265
- "labels",
50266
- "addLabels",
50267
- "removeLabels",
50268
- "depends",
50269
- "addDepends",
50270
- "removeDepends",
50271
- "notes",
50272
- "acceptance",
50273
- "files",
50274
- "blockedBy",
50275
- "parentId",
50276
- "noAutoComplete",
50277
- "pipelineStage"
50278
- ];
50713
+ }
50714
+ });
50715
+
50716
+ // packages/core/src/pipeline/phase.ts
50717
+ async function listPhases2(projectRoot, accessor) {
50718
+ try {
50719
+ const result = await listPhases(projectRoot, accessor);
50720
+ return { success: true, data: result };
50721
+ } catch (err) {
50722
+ const message = err instanceof Error ? err.message : String(err);
50723
+ return {
50724
+ success: false,
50725
+ error: { code: "E_PHASE_LIST_FAILED", message }
50726
+ };
50727
+ }
50728
+ }
50729
+ async function showPhase2(projectRoot, phaseId, accessor) {
50730
+ try {
50731
+ const result = await showPhase(phaseId, projectRoot, accessor);
50732
+ return { success: true, data: result };
50733
+ } catch (err) {
50734
+ const message = err instanceof Error ? err.message : String(err);
50735
+ return {
50736
+ success: false,
50737
+ error: { code: "E_PHASE_SHOW_FAILED", message }
50738
+ };
50739
+ }
50740
+ }
50741
+ var init_phase = __esm({
50742
+ "packages/core/src/pipeline/phase.ts"() {
50743
+ "use strict";
50744
+ init_phases();
50745
+ }
50746
+ });
50747
+
50748
+ // packages/core/src/pipeline/index.ts
50749
+ var pipeline_exports2 = {};
50750
+ __export(pipeline_exports2, {
50751
+ listPhases: () => listPhases2,
50752
+ showPhase: () => showPhase2
50753
+ });
50754
+ var init_pipeline2 = __esm({
50755
+ "packages/core/src/pipeline/index.ts"() {
50756
+ "use strict";
50757
+ init_phase();
50279
50758
  }
50280
50759
  });
50281
50760
 
@@ -62663,145 +63142,6 @@ var init_system2 = __esm({
62663
63142
  }
62664
63143
  });
62665
63144
 
62666
- // packages/core/src/task-work/index.ts
62667
- var task_work_exports = {};
62668
- __export(task_work_exports, {
62669
- currentTask: () => currentTask,
62670
- getTaskHistory: () => getTaskHistory,
62671
- getWorkHistory: () => getWorkHistory,
62672
- startTask: () => startTask,
62673
- stopTask: () => stopTask
62674
- });
62675
- async function currentTask(cwd, accessor) {
62676
- const acc = accessor ?? await getAccessor(cwd);
62677
- const focus = await acc.getMetaValue("focus_state");
62678
- return {
62679
- currentTask: focus?.currentTask ?? null,
62680
- currentPhase: focus?.currentPhase ?? null,
62681
- sessionNote: focus?.sessionNote ?? null,
62682
- nextAction: focus?.nextAction ?? null
62683
- };
62684
- }
62685
- async function startTask(taskId, cwd, accessor) {
62686
- if (!taskId) {
62687
- throw new CleoError(2 /* INVALID_INPUT */, "Task ID is required");
62688
- }
62689
- const acc = accessor ?? await getAccessor(cwd);
62690
- const task = await acc.loadSingleTask(taskId);
62691
- if (!task) {
62692
- throw new CleoError(4 /* NOT_FOUND */, `Task not found: ${taskId}`, {
62693
- fix: `Use 'cleo find "${taskId}"' to search`
62694
- });
62695
- }
62696
- const { tasks: allTasks } = await acc.queryTasks({});
62697
- const unresolvedDeps = getUnresolvedDeps(taskId, allTasks);
62698
- if (unresolvedDeps.length > 0) {
62699
- throw new CleoError(
62700
- 5 /* DEPENDENCY_ERROR */,
62701
- `Task ${taskId} is blocked by unresolved dependencies: ${unresolvedDeps.join(", ")}`,
62702
- {
62703
- fix: `Complete blockers first: ${unresolvedDeps.map((d) => `cleo complete ${d}`).join(", ")}`
62704
- }
62705
- );
62706
- }
62707
- const focus = await acc.getMetaValue("focus_state") ?? {};
62708
- const previousTask = focus.currentTask ?? null;
62709
- focus.currentTask = taskId;
62710
- focus.currentPhase = task.phase ?? null;
62711
- const noteEntry = {
62712
- note: `Started work on ${taskId}: ${task.title}`,
62713
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
62714
- };
62715
- if (!focus.sessionNotes) {
62716
- focus.sessionNotes = [];
62717
- }
62718
- focus.sessionNotes.push(noteEntry);
62719
- await acc.setMetaValue("focus_state", focus);
62720
- await logOperation(
62721
- "task_start",
62722
- taskId,
62723
- {
62724
- previousTask,
62725
- title: task.title
62726
- },
62727
- accessor
62728
- );
62729
- const { hooks: hooks2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
62730
- hooks2.dispatch("PreToolUse", cwd ?? process.cwd(), {
62731
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
62732
- taskId,
62733
- taskTitle: task.title
62734
- }).catch(() => {
62735
- });
62736
- return {
62737
- taskId,
62738
- taskTitle: task.title,
62739
- previousTask
62740
- };
62741
- }
62742
- async function stopTask(cwd, accessor) {
62743
- const acc = accessor ?? await getAccessor(cwd);
62744
- const focus = await acc.getMetaValue("focus_state");
62745
- const previousTask = focus?.currentTask ?? null;
62746
- if (!focus) {
62747
- return { previousTask: null };
62748
- }
62749
- const taskId = focus.currentTask;
62750
- const task = taskId ? await acc.loadSingleTask(taskId) : void 0;
62751
- focus.currentTask = null;
62752
- focus.nextAction = null;
62753
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
62754
- if (taskId && task) {
62755
- const { hooks: hooks2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
62756
- hooks2.dispatch("PostToolUse", cwd ?? process.cwd(), {
62757
- timestamp: now2,
62758
- taskId,
62759
- taskTitle: task.title,
62760
- status: "done"
62761
- }).catch(() => {
62762
- });
62763
- }
62764
- await acc.setMetaValue("focus_state", focus);
62765
- await logOperation(
62766
- "task_stop",
62767
- previousTask ?? "none",
62768
- {
62769
- previousTask
62770
- },
62771
- accessor
62772
- );
62773
- return { previousTask };
62774
- }
62775
- async function getWorkHistory(cwd, accessor) {
62776
- const acc = accessor ?? await getAccessor(cwd);
62777
- const focus = await acc.getMetaValue("focus_state");
62778
- const notes = focus?.sessionNotes ?? [];
62779
- const history = [];
62780
- for (const note of notes) {
62781
- const match = note.note.match(/^(?:Focus set to|Started work on) (T\d+)/);
62782
- if (match) {
62783
- history.push({
62784
- taskId: match[1],
62785
- timestamp: note.timestamp
62786
- });
62787
- }
62788
- }
62789
- return history.reverse();
62790
- }
62791
- var getTaskHistory;
62792
- var init_task_work = __esm({
62793
- "packages/core/src/task-work/index.ts"() {
62794
- "use strict";
62795
- init_src();
62796
- init_errors3();
62797
- init_data_accessor();
62798
- init_add();
62799
- init_dependency_check();
62800
- init_handlers();
62801
- getTaskHistory = getWorkHistory;
62802
- }
62803
- });
62804
-
62805
63145
  // packages/core/src/tasks/archive.ts
62806
63146
  async function archiveTasks(options = {}, cwd, accessor) {
62807
63147
  const acc = accessor ?? await getAccessor(cwd);
@@ -63964,12 +64304,12 @@ function parseCommonFlags(args) {
63964
64304
  flags.remaining = remaining;
63965
64305
  return flags;
63966
64306
  }
63967
- function resolveFormat(flagFormat) {
64307
+ function resolveFormat2(flagFormat) {
63968
64308
  if (flagFormat === "json" || flagFormat === "human") return flagFormat;
63969
64309
  return process.stdout.isTTY ? "human" : "json";
63970
64310
  }
63971
64311
  function isJsonOutput(flags) {
63972
- return resolveFormat(flags.format) === "json";
64312
+ return resolveFormat2(flags.format) === "json";
63973
64313
  }
63974
64314
  var init_flags = __esm({
63975
64315
  "packages/core/src/ui/flags.ts"() {
@@ -64004,7 +64344,7 @@ __export(ui_exports, {
64004
64344
  parseCommandHeader: () => parseCommandHeader,
64005
64345
  parseCommonFlags: () => parseCommonFlags,
64006
64346
  removeAliases: () => removeAliases,
64007
- resolveFormat: () => resolveFormat,
64347
+ resolveFormat: () => resolveFormat2,
64008
64348
  scanAllCommands: () => scanAllCommands,
64009
64349
  validateHeader: () => validateHeader,
64010
64350
  writeChangelogFile: () => writeChangelogFile
@@ -68333,6 +68673,7 @@ var init_cleo = __esm({
68333
68673
  init_permissions();
68334
68674
  init_registry3();
68335
68675
  init_sharing();
68676
+ init_workspace();
68336
68677
  init_orchestration();
68337
68678
  init_reconciliation();
68338
68679
  init_link_store();
@@ -68571,7 +68912,14 @@ var init_cleo = __esm({
68571
68912
  discover: (p) => discoverRelated(p.query, p.method, p.limit),
68572
68913
  search: (p) => searchAcrossProjects(p.pattern, p.project, p.limit),
68573
68914
  setPermission: (p) => setPermission(p.name, p.level),
68574
- sharingStatus: () => getSharingStatus()
68915
+ sharingStatus: () => getSharingStatus(),
68916
+ route: (message) => {
68917
+ const directive = parseDirective(message);
68918
+ if (!directive) return Promise.resolve([]);
68919
+ return routeDirective(directive);
68920
+ },
68921
+ workspaceStatus: () => workspaceStatus(),
68922
+ workspaceAgents: () => workspaceAgents()
68575
68923
  };
68576
68924
  }
68577
68925
  // === Agents ===
@@ -75933,7 +76281,7 @@ async function runUpgrade(options = {}) {
75933
76281
  return { success: false, upToDate: false, dryRun: isDryRun, actions, applied: 0, errors };
75934
76282
  }
75935
76283
  await forceCheckpointBeforeOperation("storage-migration", options.cwd);
75936
- const { MigrationLogger: MigrationLogger2 } = await Promise.resolve().then(() => (init_logger3(), logger_exports));
76284
+ const { MigrationLogger: MigrationLogger2 } = await Promise.resolve().then(() => (init_logger3(), logger_exports2));
75937
76285
  const {
75938
76286
  createMigrationState: createMigrationState2,
75939
76287
  updateMigrationPhase: updateMigrationPhase2,
@@ -80487,41 +80835,6 @@ var init_internal = __esm({
80487
80835
  }
80488
80836
  });
80489
80837
 
80490
- // packages/cleo/src/cli/field-context.ts
80491
- import {
80492
- resolveFieldExtraction
80493
- } from "@cleocode/lafs-protocol";
80494
- function getFieldContext() {
80495
- return currentContext;
80496
- }
80497
- var currentContext;
80498
- var init_field_context = __esm({
80499
- "packages/cleo/src/cli/field-context.ts"() {
80500
- "use strict";
80501
- currentContext = {
80502
- mvi: "standard",
80503
- mviSource: "default",
80504
- expectsCustomMvi: false
80505
- };
80506
- }
80507
- });
80508
-
80509
- // packages/cleo/src/cli/format-context.ts
80510
- function getFormatContext() {
80511
- return currentResolution;
80512
- }
80513
- var currentResolution;
80514
- var init_format_context = __esm({
80515
- "packages/cleo/src/cli/format-context.ts"() {
80516
- "use strict";
80517
- currentResolution = {
80518
- format: "json",
80519
- source: "default",
80520
- quiet: false
80521
- };
80522
- }
80523
- });
80524
-
80525
80838
  // packages/cleo/src/cli/renderers/normalizer.ts
80526
80839
  function normalizeForHuman(command, data) {
80527
80840
  switch (command) {
@@ -95457,6 +95770,23 @@ function camelToKebab(str) {
95457
95770
  return str.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
95458
95771
  }
95459
95772
 
95773
+ // packages/cleo/src/cli/index.ts
95774
+ init_field_context();
95775
+ init_format_context();
95776
+
95777
+ // packages/cleo/src/cli/middleware/output-format.ts
95778
+ import { resolveOutputFormat } from "@cleocode/lafs-protocol";
95779
+ function resolveFormat(opts, defaults) {
95780
+ const input = {
95781
+ jsonFlag: opts["json"] === true,
95782
+ humanFlag: opts["human"] === true,
95783
+ quiet: opts["quiet"] === true,
95784
+ projectDefault: defaults?.projectDefault,
95785
+ userDefault: defaults?.userDefault
95786
+ };
95787
+ return resolveOutputFormat(input);
95788
+ }
95789
+
95460
95790
  // packages/cleo/src/cli/commands/add.ts
95461
95791
  init_cli();
95462
95792
  init_renderers();
@@ -101394,6 +101724,28 @@ for (const shim of rootShim._subcommands) {
101394
101724
  subCommands[alias] = shimToCitty(shim);
101395
101725
  }
101396
101726
  }
101727
+ {
101728
+ const argv = process.argv.slice(2);
101729
+ const rawOpts = {};
101730
+ for (let i = 0; i < argv.length; i++) {
101731
+ const arg = argv[i];
101732
+ if (arg === "--json") rawOpts["json"] = true;
101733
+ else if (arg === "--human") rawOpts["human"] = true;
101734
+ else if (arg === "--quiet") rawOpts["quiet"] = true;
101735
+ else if (arg === "--field" && i + 1 < argv.length) rawOpts["field"] = argv[++i];
101736
+ else if (arg === "--fields" && i + 1 < argv.length) rawOpts["fields"] = argv[++i];
101737
+ else if (arg === "--mvi" && i + 1 < argv.length) rawOpts["mvi"] = argv[++i];
101738
+ }
101739
+ const formatResolution = resolveFormat(rawOpts);
101740
+ setFormatContext(formatResolution);
101741
+ const fieldResolution = resolveFieldContext(rawOpts);
101742
+ setFieldContext(fieldResolution);
101743
+ if (argv[0] === "-V") {
101744
+ const { cliOutput: cliOutput2 } = await Promise.resolve().then(() => (init_renderers(), renderers_exports));
101745
+ cliOutput2({ version: CLI_VERSION }, { command: "version" });
101746
+ process.exit(0);
101747
+ }
101748
+ }
101397
101749
  if (process.argv[2] === "mcp") {
101398
101750
  const mcpPath = join119(import.meta.dirname ?? "", "..", "mcp", "index.js");
101399
101751
  const { spawn: spawn3 } = await import("node:child_process");