@uoyo/mvtt 2.0.0-beta.4 → 2.0.0-beta.6

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.
Files changed (86) hide show
  1. package/README.md +299 -64
  2. package/README.zh-CN.md +419 -0
  3. package/dist/commands/install.d.ts.map +1 -1
  4. package/dist/commands/install.js +27 -2
  5. package/dist/commands/install.js.map +1 -1
  6. package/dist/commands/uninstall.d.ts.map +1 -1
  7. package/dist/commands/uninstall.js +19 -7
  8. package/dist/commands/uninstall.js.map +1 -1
  9. package/dist/commands/update.d.ts.map +1 -1
  10. package/dist/commands/update.js +4 -2
  11. package/dist/commands/update.js.map +1 -1
  12. package/dist/fs/install-manifest.d.ts +4 -1
  13. package/dist/fs/install-manifest.d.ts.map +1 -1
  14. package/dist/fs/install-manifest.js +13 -1
  15. package/dist/fs/install-manifest.js.map +1 -1
  16. package/dist/fs/materialize.d.ts +2 -0
  17. package/dist/fs/materialize.d.ts.map +1 -1
  18. package/dist/fs/materialize.js +39 -9
  19. package/dist/fs/materialize.js.map +1 -1
  20. package/dist/fs/registry-merge.d.ts.map +1 -1
  21. package/dist/fs/registry-merge.js +72 -29
  22. package/dist/fs/registry-merge.js.map +1 -1
  23. package/dist/scripts/epic-update.cjs +7670 -0
  24. package/dist/scripts/plan-update.cjs +255 -82
  25. package/dist/scripts/session-update.cjs +84 -6
  26. package/dist/types/platform.d.ts +12 -0
  27. package/dist/types/platform.d.ts.map +1 -0
  28. package/dist/types/platform.js +24 -0
  29. package/dist/types/platform.js.map +1 -0
  30. package/dist/types/registry.d.ts +3 -24
  31. package/dist/types/registry.d.ts.map +1 -1
  32. package/install-manifest.yaml +4 -0
  33. package/package.json +1 -1
  34. package/registry.yaml +72 -198
  35. package/sources/defaults/config.yaml +8 -13
  36. package/sources/defaults/project-context.yaml +2 -5
  37. package/sources/defaults/session.yaml +14 -2
  38. package/sources/knowledge/core/manifest.yaml +1 -4
  39. package/sources/scripts/epic-update.js +512 -0
  40. package/sources/scripts/plan-update.js +614 -353
  41. package/sources/scripts/session-update.js +102 -2
  42. package/sources/sections/activation-load-config.md +1 -1
  43. package/sources/sections/activation-load-context.md +42 -13
  44. package/sources/sections/activation-preflight.md +1 -1
  45. package/sources/sections/footer-next-steps.md +3 -2
  46. package/sources/sections/session-update.md +41 -1
  47. package/sources/skills/mvt-analyze/business.md +46 -8
  48. package/sources/skills/mvt-analyze/manifest.yaml +5 -1
  49. package/sources/skills/mvt-analyze-code/business.md +18 -17
  50. package/sources/skills/mvt-analyze-code/manifest.yaml +3 -6
  51. package/sources/skills/mvt-check-context/business.md +13 -6
  52. package/sources/skills/mvt-check-context/manifest.yaml +0 -5
  53. package/sources/skills/mvt-cleanup/business.md +17 -2
  54. package/sources/skills/mvt-config/business.md +5 -5
  55. package/sources/skills/mvt-config/manifest.yaml +3 -6
  56. package/sources/skills/mvt-create-skill/business.md +2 -14
  57. package/sources/skills/mvt-create-skill/manifest.yaml +0 -5
  58. package/sources/skills/mvt-decompose/business.md +94 -0
  59. package/sources/skills/mvt-decompose/manifest.yaml +121 -0
  60. package/sources/skills/mvt-fix/business.md +21 -6
  61. package/sources/skills/mvt-fix/manifest.yaml +1 -1
  62. package/sources/skills/mvt-help/business.md +11 -9
  63. package/sources/skills/mvt-help/manifest.yaml +0 -5
  64. package/sources/skills/mvt-implement/business.md +51 -8
  65. package/sources/skills/mvt-init/business.md +23 -13
  66. package/sources/skills/mvt-init/manifest.yaml +1 -2
  67. package/sources/skills/mvt-manage-context/business.md +41 -14
  68. package/sources/skills/mvt-manage-context/manifest.yaml +2 -6
  69. package/sources/skills/mvt-plan-dev/business.md +17 -9
  70. package/sources/skills/mvt-quick-dev/business.md +22 -7
  71. package/sources/skills/mvt-quick-dev/manifest.yaml +0 -1
  72. package/sources/skills/mvt-refactor/business.md +32 -17
  73. package/sources/skills/mvt-refactor/manifest.yaml +0 -5
  74. package/sources/skills/mvt-resume/business.md +32 -12
  75. package/sources/skills/mvt-resume/manifest.yaml +3 -3
  76. package/sources/skills/mvt-review/business.md +24 -9
  77. package/sources/skills/mvt-status/business.md +37 -9
  78. package/sources/skills/mvt-status/manifest.yaml +2 -2
  79. package/sources/skills/mvt-sync-context/business.md +30 -16
  80. package/sources/skills/mvt-template/business.md +1 -1
  81. package/sources/skills/mvt-template/manifest.yaml +0 -5
  82. package/sources/skills/mvt-test/business.md +30 -15
  83. package/sources/skills/mvt-update-plan/business.md +41 -12
  84. package/sources/skills/mvt-update-plan/manifest.yaml +7 -7
  85. package/sources/templates/decompose-output/body.md +13 -0
  86. package/sources/templates/decompose-output/manifest.yaml +11 -0
@@ -7316,9 +7316,36 @@ var require_dist = __commonJS({
7316
7316
 
7317
7317
  // sources/scripts/plan-update.js
7318
7318
  var import_node_fs = require("node:fs");
7319
+ var import_node_path = require("node:path");
7319
7320
  var import_yaml = __toESM(require_dist(), 1);
7321
+ function findProjectRootFromPath(filePath) {
7322
+ let dir = (0, import_node_path.resolve)((0, import_node_path.dirname)(filePath));
7323
+ while (true) {
7324
+ if ((0, import_node_fs.existsSync)((0, import_node_path.join)(dir, ".ai-agents"))) return dir;
7325
+ const parent = (0, import_node_path.dirname)(dir);
7326
+ if (parent === dir) return null;
7327
+ dir = parent;
7328
+ }
7329
+ }
7330
+ function loadSoleProject(projectRoot) {
7331
+ if (!projectRoot) return null;
7332
+ const ctxPath = (0, import_node_path.join)(projectRoot, ".ai-agents/workspace/project-context.yaml");
7333
+ if (!(0, import_node_fs.existsSync)(ctxPath)) return null;
7334
+ try {
7335
+ const ctx = (0, import_yaml.parse)((0, import_node_fs.readFileSync)(ctxPath, "utf-8"));
7336
+ const projects = ctx?.projects;
7337
+ if (!Array.isArray(projects) || projects.length !== 1) return null;
7338
+ const name = projects[0]?.name;
7339
+ if (typeof name !== "string" || name === "") return null;
7340
+ return [name];
7341
+ } catch {
7342
+ return null;
7343
+ }
7344
+ }
7320
7345
  var VALID_STATUSES = ["pending", "in_progress", "done", "blocked", "skipped"];
7321
7346
  var TERMINAL_STATUSES = ["done", "blocked", "skipped"];
7347
+ var PROJECT_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
7348
+ var VALID_FRESHNESS = ["current", "stale"];
7322
7349
  var ERRORS = {
7323
7350
  MISSING_PLAN: () => "Missing required argument: --plan",
7324
7351
  MISSING_TASK: () => "Missing required argument: --task",
@@ -7329,37 +7356,42 @@ var ERRORS = {
7329
7356
  TASK_NOT_FOUND: (id, valid) => `Task "${id}" not found. Valid task ids: ${valid.length ? valid.join(", ") : "(none)"}.`,
7330
7357
  VALIDATION_FAILED: (errs) => `Plan validation failed; file not written:
7331
7358
  - ${errs.join("\n - ")}`,
7332
- PLAN_WRITE_FAILED: (detail) => `Failed to write plan.yaml: ${detail}`
7359
+ PLAN_WRITE_FAILED: (detail) => `Failed to write plan.yaml: ${detail}`,
7360
+ INVALID_PROJECT_NAME: (name) => `Invalid project name "${name}". Must match ${PROJECT_NAME_RE.source} (no leading underscore).`,
7361
+ INVALID_TASK_PROJECT: (taskId, proj, valid) => `Task "${taskId}" has project "${proj}" not in --projects list: ${valid.join(", ")}.`,
7362
+ INVALID_FRESHNESS: (taskId, val) => `Task "${taskId}" has invalid deliverables.freshness "${val}". Must be one of: ${VALID_FRESHNESS.join(", ")}.`,
7363
+ STALE_TASK_NOT_FOUND: (id, valid) => `--mark-deliverable-stale task "${id}" not found. Valid task ids: ${valid.length ? valid.join(", ") : "(none)"}.`,
7364
+ INVALID_DELIVERABLES_POINTER: (val) => `Invalid --deliverables-pointer "${val}". Only "current" is supported.`
7333
7365
  };
7334
7366
  function parseArgs(argv) {
7335
- const args = {};
7367
+ const args2 = {};
7336
7368
  for (let i = 2; i < argv.length; i++) {
7337
7369
  if (argv[i].startsWith("--")) {
7338
7370
  const key = argv[i].slice(2);
7339
7371
  const next = argv[i + 1];
7340
7372
  if (next && !next.startsWith("--")) {
7341
- args[key] = next;
7373
+ args2[key] = next;
7342
7374
  i++;
7343
7375
  } else {
7344
- args[key] = true;
7376
+ args2[key] = true;
7345
7377
  }
7346
7378
  }
7347
7379
  }
7348
- return args;
7380
+ return args2;
7349
7381
  }
7350
- function validateArgs(args) {
7351
- if (!args.plan || args.plan === true) return ERRORS.MISSING_PLAN();
7352
- if (!args.task || args.task === true) return ERRORS.MISSING_TASK();
7353
- if (!args.status || args.status === true) return ERRORS.MISSING_STATUS();
7354
- if (!VALID_STATUSES.includes(args.status)) return ERRORS.INVALID_STATUS(args.status);
7382
+ function validateArgs(args2) {
7383
+ if (!args2.plan || args2.plan === true) return ERRORS.MISSING_PLAN();
7384
+ if (!args2.task || args2.task === true) return ERRORS.MISSING_TASK();
7385
+ if (!args2.status || args2.status === true) return ERRORS.MISSING_STATUS();
7386
+ if (!VALID_STATUSES.includes(args2.status)) return ERRORS.INVALID_STATUS(args2.status);
7355
7387
  return null;
7356
7388
  }
7357
- function applyUpdate(plan, args, now) {
7358
- const task = plan.tasks.find((t) => t.id === args.task);
7389
+ function applyUpdate(plan, args2, now) {
7390
+ const task = plan.tasks.find((t) => t.id === args2.task);
7359
7391
  const oldStatus = task.status;
7360
- task.status = args.status;
7361
- if (args.artifacts && args.artifacts !== true) {
7362
- const incoming = args.artifacts.split(",").map((s) => s.trim()).filter(Boolean);
7392
+ task.status = args2.status;
7393
+ if (args2.artifacts && args2.artifacts !== true) {
7394
+ const incoming = args2.artifacts.split(",").map((s) => s.trim()).filter(Boolean);
7363
7395
  if (incoming.length) {
7364
7396
  if (!task.artifacts || typeof task.artifacts !== "object") {
7365
7397
  task.artifacts = { files: [] };
@@ -7376,48 +7408,110 @@ function applyUpdate(plan, args, now) {
7376
7408
  }
7377
7409
  }
7378
7410
  }
7379
- if (args.notes && args.notes !== true) {
7380
- task.notes = args.notes;
7411
+ if (args2.notes && args2.notes !== true) {
7412
+ task.notes = args2.notes;
7413
+ }
7414
+ if (args2.status === "done" && !task.completed_at) {
7415
+ task.completed_at = now;
7416
+ } else if (args2.status !== "done") {
7417
+ task.completed_at = null;
7418
+ }
7419
+ if (args2["deliverables-pointer"] && args2["deliverables-pointer"] !== true) {
7420
+ if (args2["deliverables-pointer"] !== "current") {
7421
+ return { error: ERRORS.INVALID_DELIVERABLES_POINTER(args2["deliverables-pointer"]) };
7422
+ }
7423
+ task.deliverables = { freshness: "current" };
7424
+ }
7425
+ if (args2["mark-deliverable-stale"] && args2["mark-deliverable-stale"] !== true) {
7426
+ const staleIds = args2["mark-deliverable-stale"].split(",").map((s) => s.trim()).filter(Boolean);
7427
+ for (const staleTaskId of staleIds) {
7428
+ const staleTask = plan.tasks.find((t) => t.id === staleTaskId);
7429
+ if (staleTask) {
7430
+ if (!staleTask.deliverables || typeof staleTask.deliverables !== "object") {
7431
+ staleTask.deliverables = { freshness: "stale" };
7432
+ } else {
7433
+ staleTask.deliverables.freshness = "stale";
7434
+ }
7435
+ }
7436
+ }
7381
7437
  }
7382
- task.completed_at = args.status === "done" ? now : null;
7383
7438
  plan.updated_at = now;
7384
- return { id: task.id, title: task.title || "", old_status: oldStatus, new_status: args.status };
7439
+ return { id: task.id, title: task.title || "", old_status: oldStatus, new_status: args2.status };
7385
7440
  }
7386
- function recomputeCurrentTask(plan, changedTaskId) {
7441
+ function recomputeCurrentTasks(plan, changedTaskId, projectList) {
7387
7442
  let warning = null;
7388
7443
  const changedTask = plan.tasks.find((t) => t.id === changedTaskId);
7389
7444
  const changedToTerminal = changedTask && TERMINAL_STATUSES.includes(changedTask.status);
7390
- const activeInProgress = plan.tasks.find(
7391
- (t) => t.status === "in_progress" && !(t.id === changedTaskId && changedToTerminal)
7392
- );
7393
- if (activeInProgress) {
7394
- plan.current_task = activeInProgress.id;
7395
- plan.status = "in_progress";
7396
- return { warning };
7397
- }
7398
- const doneIds = new Set(
7399
- plan.tasks.filter((t) => t.status === "done").map((t) => t.id)
7445
+ const priorActiveProjects = new Set(
7446
+ Object.keys(plan.current_tasks || {})
7400
7447
  );
7401
- const nextPending = plan.tasks.find(
7402
- (t) => t.status === "pending" && (t.depends_on || []).every((d) => doneIds.has(d))
7448
+ const resolvedIds = new Set(
7449
+ plan.tasks.filter((t) => t.status === "done" || t.status === "skipped").map((t) => t.id)
7403
7450
  );
7404
- if (nextPending) {
7405
- nextPending.status = "in_progress";
7406
- plan.current_task = nextPending.id;
7407
- plan.status = "in_progress";
7408
- return { warning };
7451
+ const projects = projectList && projectList.length > 0 ? projectList : loadSoleProject(findProjectRootFromPath(args.plan)) || ["default"];
7452
+ const currentTasks = {};
7453
+ for (const proj of projects) {
7454
+ const inProgressForProject = plan.tasks.filter(
7455
+ (t) => t.status === "in_progress" && getTaskProjects(t).includes(proj)
7456
+ );
7457
+ if (inProgressForProject.length > 0) {
7458
+ currentTasks[proj] = inProgressForProject[0].id;
7459
+ continue;
7460
+ }
7461
+ const nextPending = plan.tasks.find(
7462
+ (t) => t.status === "pending" && getTaskProjects(t).includes(proj) && (t.depends_on || []).every((d) => resolvedIds.has(d))
7463
+ );
7464
+ if (nextPending) {
7465
+ nextPending.status = "in_progress";
7466
+ currentTasks[proj] = nextPending.id;
7467
+ }
7409
7468
  }
7410
- if (plan.tasks.every((t) => t.status === "done")) {
7469
+ let switchNotification = null;
7470
+ if (changedToTerminal && changedTask) {
7471
+ const newActiveProjects = new Set(Object.keys(currentTasks));
7472
+ const newlyActive = [...newActiveProjects].filter((p) => !priorActiveProjects.has(p));
7473
+ if (newlyActive.length > 0) {
7474
+ switchNotification = {
7475
+ project_switch: {
7476
+ from: [...priorActiveProjects].filter((p) => !newActiveProjects.has(p)),
7477
+ to: newlyActive
7478
+ }
7479
+ };
7480
+ }
7481
+ }
7482
+ const allDone = plan.tasks.every((t) => t.status === "done");
7483
+ const anyInProgress = plan.tasks.some((t) => t.status === "in_progress");
7484
+ const anyPending = plan.tasks.some((t) => t.status === "pending");
7485
+ if (allDone) {
7411
7486
  plan.status = "done";
7412
- plan.current_task = null;
7413
- return { warning };
7487
+ plan.current_tasks = {};
7488
+ } else {
7489
+ plan.current_tasks = currentTasks;
7490
+ if (anyInProgress || Object.keys(currentTasks).length > 0) {
7491
+ plan.status = "in_progress";
7492
+ } else if (anyPending) {
7493
+ plan.status = "in_progress";
7494
+ warning = "All remaining tasks are blocked by dependencies; resolve a blocker before continuing.";
7495
+ }
7414
7496
  }
7415
- plan.current_task = null;
7416
- plan.status = "in_progress";
7417
- warning = "All remaining tasks are blocked by dependencies; resolve a blocker before continuing.";
7418
- return { warning };
7497
+ return { warning, project_switch: switchNotification };
7419
7498
  }
7420
- function validatePlan(plan) {
7499
+ function getTaskProjects(task) {
7500
+ if (Array.isArray(task.project) && task.project.length > 0) {
7501
+ return task.project;
7502
+ }
7503
+ return ["default"];
7504
+ }
7505
+ function deriveProjectList(tasks) {
7506
+ const projects = /* @__PURE__ */ new Set();
7507
+ for (const t of tasks) {
7508
+ for (const p of getTaskProjects(t)) {
7509
+ projects.add(p);
7510
+ }
7511
+ }
7512
+ return [...projects];
7513
+ }
7514
+ function validatePlan(plan, projectList) {
7421
7515
  const errors = [];
7422
7516
  const tasks = Array.isArray(plan.tasks) ? plan.tasks : [];
7423
7517
  const ids = tasks.map((t) => t.id);
@@ -7433,15 +7527,38 @@ function validatePlan(plan) {
7433
7527
  }
7434
7528
  }
7435
7529
  }
7436
- const cycle = findCycle(tasks);
7530
+ const cycle = findCycle(tasks, projectList);
7437
7531
  if (cycle) {
7438
7532
  errors.push(`Dependency cycle detected: ${cycle.join(" -> ")}`);
7439
7533
  }
7440
- const inProgress = tasks.filter((t) => t.status === "in_progress");
7441
- if (inProgress.length > 1) {
7442
- errors.push(
7443
- `More than one task is in_progress: ${inProgress.map((t) => t.id).join(", ")}`
7534
+ const projects = projectList && projectList.length > 0 ? projectList : loadSoleProject(findProjectRootFromPath(args.plan)) || ["default"];
7535
+ for (const proj of projects) {
7536
+ const inProgressForProject = tasks.filter(
7537
+ (t) => t.status === "in_progress" && getTaskProjects(t).includes(proj)
7444
7538
  );
7539
+ if (inProgressForProject.length > 1) {
7540
+ errors.push(
7541
+ `More than one task is in_progress for project "${proj}": ${inProgressForProject.map((t) => t.id).join(", ")}`
7542
+ );
7543
+ }
7544
+ }
7545
+ if (projectList && projectList.length > 0) {
7546
+ for (const t of tasks) {
7547
+ if (Array.isArray(t.project)) {
7548
+ for (const p of t.project) {
7549
+ if (!projectList.includes(p)) {
7550
+ errors.push(ERRORS.INVALID_TASK_PROJECT(t.id, p, projectList));
7551
+ }
7552
+ }
7553
+ }
7554
+ }
7555
+ }
7556
+ if (projectList && projectList.length > 0) {
7557
+ for (const p of projectList) {
7558
+ if (!PROJECT_NAME_RE.test(p)) {
7559
+ errors.push(ERRORS.INVALID_PROJECT_NAME(p));
7560
+ }
7561
+ }
7445
7562
  }
7446
7563
  for (const t of tasks) {
7447
7564
  if (!Array.isArray(t.acceptance) || t.acceptance.length === 0) {
@@ -7453,27 +7570,63 @@ function validatePlan(plan) {
7453
7570
  errors.push(`Task "${t.id}" is not done but has completed_at set`);
7454
7571
  }
7455
7572
  }
7573
+ for (const t of tasks) {
7574
+ if (t.deliverables && typeof t.deliverables === "object") {
7575
+ if (!VALID_FRESHNESS.includes(t.deliverables.freshness)) {
7576
+ errors.push(ERRORS.INVALID_FRESHNESS(t.id, t.deliverables.freshness));
7577
+ }
7578
+ }
7579
+ }
7456
7580
  if (plan.status === "done") {
7457
- if (plan.current_task != null) {
7458
- errors.push("plan.status is done but current_task is not null");
7459
- }
7460
- } else if (plan.current_task != null) {
7461
- const ct = tasks.find((t) => t.id === plan.current_task);
7462
- if (!ct) {
7463
- errors.push(`current_task "${plan.current_task}" does not reference a task`);
7464
- } else if (ct.status !== "pending" && ct.status !== "in_progress") {
7465
- errors.push(
7466
- `current_task "${plan.current_task}" has status "${ct.status}" (must be pending or in_progress)`
7467
- );
7581
+ if (plan.current_tasks && Object.keys(plan.current_tasks).length > 0) {
7582
+ errors.push("plan.status is done but current_tasks is not empty");
7583
+ }
7584
+ } else if (plan.current_tasks && typeof plan.current_tasks === "object") {
7585
+ for (const [proj, taskId] of Object.entries(plan.current_tasks)) {
7586
+ const ct = tasks.find((t) => t.id === taskId);
7587
+ if (!ct) {
7588
+ errors.push(`current_tasks["${proj}"] = "${taskId}" does not reference a task`);
7589
+ } else if (ct.status !== "pending" && ct.status !== "in_progress") {
7590
+ errors.push(
7591
+ `current_tasks["${proj}"] = "${taskId}" has status "${ct.status}" (must be pending or in_progress)`
7592
+ );
7593
+ }
7468
7594
  }
7469
7595
  }
7470
7596
  return errors;
7471
7597
  }
7472
- function findCycle(tasks) {
7598
+ function findCycle(tasks, projectList) {
7599
+ if (!projectList || projectList.length <= 1) {
7600
+ return findCycleInSubgraph(tasks, tasks.map((t) => t.id));
7601
+ }
7602
+ const taskMap = new Map(tasks.map((t) => [t.id, t]));
7603
+ for (const proj of projectList) {
7604
+ const idSet = new Set(
7605
+ tasks.filter((t) => getTaskProjects(t).includes(proj)).map((t) => t.id)
7606
+ );
7607
+ const queue = [...idSet];
7608
+ for (const id of queue) {
7609
+ for (const dep of taskMap.get(id)?.depends_on || []) {
7610
+ if (!idSet.has(dep)) {
7611
+ idSet.add(dep);
7612
+ queue.push(dep);
7613
+ }
7614
+ }
7615
+ }
7616
+ const cycle = findCycleInSubgraph(tasks, [...idSet]);
7617
+ if (cycle) return cycle;
7618
+ }
7619
+ return null;
7620
+ }
7621
+ function findCycleInSubgraph(tasks, taskIds) {
7622
+ const idSet = new Set(taskIds);
7473
7623
  const adj = /* @__PURE__ */ new Map();
7474
- for (const t of tasks) adj.set(t.id, t.depends_on || []);
7624
+ for (const t of tasks) {
7625
+ if (!idSet.has(t.id)) continue;
7626
+ adj.set(t.id, (t.depends_on || []).filter((d) => idSet.has(d)));
7627
+ }
7475
7628
  const WHITE = 0, GRAY = 1, BLACK = 2;
7476
- const color = new Map(tasks.map((t) => [t.id, WHITE]));
7629
+ const color = new Map(taskIds.map((id) => [id, WHITE]));
7477
7630
  const stack = [];
7478
7631
  function dfs(node) {
7479
7632
  color.set(node, GRAY);
@@ -7493,28 +7646,28 @@ function findCycle(tasks) {
7493
7646
  color.set(node, BLACK);
7494
7647
  return null;
7495
7648
  }
7496
- for (const t of tasks) {
7497
- if (color.get(t.id) === WHITE) {
7498
- const found = dfs(t.id);
7649
+ for (const id of taskIds) {
7650
+ if (color.get(id) === WHITE) {
7651
+ const found = dfs(id);
7499
7652
  if (found) return found;
7500
7653
  }
7501
7654
  }
7502
7655
  return null;
7503
7656
  }
7504
7657
  function main() {
7505
- const args = parseArgs(process.argv);
7506
- const argErr = validateArgs(args);
7658
+ const args2 = parseArgs(process.argv);
7659
+ const argErr = validateArgs(args2);
7507
7660
  if (argErr) {
7508
7661
  process.stderr.write(argErr + "\n");
7509
7662
  process.exit(1);
7510
7663
  }
7511
- if (!(0, import_node_fs.existsSync)(args.plan)) {
7512
- process.stderr.write(ERRORS.PLAN_NOT_FOUND(args.plan) + "\n");
7664
+ if (!(0, import_node_fs.existsSync)(args2.plan)) {
7665
+ process.stderr.write(ERRORS.PLAN_NOT_FOUND(args2.plan) + "\n");
7513
7666
  process.exit(1);
7514
7667
  }
7515
7668
  let plan;
7516
7669
  try {
7517
- plan = (0, import_yaml.parse)((0, import_node_fs.readFileSync)(args.plan, "utf-8"));
7670
+ plan = (0, import_yaml.parse)((0, import_node_fs.readFileSync)(args2.plan, "utf-8"));
7518
7671
  } catch (e) {
7519
7672
  process.stderr.write(ERRORS.PLAN_PARSE_FAILED(e.message) + "\n");
7520
7673
  process.exit(1);
@@ -7523,24 +7676,43 @@ function main() {
7523
7676
  process.stderr.write(ERRORS.PLAN_PARSE_FAILED("missing tasks[]") + "\n");
7524
7677
  process.exit(1);
7525
7678
  }
7526
- if (!plan.tasks.some((t) => t.id === args.task)) {
7679
+ if (!plan.tasks.some((t) => t.id === args2.task)) {
7527
7680
  process.stderr.write(
7528
- ERRORS.TASK_NOT_FOUND(args.task, plan.tasks.map((t) => t.id)) + "\n"
7681
+ ERRORS.TASK_NOT_FOUND(args2.task, plan.tasks.map((t) => t.id)) + "\n"
7529
7682
  );
7530
7683
  process.exit(1);
7531
7684
  }
7685
+ let projectList = null;
7686
+ if (args2.projects && args2.projects !== true) {
7687
+ projectList = args2.projects.split(",").map((s) => s.trim()).filter(Boolean);
7688
+ } else {
7689
+ projectList = deriveProjectList(plan.tasks);
7690
+ }
7691
+ if (plan.current_task != null && (!plan.current_tasks || typeof plan.current_tasks !== "object")) {
7692
+ plan.current_tasks = { default: plan.current_task };
7693
+ }
7694
+ if ("current_task" in plan) {
7695
+ delete plan.current_task;
7696
+ }
7697
+ if (!plan.current_tasks) {
7698
+ plan.current_tasks = {};
7699
+ }
7532
7700
  const now = (/* @__PURE__ */ new Date()).toISOString();
7533
- const taskChange = applyUpdate(plan, args, now);
7534
- const { warning } = recomputeCurrentTask(plan, args.task);
7535
- const validationErrors = validatePlan(plan);
7701
+ const taskChange = applyUpdate(plan, args2, now);
7702
+ if (taskChange.error) {
7703
+ process.stderr.write(taskChange.error + "\n");
7704
+ process.exit(1);
7705
+ }
7706
+ const { warning, project_switch: switchNotif } = recomputeCurrentTasks(plan, args2.task, projectList);
7707
+ const validationErrors = validatePlan(plan, projectList);
7536
7708
  if (validationErrors.length) {
7537
7709
  process.stderr.write(ERRORS.VALIDATION_FAILED(validationErrors) + "\n");
7538
7710
  process.exit(1);
7539
7711
  }
7540
- const tmpPath = args.plan + ".tmp";
7712
+ const tmpPath = args2.plan + ".tmp";
7541
7713
  try {
7542
- (0, import_node_fs.writeFileSync)(tmpPath, (0, import_yaml.stringify)(plan), "utf-8");
7543
- (0, import_node_fs.renameSync)(tmpPath, args.plan);
7714
+ (0, import_node_fs.writeFileSync)(tmpPath, (0, import_yaml.stringify)(plan, { lineWidth: 200 }), "utf-8");
7715
+ (0, import_node_fs.renameSync)(tmpPath, args2.plan);
7544
7716
  } catch (e) {
7545
7717
  try {
7546
7718
  if ((0, import_node_fs.existsSync)(tmpPath)) (0, import_node_fs.unlinkSync)(tmpPath);
@@ -7553,10 +7725,11 @@ function main() {
7553
7725
  const result = {
7554
7726
  ok: true,
7555
7727
  task: taskChange,
7556
- current_task: plan.current_task ?? null,
7728
+ current_tasks: plan.current_tasks,
7557
7729
  plan_status: plan.status,
7558
7730
  progress: { done: doneCount, total: plan.tasks.length },
7559
- warning
7731
+ ...warning ? { warning } : {},
7732
+ ...switchNotif ? switchNotif : {}
7560
7733
  };
7561
7734
  process.stdout.write(JSON.stringify(result) + "\n");
7562
7735
  }
@@ -7326,7 +7326,11 @@ var ERRORS = {
7326
7326
  NO_SESSION_YAML: () => "session.yaml not found. Run /mvt-init first to initialize the project.",
7327
7327
  SESSION_PARSE_FAILED: (detail) => `Failed to parse session.yaml: ${detail}. Check the file for syntax errors.`,
7328
7328
  SESSION_WRITE_FAILED: (detail) => `Failed to write session.yaml: ${detail}`,
7329
- CONFIG_LIMIT_INVALID: (key, val, min, max, fallback) => `Warning: config history_limits.${key} value "${val}" is invalid (must be integer ${min}-${max}). Using default ${fallback}.`
7329
+ CONFIG_LIMIT_INVALID: (key, val, min, max, fallback) => `Warning: config history_limits.${key} value "${val}" is invalid (must be integer ${min}-${max}). Using default ${fallback}.`,
7330
+ EPIC_ID_REQUIRED: () => "--new-epic requires --epic-id",
7331
+ CLOSE_NEW_EPIC_CONFLICT: () => "--close-epic and --new-epic are mutually exclusive",
7332
+ NO_ACTIVE_EPIC: (flag) => `${flag} requires an active epic (active_epic.id is empty)`,
7333
+ EPIC_ID_ORPHAN: () => "--epic-id (for sub-change) requires --new-change"
7330
7334
  };
7331
7335
  var DEFAULT_LIMITS = {
7332
7336
  history: 20,
@@ -7388,6 +7392,9 @@ function validate(args) {
7388
7392
  if (!args.skill) return ERRORS.MISSING_SKILL();
7389
7393
  if (!args.summary) return ERRORS.MISSING_SUMMARY();
7390
7394
  if (args["new-change"] && !args["change-id"]) return ERRORS.CHANGE_ID_REQUIRED();
7395
+ if (args["new-epic"] && !args["epic-id"]) return ERRORS.EPIC_ID_REQUIRED();
7396
+ if (args["close-epic"] && args["new-epic"]) return ERRORS.CLOSE_NEW_EPIC_CONFLICT();
7397
+ if (args["epic-id"] && !args["new-change"] && !args["new-epic"]) return ERRORS.EPIC_ID_ORPHAN();
7391
7398
  return null;
7392
7399
  }
7393
7400
  function main() {
@@ -7440,7 +7447,8 @@ function main() {
7440
7447
  title: session.active_change.title || "",
7441
7448
  plan_path: session.active_change.plan_path || "",
7442
7449
  status: "active",
7443
- updated_at: now
7450
+ updated_at: now,
7451
+ epic_id: session.active_change.epic_id || ""
7444
7452
  };
7445
7453
  if (existingIdx >= 0) {
7446
7454
  session.changes[existingIdx] = snapshotEntry;
@@ -7456,6 +7464,7 @@ function main() {
7456
7464
  session.active_change.title = args["new-change"];
7457
7465
  session.active_change.created_at = now;
7458
7466
  session.active_change.plan_path = "";
7467
+ session.active_change.epic_id = args["epic-id"] || "";
7459
7468
  }
7460
7469
  if (args["set-initialized"]) {
7461
7470
  session.session = session.session || {};
@@ -7482,7 +7491,8 @@ function main() {
7482
7491
  title: ac.title || "",
7483
7492
  plan_path: ac.plan_path || "",
7484
7493
  status: "active",
7485
- updated_at: now
7494
+ updated_at: now,
7495
+ epic_id: ac.epic_id || ""
7486
7496
  };
7487
7497
  if (existingIdx >= 0) {
7488
7498
  session.changes[existingIdx] = entry;
@@ -7508,7 +7518,8 @@ function main() {
7508
7518
  title: ac.title || "",
7509
7519
  plan_path: ac.plan_path || "",
7510
7520
  status: "done",
7511
- updated_at: now
7521
+ updated_at: now,
7522
+ epic_id: ac.epic_id || ""
7512
7523
  };
7513
7524
  if (existingIdx >= 0) {
7514
7525
  session.changes[existingIdx] = entry;
@@ -7526,7 +7537,8 @@ function main() {
7526
7537
  id: "",
7527
7538
  title: "",
7528
7539
  created_at: "",
7529
- plan_path: ""
7540
+ plan_path: "",
7541
+ epic_id: ""
7530
7542
  };
7531
7543
  }
7532
7544
  if (args["set-change-status"]) {
@@ -7551,9 +7563,75 @@ function main() {
7551
7563
  }
7552
7564
  }
7553
7565
  }
7566
+ if (args["new-epic"]) {
7567
+ session.active_epic = session.active_epic || {};
7568
+ if (session.active_epic.id) {
7569
+ session.epics = session.epics || [];
7570
+ const existingIdx = session.epics.findIndex(
7571
+ (e) => e.id === session.active_epic.id
7572
+ );
7573
+ const snapshotEntry = {
7574
+ id: session.active_epic.id,
7575
+ title: session.active_epic.title || "",
7576
+ epic_path: session.active_epic.epic_path || "",
7577
+ status: "active",
7578
+ updated_at: now
7579
+ };
7580
+ if (existingIdx >= 0) {
7581
+ session.epics[existingIdx] = snapshotEntry;
7582
+ } else {
7583
+ session.epics.push(snapshotEntry);
7584
+ }
7585
+ }
7586
+ session.active_epic.id = args["epic-id"];
7587
+ session.active_epic.title = args["new-epic"];
7588
+ session.active_epic.created_at = now;
7589
+ session.active_epic.epic_path = "";
7590
+ }
7591
+ if (args["set-epic-path"]) {
7592
+ session.active_epic = session.active_epic || {};
7593
+ if (!session.active_epic.id && !args["new-epic"]) {
7594
+ process.stderr.write(ERRORS.NO_ACTIVE_EPIC("--set-epic-path") + "\n");
7595
+ process.exit(1);
7596
+ }
7597
+ session.active_epic.epic_path = args["set-epic-path"];
7598
+ }
7599
+ if (args["set-epic-status"]) {
7600
+ session.active_epic = session.active_epic || {};
7601
+ if (!session.active_epic.id && !args["new-epic"]) {
7602
+ process.stderr.write(ERRORS.NO_ACTIVE_EPIC("--set-epic-status") + "\n");
7603
+ process.exit(1);
7604
+ }
7605
+ session.epics = session.epics || [];
7606
+ const epicIdx = session.epics.findIndex(
7607
+ (e) => e.id === session.active_epic.id
7608
+ );
7609
+ if (epicIdx >= 0) {
7610
+ session.epics[epicIdx].status = args["set-epic-status"];
7611
+ session.epics[epicIdx].updated_at = now;
7612
+ }
7613
+ }
7614
+ if (args["close-epic"]) {
7615
+ session.epics = session.epics || [];
7616
+ session.active_epic = session.active_epic || {};
7617
+ const aeId = session.active_epic.id;
7618
+ if (aeId) {
7619
+ const epicIdx = session.epics.findIndex((e) => e.id === aeId);
7620
+ if (epicIdx >= 0) {
7621
+ session.epics[epicIdx].status = "done";
7622
+ session.epics[epicIdx].updated_at = now;
7623
+ }
7624
+ }
7625
+ session.active_epic = {
7626
+ id: "",
7627
+ title: "",
7628
+ created_at: "",
7629
+ epic_path: ""
7630
+ };
7631
+ }
7554
7632
  const tmpPath = sessionPath + ".tmp";
7555
7633
  try {
7556
- (0, import_node_fs.writeFileSync)(tmpPath, (0, import_yaml.stringify)(session), "utf-8");
7634
+ (0, import_node_fs.writeFileSync)(tmpPath, (0, import_yaml.stringify)(session, { lineWidth: 200 }), "utf-8");
7557
7635
  (0, import_node_fs.renameSync)(tmpPath, sessionPath);
7558
7636
  } catch (e) {
7559
7637
  try {
@@ -0,0 +1,12 @@
1
+ export type PlatformId = "claude" | "qoder";
2
+ export interface PlatformDef {
3
+ id: PlatformId;
4
+ dir: string;
5
+ skillDir: string;
6
+ description: string;
7
+ }
8
+ export declare const PLATFORMS: PlatformDef[];
9
+ export declare const DEFAULT_PLATFORMS: PlatformId[];
10
+ export declare function getPlatformById(id: string): PlatformDef | undefined;
11
+ export declare function getPlatformsByIds(ids: PlatformId[]): PlatformDef[];
12
+ //# sourceMappingURL=platform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../src/types/platform.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE5C,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,UAAU,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,SAAS,EAAE,WAAW,EAalC,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,UAAU,EAAe,CAAC;AAE1D,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAEnE;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,WAAW,EAAE,CAIlE"}
@@ -0,0 +1,24 @@
1
+ export const PLATFORMS = [
2
+ {
3
+ id: "claude",
4
+ dir: ".claude",
5
+ skillDir: ".claude/skills",
6
+ description: "Claude Code / Claude Desktop / Github Copilot",
7
+ },
8
+ {
9
+ id: "qoder",
10
+ dir: ".qoder",
11
+ skillDir: ".qoder/skills",
12
+ description: "Qoder IDE",
13
+ },
14
+ ];
15
+ export const DEFAULT_PLATFORMS = ["claude"];
16
+ export function getPlatformById(id) {
17
+ return PLATFORMS.find((p) => p.id === id);
18
+ }
19
+ export function getPlatformsByIds(ids) {
20
+ return ids
21
+ .map((id) => getPlatformById(id))
22
+ .filter((p) => p !== undefined);
23
+ }
24
+ //# sourceMappingURL=platform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.js","sourceRoot":"","sources":["../../src/types/platform.ts"],"names":[],"mappings":"AASA,MAAM,CAAC,MAAM,SAAS,GAAkB;IACtC;QACE,EAAE,EAAE,QAAQ;QACZ,GAAG,EAAE,SAAS;QACd,QAAQ,EAAE,gBAAgB;QAC1B,WAAW,EAAE,+CAA+C;KAC7D;IACD;QACE,EAAE,EAAE,OAAO;QACX,GAAG,EAAE,QAAQ;QACb,QAAQ,EAAE,eAAe;QACzB,WAAW,EAAE,WAAW;KACzB;CACF,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAiB,CAAC,QAAQ,CAAC,CAAC;AAE1D,MAAM,UAAU,eAAe,CAAC,EAAU;IACxC,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,GAAiB;IACjD,OAAO,GAAG;SACP,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,eAAe,CAAC,EAAE,CAAC,CAAC;SAChC,MAAM,CAAC,CAAC,CAAC,EAAoB,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC;AACtD,CAAC"}