@hasna/todos 0.11.70 → 0.11.72

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
@@ -1011,7 +1011,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1011
1011
  this._exitCallback = (err) => {
1012
1012
  if (err.code !== "commander.executeSubCommandAsync") {
1013
1013
  throw err;
1014
- }
1014
+ } else {}
1015
1015
  };
1016
1016
  }
1017
1017
  return this;
@@ -3282,6 +3282,11 @@ var init_migrations = __esm(() => {
3282
3282
  CREATE INDEX IF NOT EXISTS idx_storage_tombstones_object ON storage_tombstones(object_type, object_id);
3283
3283
  CREATE INDEX IF NOT EXISTS idx_storage_tombstones_updated ON storage_tombstones(updated_at);
3284
3284
  INSERT OR IGNORE INTO _migrations (id) VALUES (63);
3285
+ `,
3286
+ `
3287
+ ALTER TABLE plans ADD COLUMN slug TEXT;
3288
+ CREATE INDEX IF NOT EXISTS idx_plans_slug ON plans(slug);
3289
+ INSERT OR IGNORE INTO _migrations (id) VALUES (64);
3285
3290
  `
3286
3291
  ];
3287
3292
  });
@@ -3305,6 +3310,29 @@ function runMigrations(db) {
3305
3310
  }
3306
3311
  ensureSchema(db);
3307
3312
  }
3313
+ function planSlugBase(value) {
3314
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "plan";
3315
+ }
3316
+ function backfillPlanSlugs(db) {
3317
+ try {
3318
+ const rows = db.query("SELECT id, project_id, name, slug FROM plans ORDER BY created_at ASC, id ASC").all();
3319
+ const used = new Set;
3320
+ for (const row of rows) {
3321
+ const scope = row.project_id ?? "__global__";
3322
+ const base = planSlugBase(row.slug || row.name);
3323
+ let candidate = base;
3324
+ let suffix = 2;
3325
+ while (used.has(`${scope}:${candidate}`)) {
3326
+ candidate = `${base}-${suffix}`;
3327
+ suffix += 1;
3328
+ }
3329
+ used.add(`${scope}:${candidate}`);
3330
+ if (row.slug !== candidate) {
3331
+ db.run("UPDATE plans SET slug = ? WHERE id = ?", [candidate, row.id]);
3332
+ }
3333
+ }
3334
+ } catch {}
3335
+ }
3308
3336
  function ensureSchema(db) {
3309
3337
  const ensureColumn = (table, column, type) => {
3310
3338
  try {
@@ -3354,7 +3382,8 @@ function ensureSchema(db) {
3354
3382
  )`);
3355
3383
  ensureTable("plans", `
3356
3384
  CREATE TABLE plans (
3357
- id TEXT PRIMARY KEY, project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
3385
+ id TEXT PRIMARY KEY, slug TEXT,
3386
+ project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
3358
3387
  task_list_id TEXT, agent_id TEXT,
3359
3388
  name TEXT NOT NULL, description TEXT,
3360
3389
  status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'archived')),
@@ -3869,8 +3898,10 @@ function ensureSchema(db) {
3869
3898
  ensureColumn("agents", "org_id", "TEXT");
3870
3899
  ensureColumn("agents", "capabilities", "TEXT DEFAULT '[]'");
3871
3900
  ensureColumn("projects", "org_id", "TEXT");
3901
+ ensureColumn("plans", "slug", "TEXT");
3872
3902
  ensureColumn("plans", "task_list_id", "TEXT");
3873
3903
  ensureColumn("plans", "agent_id", "TEXT");
3904
+ backfillPlanSlugs(db);
3874
3905
  ensureColumn("task_templates", "variables", "TEXT DEFAULT '[]'");
3875
3906
  ensureColumn("task_templates", "version", "INTEGER NOT NULL DEFAULT 1");
3876
3907
  ensureColumn("template_tasks", "condition", "TEXT");
@@ -3985,6 +4016,8 @@ function ensureSchema(db) {
3985
4016
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name)");
3986
4017
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_id)");
3987
4018
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status)");
4019
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_slug ON plans(slug)");
4020
+ ensureIndex("CREATE UNIQUE INDEX IF NOT EXISTS idx_plans_scope_slug ON plans(COALESCE(project_id, ''), slug) WHERE slug IS NOT NULL");
3988
4021
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_task_list ON plans(task_list_id)");
3989
4022
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_agent ON plans(agent_id)");
3990
4023
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_task ON task_history(task_id)");
@@ -4838,6 +4871,9 @@ function clearExpiredLocks(db) {
4838
4871
  const cutoff = lockExpiryCutoff();
4839
4872
  db.run("UPDATE tasks SET locked_by = NULL, locked_at = NULL WHERE locked_at IS NOT NULL AND locked_at < ?", [cutoff]);
4840
4873
  }
4874
+ function slugifyRef(value) {
4875
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
4876
+ }
4841
4877
  function resolvePartialId(db, table, partialId) {
4842
4878
  if (!ALLOWED_TABLES.has(table)) {
4843
4879
  throw new Error(`Invalid table name: ${table}`);
@@ -4864,6 +4900,16 @@ function resolvePartialId(db, table, partialId) {
4864
4900
  if (slugRow)
4865
4901
  return slugRow.id;
4866
4902
  }
4903
+ if (table === "plans") {
4904
+ const slug = slugifyRef(partialId);
4905
+ if (slug) {
4906
+ const slugRows = db.query("SELECT id FROM plans WHERE slug = ?").all(slug);
4907
+ if (slugRows.length === 1)
4908
+ return slugRows[0].id;
4909
+ if (slugRows.length > 1)
4910
+ return null;
4911
+ }
4912
+ }
4867
4913
  if (table === "projects") {
4868
4914
  const nameRow = db.query("SELECT id FROM projects WHERE lower(name) = ?").get(partialId.toLowerCase());
4869
4915
  if (nameRow)
@@ -7169,8 +7215,6 @@ function routeEnabledForTask(task, taskList) {
7169
7215
  const explicit = booleanField(task.metadata.route_enabled);
7170
7216
  if (explicit !== undefined)
7171
7217
  return explicit;
7172
- if (task.tags.includes("auto:route") || task.tags.includes("route:enabled"))
7173
- return true;
7174
7218
  const taskListDefault = taskList ? booleanField(taskList.metadata.route_enabled) : undefined;
7175
7219
  if (taskListDefault !== undefined)
7176
7220
  return taskListDefault;
@@ -7192,8 +7236,26 @@ function workflowPointersFromMetadata(metadata) {
7192
7236
  function compactWorkflowPointers(pointers) {
7193
7237
  return Object.fromEntries(Object.entries(pointers).filter(([, value]) => typeof value === "string" && value.length > 0));
7194
7238
  }
7195
- function classifyProjectKind(path) {
7196
- return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
7239
+ function metadataStringField(record, keys) {
7240
+ if (!record)
7241
+ return;
7242
+ for (const key of keys) {
7243
+ const value = record[key];
7244
+ if (typeof value === "string" && value.trim())
7245
+ return value.trim();
7246
+ }
7247
+ return;
7248
+ }
7249
+ function projectKindFromMetadata(...records) {
7250
+ for (const record of records) {
7251
+ const value = metadataStringField(record ?? undefined, ["project_kind", "projectKind", "source_kind", "sourceKind"]);
7252
+ if (value)
7253
+ return value;
7254
+ }
7255
+ return null;
7256
+ }
7257
+ function classifyProjectKind(_path, metadata) {
7258
+ return projectKindFromMetadata(metadata);
7197
7259
  }
7198
7260
  function isWorktreePath(path) {
7199
7261
  return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
@@ -7266,7 +7328,6 @@ function taskEventMetadata(task) {
7266
7328
  metadata.project_canonical_path = projectPath;
7267
7329
  }
7268
7330
  if (projectPath) {
7269
- metadata.project_kind = classifyProjectKind(projectPath);
7270
7331
  metadata.project_is_worktree = isWorktreePath(projectPath);
7271
7332
  metadata.working_dir = task.working_dir ?? projectPath;
7272
7333
  }
@@ -7278,6 +7339,10 @@ function taskEventMetadata(task) {
7278
7339
  metadata.task_list_project_id = taskList.project_id;
7279
7340
  metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
7280
7341
  }
7342
+ const projectKind = projectKindFromMetadata(task.metadata, taskList?.metadata);
7343
+ if (projectKind) {
7344
+ metadata.project_kind = classifyProjectKind(projectPath ?? "", { project_kind: projectKind });
7345
+ }
7281
7346
  const routeEnabled = routeEnabledForTask(task, taskList);
7282
7347
  if (routeEnabled !== undefined) {
7283
7348
  metadata.route_enabled = routeEnabled;
@@ -10333,15 +10398,76 @@ var init_task_relations = __esm(() => {
10333
10398
  });
10334
10399
 
10335
10400
  // src/db/plans.ts
10401
+ function planSlugBase2(value) {
10402
+ return slugify(value) || "plan";
10403
+ }
10404
+ function normalizePlanSlug(value) {
10405
+ const slug = slugify(value);
10406
+ if (!slug)
10407
+ throw new Error("Invalid plan slug");
10408
+ return slug;
10409
+ }
10410
+ function plansBySlug(slug, db, projectId) {
10411
+ if (projectId !== undefined) {
10412
+ if (projectId === null) {
10413
+ return db.query("SELECT * FROM plans WHERE slug = ? AND project_id IS NULL ORDER BY created_at ASC, id ASC").all(slug);
10414
+ }
10415
+ return db.query("SELECT * FROM plans WHERE slug = ? AND project_id = ? ORDER BY created_at ASC, id ASC").all(slug, projectId);
10416
+ }
10417
+ return db.query("SELECT * FROM plans WHERE slug = ? ORDER BY created_at ASC, id ASC").all(slug);
10418
+ }
10419
+ function planSlugExists(slug, projectId, db, excludeId) {
10420
+ const rows = plansBySlug(slug, db, projectId);
10421
+ return rows.some((plan) => plan.id !== excludeId);
10422
+ }
10423
+ function nextPlanSlug(base, projectId, db, excludeId) {
10424
+ let candidate = base;
10425
+ let suffix = 2;
10426
+ while (planSlugExists(candidate, projectId, db, excludeId)) {
10427
+ candidate = `${base}-${suffix}`;
10428
+ suffix += 1;
10429
+ }
10430
+ return candidate;
10431
+ }
10432
+ function resolveCreateSlug(input, projectId, db) {
10433
+ if (input.slug !== undefined) {
10434
+ const slug = normalizePlanSlug(input.slug);
10435
+ if (planSlugExists(slug, projectId, db)) {
10436
+ throw new Error(`Plan slug already exists in this scope: ${slug}`);
10437
+ }
10438
+ return slug;
10439
+ }
10440
+ return nextPlanSlug(planSlugBase2(input.name), projectId, db);
10441
+ }
10442
+ function resolvePlanRefDetailed(ref, db, projectId) {
10443
+ const d = db || getDatabase();
10444
+ const byId = d.query("SELECT * FROM plans WHERE id = ? OR id LIKE ? ORDER BY id").all(ref, `${ref}%`);
10445
+ if (byId.length === 1)
10446
+ return { id: byId[0].id, reason: "id", matches: byId };
10447
+ if (byId.length > 1)
10448
+ return { id: null, reason: "ambiguous", matches: byId };
10449
+ const slug = slugify(ref);
10450
+ if (!slug)
10451
+ return { id: null, reason: "not_found", matches: [] };
10452
+ const bySlug = plansBySlug(slug, d, projectId);
10453
+ if (bySlug.length === 1)
10454
+ return { id: bySlug[0].id, reason: "slug", matches: bySlug };
10455
+ if (bySlug.length > 1)
10456
+ return { id: null, reason: "ambiguous", matches: bySlug };
10457
+ return { id: null, reason: "not_found", matches: [] };
10458
+ }
10336
10459
  function createPlan(input, db) {
10337
10460
  const d = db || getDatabase();
10338
10461
  const id = uuid();
10339
10462
  const timestamp = now();
10463
+ const projectId = input.project_id || null;
10464
+ const slug = resolveCreateSlug(input, projectId, d);
10340
10465
  const machineId = currentStorageMachineId(d);
10341
- d.run(`INSERT INTO plans (id, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at, machine_id)
10342
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
10466
+ d.run(`INSERT INTO plans (id, slug, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at, machine_id)
10467
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
10343
10468
  id,
10344
- input.project_id || null,
10469
+ slug,
10470
+ projectId,
10345
10471
  input.task_list_id || null,
10346
10472
  input.agent_id || null,
10347
10473
  input.name,
@@ -10376,6 +10502,14 @@ function updatePlan(id, input, db) {
10376
10502
  sets.push("name = ?");
10377
10503
  params.push(input.name);
10378
10504
  }
10505
+ if (input.slug !== undefined) {
10506
+ const slug = normalizePlanSlug(input.slug);
10507
+ if (planSlugExists(slug, plan.project_id, d, id)) {
10508
+ throw new Error(`Plan slug already exists in this scope: ${slug}`);
10509
+ }
10510
+ sets.push("slug = ?");
10511
+ params.push(slug);
10512
+ }
10379
10513
  if (input.description !== undefined) {
10380
10514
  sets.push("description = ?");
10381
10515
  params.push(input.description);
@@ -10420,6 +10554,7 @@ var init_plans = __esm(() => {
10420
10554
  init_event_emission_safety();
10421
10555
  init_event_hooks();
10422
10556
  init_database();
10557
+ init_projects();
10423
10558
  init_storage_tombstones();
10424
10559
  });
10425
10560
 
@@ -12626,11 +12761,6 @@ __export(exports_task_routing, {
12626
12761
  setTaskWorkflowPointers: () => setTaskWorkflowPointers,
12627
12762
  getTaskRouteState: () => getTaskRouteState
12628
12763
  });
12629
- function classifyProjectKind2(path) {
12630
- if (!path)
12631
- return null;
12632
- return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
12633
- }
12634
12764
  function machineLocalPath(project, db) {
12635
12765
  const machineId = process.env["TODOS_MACHINE_ID"];
12636
12766
  if (!machineId)
@@ -12678,6 +12808,7 @@ function getTaskRouteState(taskOrId, db) {
12678
12808
  const automation = routingAutomationMetadata(task, taskList) ?? {};
12679
12809
  const routeEnabled = routeEnabledForTask(task, taskList) === true;
12680
12810
  const tagOptIn = task.tags.includes("auto:route") || task.tags.includes("route:enabled");
12811
+ const projectKind = projectKindFromMetadata(task.metadata, taskList?.metadata);
12681
12812
  const locked = Boolean(task.locked_by && !isLockExpired(task.locked_at));
12682
12813
  const blockers = getBlockingDeps(task.id, d);
12683
12814
  const blocked = blockers.length > 0;
@@ -12740,7 +12871,7 @@ function getTaskRouteState(taskOrId, db) {
12740
12871
  project_id: project?.id ?? task.project_id,
12741
12872
  project_path: projectPath,
12742
12873
  working_dir: task.working_dir ?? projectPath,
12743
- project_kind: classifyProjectKind2(projectPath),
12874
+ project_kind: projectKind,
12744
12875
  task_list_id: taskList?.id ?? task.task_list_id,
12745
12876
  task_list_slug: taskList?.slug ?? null,
12746
12877
  task_list_name: taskList?.name ?? null,
@@ -13718,6 +13849,9 @@ function projectSlugMatches(project, ref) {
13718
13849
  const normalized = slugify(ref);
13719
13850
  return Boolean(normalized) && (project.task_list_id === normalized || slugify(project.name) === normalized);
13720
13851
  }
13852
+ function planArtifactSlug(plan) {
13853
+ return slugify(plan.slug || plan.name) || "plan";
13854
+ }
13721
13855
  function resolvePlanArtifactProject(input) {
13722
13856
  const db = input.db || getDatabase();
13723
13857
  const ref = input.project_id || input.project_ref;
@@ -13743,11 +13877,24 @@ function resolvePlanArtifactPaths(input) {
13743
13877
  const projectRoot = resolve10(project.path);
13744
13878
  const directory = join7(projectRoot, ".hasna", "todos", "plans", projectId);
13745
13879
  const planId = input.plan_id ? assertSafePathSegment(input.plan_id, "plan id") : null;
13880
+ const planSlug = input.plan_slug ? assertSafePathSegment(slugify(input.plan_slug), "plan slug") : null;
13881
+ const fileName = planId ? planSlug ? `${planSlug}--${planId.slice(0, 8)}.md` : `${planId}.md` : null;
13746
13882
  return {
13747
13883
  project_id: project.id,
13748
13884
  project_root: projectRoot,
13749
13885
  directory,
13750
- file_path: planId ? join7(directory, `${planId}.md`) : directory
13886
+ file_path: fileName ? join7(directory, fileName) : directory
13887
+ };
13888
+ }
13889
+ function resolvePlanArtifactCandidatePaths(plan, db) {
13890
+ return {
13891
+ primary: resolvePlanArtifactPaths({
13892
+ project_id: plan.project_id,
13893
+ plan_id: plan.id,
13894
+ plan_slug: planArtifactSlug(plan),
13895
+ db
13896
+ }),
13897
+ legacy: resolvePlanArtifactPaths({ project_id: plan.project_id, plan_id: plan.id, db })
13751
13898
  };
13752
13899
  }
13753
13900
  function buildPlanArtifactSnapshot(plan, tasks = [], artifactUpdatedAt = new Date().toISOString()) {
@@ -13764,6 +13911,7 @@ function buildPlanArtifactSnapshot(plan, tasks = [], artifactUpdatedAt = new Dat
13764
13911
  metadata: {
13765
13912
  schema: PLAN_MARKDOWN_SCHEMA,
13766
13913
  plan_id: plan.id,
13914
+ plan_slug: plan.slug ?? null,
13767
13915
  project_id: plan.project_id,
13768
13916
  task_list_id: plan.task_list_id ?? null,
13769
13917
  agent_id: plan.agent_id ?? null,
@@ -13803,6 +13951,7 @@ function renderPlanArtifactMarkdown(snapshot) {
13803
13951
  "---",
13804
13952
  `schema: ${frontmatterScalar(metadata.schema)}`,
13805
13953
  `plan_id: ${frontmatterScalar(metadata.plan_id)}`,
13954
+ `plan_slug: ${frontmatterScalar(metadata.plan_slug)}`,
13806
13955
  `project_id: ${frontmatterScalar(metadata.project_id)}`,
13807
13956
  `task_list_id: ${frontmatterScalar(metadata.task_list_id)}`,
13808
13957
  `agent_id: ${frontmatterScalar(metadata.agent_id)}`,
@@ -13848,6 +13997,7 @@ function parsePlanArtifactMarkdown(markdown) {
13848
13997
  metadata: {
13849
13998
  schema: PLAN_MARKDOWN_SCHEMA,
13850
13999
  plan_id: rawMetadata.plan_id,
14000
+ plan_slug: rawMetadata.plan_slug ?? null,
13851
14001
  project_id: rawMetadata.project_id,
13852
14002
  task_list_id: rawMetadata.task_list_id ?? null,
13853
14003
  agent_id: rawMetadata.agent_id ?? null,
@@ -13888,7 +14038,7 @@ function writePlanArtifact(plan, db) {
13888
14038
  return null;
13889
14039
  const d = db || getDatabase();
13890
14040
  const tasks = listTasks({ plan_id: plan.id, include_archived: true }, d);
13891
- const paths = resolvePlanArtifactPaths({ project_id: plan.project_id, plan_id: plan.id, db: d });
14041
+ const paths = resolvePlanArtifactCandidatePaths(plan, d).primary;
13892
14042
  const snapshot = buildPlanArtifactSnapshot(plan, tasks);
13893
14043
  mkdirSync5(paths.directory, { recursive: true });
13894
14044
  writeFileSync3(paths.file_path, renderPlanArtifactMarkdown(snapshot), "utf8");
@@ -13898,12 +14048,13 @@ function readPlanArtifact(plan, db) {
13898
14048
  if (!plan.project_id)
13899
14049
  return null;
13900
14050
  const d = db || getDatabase();
13901
- const paths = resolvePlanArtifactPaths({ project_id: plan.project_id, plan_id: plan.id, db: d });
13902
- if (!existsSync8(paths.file_path))
14051
+ const paths = resolvePlanArtifactCandidatePaths(plan, d);
14052
+ const path = existsSync8(paths.primary.file_path) ? paths.primary.file_path : existsSync8(paths.legacy.file_path) ? paths.legacy.file_path : null;
14053
+ if (!path)
13903
14054
  return null;
13904
- const markdown = readFileSync4(paths.file_path, "utf8");
14055
+ const markdown = readFileSync4(path, "utf8");
13905
14056
  return {
13906
- path: paths.file_path,
14057
+ path,
13907
14058
  markdown,
13908
14059
  ...parsePlanArtifactMarkdown(markdown)
13909
14060
  };
@@ -13912,10 +14063,11 @@ function inspectPlanArtifact(plan, db) {
13912
14063
  if (!plan.project_id)
13913
14064
  return null;
13914
14065
  const d = db || getDatabase();
13915
- const paths = resolvePlanArtifactPaths({ project_id: plan.project_id, plan_id: plan.id, db: d });
13916
- if (!existsSync8(paths.file_path)) {
14066
+ const paths = resolvePlanArtifactCandidatePaths(plan, d);
14067
+ const path = existsSync8(paths.primary.file_path) ? paths.primary.file_path : existsSync8(paths.legacy.file_path) ? paths.legacy.file_path : null;
14068
+ if (!path) {
13917
14069
  return {
13918
- path: paths.file_path,
14070
+ path: paths.primary.file_path,
13919
14071
  exists: false,
13920
14072
  parse_error: null,
13921
14073
  metadata: null,
@@ -13924,9 +14076,9 @@ function inspectPlanArtifact(plan, db) {
13924
14076
  };
13925
14077
  }
13926
14078
  try {
13927
- const artifact = parsePlanArtifactMarkdown(readFileSync4(paths.file_path, "utf8"));
14079
+ const artifact = parsePlanArtifactMarkdown(readFileSync4(path, "utf8"));
13928
14080
  return {
13929
- path: paths.file_path,
14081
+ path,
13930
14082
  exists: true,
13931
14083
  parse_error: null,
13932
14084
  metadata: artifact.metadata,
@@ -13935,7 +14087,7 @@ function inspectPlanArtifact(plan, db) {
13935
14087
  };
13936
14088
  } catch (error) {
13937
14089
  return {
13938
- path: paths.file_path,
14090
+ path,
13939
14091
  exists: true,
13940
14092
  parse_error: error instanceof Error ? error.message : String(error),
13941
14093
  metadata: null,
@@ -13947,6 +14099,9 @@ function inspectPlanArtifact(plan, db) {
13947
14099
  function comparePlanArtifact(plan, artifact, tasks) {
13948
14100
  const conflicts = [];
13949
14101
  compare("plan_id", plan.id, artifact.metadata.plan_id, conflicts);
14102
+ if (artifact.metadata.plan_slug !== null) {
14103
+ compare("plan_slug", plan.slug ?? null, artifact.metadata.plan_slug, conflicts);
14104
+ }
13950
14105
  compare("project_id", plan.project_id ?? null, artifact.metadata.project_id, conflicts);
13951
14106
  compare("name", plan.name, artifact.metadata.name, conflicts);
13952
14107
  compare("status", plan.status, artifact.metadata.status, conflicts);
@@ -14316,22 +14471,44 @@ __export(exports_plan_template_commands, {
14316
14471
  registerPlanTemplateCommands: () => registerPlanTemplateCommands
14317
14472
  });
14318
14473
  import chalk3 from "chalk";
14474
+ function resolvePlanCliRef(ref, projectId) {
14475
+ const db = getDatabase();
14476
+ const resolved = resolvePlanRefDetailed(ref, db, projectId);
14477
+ if (resolved.id)
14478
+ return resolved.id;
14479
+ if (resolved.reason === "ambiguous") {
14480
+ console.error(chalk3.red(`Ambiguous plan reference: ${ref}`));
14481
+ if (resolved.matches.length > 0) {
14482
+ console.error(chalk3.dim(`Matches: ${resolved.matches.map((plan) => `${plan.slug ?? plan.name} (${plan.id.slice(0, 8)})`).join(", ")}`));
14483
+ }
14484
+ } else {
14485
+ console.error(chalk3.red(`Could not resolve plan ID or slug: ${ref}`));
14486
+ }
14487
+ process.exit(1);
14488
+ }
14319
14489
  function registerPlanTemplateCommands(program2) {
14320
- program2.command("plans").description("List and manage plans").option("--add <name>", "Create a plan").option("-d, --description <text>", "Plan description (with --add)").option("--show <id>", "Show plan details with its tasks").option("--artifact <id>", "Show local Markdown artifact diagnostics for a plan").option("--write-artifacts", "Write local Markdown artifacts for all project-scoped plans in scope").option("--delete <id>", "Delete a plan").option("--complete <id>", "Mark a plan as completed").action((opts) => {
14490
+ program2.command("plans").description("List and manage plans").option("--add <name>", "Create a plan").option("--slug <slug>", "Readable plan slug (with --add)").option("-d, --description <text>", "Plan description (with --add)").option("--show <id-or-slug>", "Show plan details with its tasks").option("--artifact <id-or-slug>", "Show local Markdown artifact diagnostics for a plan").option("--write-artifacts", "Write local Markdown artifacts for all project-scoped plans in scope").option("--delete <id>", "Delete a plan").option("--complete <id>", "Mark a plan as completed").action((opts) => {
14321
14491
  const globalOpts = program2.opts();
14322
14492
  const projectId = autoProject(globalOpts);
14323
14493
  if (opts.add) {
14324
- const plan = createPlan({
14325
- name: opts.add,
14326
- description: opts.description,
14327
- project_id: projectId
14328
- });
14494
+ let plan;
14495
+ try {
14496
+ plan = createPlan({
14497
+ name: opts.add,
14498
+ slug: opts.slug,
14499
+ description: opts.description,
14500
+ project_id: projectId
14501
+ });
14502
+ } catch (error) {
14503
+ handleError(error);
14504
+ }
14329
14505
  const artifact = writePlanArtifact(plan);
14330
14506
  if (globalOpts.json) {
14331
14507
  output(plan, true);
14332
14508
  } else {
14333
14509
  console.log(chalk3.green("Plan created:"));
14334
14510
  console.log(`${chalk3.dim(plan.id.slice(0, 8))} ${chalk3.bold(plan.name)} ${chalk3.cyan(`[${plan.status}]`)}`);
14511
+ console.log(`${chalk3.dim("Slug:")} ${plan.slug}`);
14335
14512
  if (artifact)
14336
14513
  console.log(`${chalk3.dim("Artifact:")} ${artifact.path}`);
14337
14514
  }
@@ -14339,11 +14516,7 @@ function registerPlanTemplateCommands(program2) {
14339
14516
  }
14340
14517
  if (opts.artifact) {
14341
14518
  const db = getDatabase();
14342
- const resolvedId = resolvePartialId(db, "plans", opts.artifact);
14343
- if (!resolvedId) {
14344
- console.error(chalk3.red(`Could not resolve plan ID: ${opts.artifact}`));
14345
- process.exit(1);
14346
- }
14519
+ const resolvedId = resolvePlanCliRef(opts.artifact, projectId);
14347
14520
  const plan = getPlan(resolvedId);
14348
14521
  if (!plan) {
14349
14522
  console.error(chalk3.red(`Plan not found: ${opts.artifact}`));
@@ -14396,11 +14569,7 @@ function registerPlanTemplateCommands(program2) {
14396
14569
  }
14397
14570
  if (opts.show) {
14398
14571
  const db = getDatabase();
14399
- const resolvedId = resolvePartialId(db, "plans", opts.show);
14400
- if (!resolvedId) {
14401
- console.error(chalk3.red(`Could not resolve plan ID: ${opts.show}`));
14402
- process.exit(1);
14403
- }
14572
+ const resolvedId = resolvePlanCliRef(opts.show, projectId);
14404
14573
  const plan = getPlan(resolvedId);
14405
14574
  if (!plan) {
14406
14575
  console.error(chalk3.red(`Plan not found: ${opts.show}`));
@@ -14425,6 +14594,8 @@ function registerPlanTemplateCommands(program2) {
14425
14594
  console.log(chalk3.bold(`Plan Details:
14426
14595
  `));
14427
14596
  console.log(` ${chalk3.dim("ID:")} ${plan.id}`);
14597
+ if (plan.slug)
14598
+ console.log(` ${chalk3.dim("Slug:")} ${plan.slug}`);
14428
14599
  console.log(` ${chalk3.dim("Name:")} ${plan.name}`);
14429
14600
  console.log(` ${chalk3.dim("Status:")} ${chalk3.cyan(plan.status)}`);
14430
14601
  if (plan.description)
@@ -14447,12 +14618,7 @@ function registerPlanTemplateCommands(program2) {
14447
14618
  return;
14448
14619
  }
14449
14620
  if (opts.delete) {
14450
- const db = getDatabase();
14451
- const resolvedId = resolvePartialId(db, "plans", opts.delete);
14452
- if (!resolvedId) {
14453
- console.error(chalk3.red(`Could not resolve plan ID: ${opts.delete}`));
14454
- process.exit(1);
14455
- }
14621
+ const resolvedId = resolvePlanCliRef(opts.delete, projectId);
14456
14622
  const deleted = deletePlan(resolvedId);
14457
14623
  if (globalOpts.json) {
14458
14624
  output({ deleted }, true);
@@ -14465,12 +14631,7 @@ function registerPlanTemplateCommands(program2) {
14465
14631
  return;
14466
14632
  }
14467
14633
  if (opts.complete) {
14468
- const db = getDatabase();
14469
- const resolvedId = resolvePartialId(db, "plans", opts.complete);
14470
- if (!resolvedId) {
14471
- console.error(chalk3.red(`Could not resolve plan ID: ${opts.complete}`));
14472
- process.exit(1);
14473
- }
14634
+ const resolvedId = resolvePlanCliRef(opts.complete, projectId);
14474
14635
  try {
14475
14636
  const plan = updatePlan(resolvedId, { status: "completed" });
14476
14637
  const artifact = writePlanArtifact(plan);
@@ -14500,7 +14661,8 @@ function registerPlanTemplateCommands(program2) {
14500
14661
  `));
14501
14662
  for (const p of plans) {
14502
14663
  const desc = p.description ? chalk3.dim(` - ${p.description}`) : "";
14503
- console.log(`${chalk3.dim(p.id.slice(0, 8))} ${chalk3.bold(p.name)} ${chalk3.cyan(`[${p.status}]`)}${desc}`);
14664
+ const slug = p.slug ? chalk3.dim(` ${p.slug}`) : "";
14665
+ console.log(`${chalk3.dim(p.id.slice(0, 8))}${slug} ${chalk3.bold(p.name)} ${chalk3.cyan(`[${p.status}]`)}${desc}`);
14504
14666
  }
14505
14667
  });
14506
14668
  program2.command("templates").description("List and manage task templates").option("--add <name>", "Create a template").option("--title <pattern>", "Title pattern (with --add)").option("-d, --description <text>", "Default description").option("-p, --priority <level>", "Default priority").option("-t, --tags <tags>", "Default tags (comma-separated)").option("--delete <id>", "Delete a template").option("--update <id>", "Update a template").option("--use <id>", "Create a task from a template").option("--var <vars...>", "Variable substitutions: key=value (e.g. --var feature=login)").action(async (opts) => {
@@ -22074,6 +22236,36 @@ function prepareValue(column, value) {
22074
22236
  return JSON.stringify(value ?? (column === "tags" || column === "files_changed" ? [] : {}));
22075
22237
  return value === undefined ? null : value;
22076
22238
  }
22239
+ function slugifyPlanValue(value) {
22240
+ return typeof value === "string" ? value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") : "";
22241
+ }
22242
+ function planSlugBase3(plan) {
22243
+ return slugifyPlanValue(plan.slug) || slugifyPlanValue(plan.name) || "plan";
22244
+ }
22245
+ function planSlugScope(projectId) {
22246
+ return typeof projectId === "string" && projectId ? projectId : "__global__";
22247
+ }
22248
+ function planSlugKey(projectId, slug) {
22249
+ return `${planSlugScope(projectId)}:${slug}`;
22250
+ }
22251
+ function normalizeBridgePlanSlugs(plans, db) {
22252
+ const existingRows = db.query("SELECT id, project_id, slug FROM plans WHERE slug IS NOT NULL").all();
22253
+ const existingIds = new Set(existingRows.map((row) => row.id));
22254
+ const used = new Set(existingRows.filter((row) => row.slug).map((row) => planSlugKey(row.project_id, row.slug)));
22255
+ return plans.map((plan) => {
22256
+ if (existingIds.has(plan.id))
22257
+ return plan;
22258
+ const base = planSlugBase3(plan);
22259
+ let candidate = base;
22260
+ let suffix = 2;
22261
+ while (used.has(planSlugKey(plan.project_id, candidate))) {
22262
+ candidate = `${base}-${suffix}`;
22263
+ suffix += 1;
22264
+ }
22265
+ used.add(planSlugKey(plan.project_id, candidate));
22266
+ return { ...plan, slug: candidate };
22267
+ });
22268
+ }
22077
22269
  function insertRecord(db, tableKey, row) {
22078
22270
  const table = tableByKey[tableKey];
22079
22271
  const columns = insertColumns[tableKey];
@@ -22210,6 +22402,7 @@ function importLocalBridgeBundle(bundle, options = {}, db) {
22210
22402
  const conflictStrategy = options.conflictStrategy ?? "skip";
22211
22403
  const data = {
22212
22404
  ...bundle.data,
22405
+ plans: normalizeBridgePlanSlugs(bundle.data.plans, d),
22213
22406
  tasks: sortedTasks(bundle.data.tasks),
22214
22407
  saved_views: bundle.data.saved_views ?? [],
22215
22408
  task_boards: bundle.data.task_boards ?? [],
@@ -22294,7 +22487,7 @@ var init_local_bridge = __esm(() => {
22294
22487
  insertColumns = {
22295
22488
  projects: ["id", "name", "path", "description", "task_list_id", "task_prefix", "task_counter", "created_at", "updated_at", "machine_id", "synced_at"],
22296
22489
  task_lists: ["id", "project_id", "slug", "name", "description", "metadata", "created_at", "updated_at", "machine_id", "synced_at"],
22297
- plans: ["id", "project_id", "task_list_id", "agent_id", "name", "description", "status", "created_at", "updated_at", "machine_id", "synced_at"],
22490
+ plans: ["id", "slug", "project_id", "task_list_id", "agent_id", "name", "description", "status", "created_at", "updated_at", "machine_id", "synced_at"],
22298
22491
  tasks: [
22299
22492
  "id",
22300
22493
  "short_id",
@@ -28389,6 +28582,7 @@ async function handleCreatePlan(req, _ctx, json2) {
28389
28582
  return json2({ error: "Missing 'name'" }, 400);
28390
28583
  const plan = createPlan({
28391
28584
  name: body.name,
28585
+ slug: body.slug,
28392
28586
  description: body.description,
28393
28587
  project_id: body.project_id,
28394
28588
  task_list_id: body.task_list_id,
@@ -37738,6 +37932,7 @@ Tasks:` : null,
37738
37932
  if (shouldRegisterTool("create_plan")) {
37739
37933
  server.tool("create_plan", "Create a new plan (sprint/milestone).", {
37740
37934
  name: exports_external2.string().describe("Plan name"),
37935
+ slug: exports_external2.string().optional().describe("Readable plan slug"),
37741
37936
  project_id: exports_external2.string().optional().describe("Project ID"),
37742
37937
  description: exports_external2.string().optional(),
37743
37938
  start_date: exports_external2.string().optional().describe("ISO date"),
@@ -44898,6 +45093,7 @@ function createAgentProjectDemoBundle() {
44898
45093
  });
44899
45094
  data.plans.push({
44900
45095
  id: ids.plan,
45096
+ slug: "ship-local-demo-workflow",
44901
45097
  project_id: ids.project,
44902
45098
  task_list_id: ids.list,
44903
45099
  agent_id: "demo-agent",
@@ -55038,6 +55234,339 @@ var init_config_serve_commands = __esm(() => {
55038
55234
  init_helpers();
55039
55235
  });
55040
55236
 
55237
+ // src/lib/task-route-sources.ts
55238
+ var exports_task_route_sources = {};
55239
+ __export(exports_task_route_sources, {
55240
+ discoverTaskRouteSources: () => discoverTaskRouteSources,
55241
+ TASK_ROUTE_SOURCE_DISCOVERY_SCHEMA_VERSION: () => TASK_ROUTE_SOURCE_DISCOVERY_SCHEMA_VERSION
55242
+ });
55243
+ import { Database as Database3 } from "bun:sqlite";
55244
+ import { createHash as createHash13 } from "crypto";
55245
+ import { existsSync as existsSync21, readdirSync as readdirSync5, statSync as statSync9 } from "fs";
55246
+ import { basename as basename9, dirname as dirname12, join as join21, resolve as resolve21 } from "path";
55247
+ function normalizePath5(input) {
55248
+ return resolve21(input);
55249
+ }
55250
+ function sourceStoreId(sourceDbPath) {
55251
+ const digest = createHash13("sha256").update(sourceDbPath).digest("hex").slice(0, 16);
55252
+ return `sqlite:${digest}`;
55253
+ }
55254
+ function inferSourceRepoPath(sourceDbPath) {
55255
+ const normalized = normalizePath5(sourceDbPath);
55256
+ if (normalized.endsWith(TODO_STORE_RELATIVE_PATH)) {
55257
+ return dirname12(dirname12(dirname12(normalized)));
55258
+ }
55259
+ return dirname12(normalized);
55260
+ }
55261
+ function createStoreRef(sourceDbPath) {
55262
+ const normalized = normalizePath5(sourceDbPath);
55263
+ return {
55264
+ source_store_id: sourceStoreId(normalized),
55265
+ source_repo_path: inferSourceRepoPath(normalized),
55266
+ source_db_path: normalized
55267
+ };
55268
+ }
55269
+ function normalizePatterns(patterns) {
55270
+ return (patterns ?? []).map((pattern) => pattern.trim()).filter(Boolean);
55271
+ }
55272
+ function escapeRegExp(value) {
55273
+ return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
55274
+ }
55275
+ function globPatternToRegExp(pattern) {
55276
+ let source3 = "";
55277
+ for (const char of pattern) {
55278
+ if (char === "*")
55279
+ source3 += ".*";
55280
+ else if (char === "?")
55281
+ source3 += ".";
55282
+ else
55283
+ source3 += escapeRegExp(char);
55284
+ }
55285
+ return new RegExp(`^${source3}$`);
55286
+ }
55287
+ function matchesPattern4(value, pattern) {
55288
+ const normalizedValue = value.replace(/\\/g, "/");
55289
+ const normalizedPattern = pattern.replace(/\\/g, "/");
55290
+ if (normalizedPattern.includes("*") || normalizedPattern.includes("?")) {
55291
+ return globPatternToRegExp(normalizedPattern).test(normalizedValue);
55292
+ }
55293
+ return normalizedValue.includes(normalizedPattern);
55294
+ }
55295
+ function storeMatchesAny(ref, patterns) {
55296
+ if (patterns.length === 0)
55297
+ return false;
55298
+ const paths = [ref.source_db_path, ref.source_repo_path].filter((value) => Boolean(value));
55299
+ const values = paths.flatMap((value) => [value, basename9(value)]);
55300
+ return patterns.some((pattern) => values.some((value) => matchesPattern4(value, pattern)));
55301
+ }
55302
+ function shouldIncludeStore(ref, include, exclude) {
55303
+ const included = include.length === 0 || storeMatchesAny(ref, include);
55304
+ return included && !storeMatchesAny(ref, exclude);
55305
+ }
55306
+ function discoverStoresUnderRoot(sourceRoot) {
55307
+ const rootPath = normalizePath5(sourceRoot);
55308
+ const errors2 = [];
55309
+ const stores = [];
55310
+ if (!existsSync21(rootPath)) {
55311
+ const ref = createStoreRef(join21(rootPath, TODO_STORE_RELATIVE_PATH));
55312
+ errors2.push({
55313
+ ...ref,
55314
+ code: "SOURCE_ROOT_MISSING",
55315
+ message: `Source root does not exist: ${rootPath}`
55316
+ });
55317
+ return { stores, errors: errors2 };
55318
+ }
55319
+ let rootStat;
55320
+ try {
55321
+ rootStat = statSync9(rootPath);
55322
+ } catch (error) {
55323
+ const ref = createStoreRef(join21(rootPath, TODO_STORE_RELATIVE_PATH));
55324
+ errors2.push({
55325
+ ...ref,
55326
+ code: "SOURCE_ROOT_UNREADABLE",
55327
+ message: error instanceof Error ? error.message : `Unable to read source root: ${rootPath}`
55328
+ });
55329
+ return { stores, errors: errors2 };
55330
+ }
55331
+ if (rootStat.isFile()) {
55332
+ stores.push(createStoreRef(rootPath));
55333
+ return { stores, errors: errors2 };
55334
+ }
55335
+ function scanDirectory(dir, depth) {
55336
+ const candidate = join21(dir, TODO_STORE_RELATIVE_PATH);
55337
+ if (existsSync21(candidate)) {
55338
+ stores.push(createStoreRef(candidate));
55339
+ }
55340
+ if (depth >= ROOT_SCAN_MAX_DEPTH)
55341
+ return;
55342
+ let entries;
55343
+ try {
55344
+ entries = readdirSync5(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
55345
+ } catch (error) {
55346
+ const ref = createStoreRef(candidate);
55347
+ errors2.push({
55348
+ ...ref,
55349
+ code: "SOURCE_ROOT_UNREADABLE",
55350
+ message: error instanceof Error ? error.message : `Unable to read source root: ${dir}`
55351
+ });
55352
+ return;
55353
+ }
55354
+ for (const entry of entries) {
55355
+ if (!entry.isDirectory() || SKIPPED_SCAN_DIRS.has(entry.name))
55356
+ continue;
55357
+ scanDirectory(join21(dir, entry.name), depth + 1);
55358
+ }
55359
+ }
55360
+ scanDirectory(rootPath, 0);
55361
+ return { stores, errors: errors2 };
55362
+ }
55363
+ function collectStoreRefs(input) {
55364
+ const byPath = new Map;
55365
+ const errors2 = [];
55366
+ for (const storePath of input.sourceStores ?? []) {
55367
+ const ref = createStoreRef(storePath);
55368
+ byPath.set(ref.source_db_path, ref);
55369
+ }
55370
+ for (const sourceRoot of input.sourceRoots ?? []) {
55371
+ const discovered = discoverStoresUnderRoot(sourceRoot);
55372
+ for (const ref of discovered.stores) {
55373
+ byPath.set(ref.source_db_path, ref);
55374
+ }
55375
+ errors2.push(...discovered.errors);
55376
+ }
55377
+ return {
55378
+ stores: [...byPath.values()].sort((a, b) => a.source_db_path.localeCompare(b.source_db_path)),
55379
+ errors: errors2.sort((a, b) => a.source_db_path.localeCompare(b.source_db_path))
55380
+ };
55381
+ }
55382
+ function openReadonlyStore(ref) {
55383
+ if (!existsSync21(ref.source_db_path)) {
55384
+ throw Object.assign(new Error(`Store does not exist: ${ref.source_db_path}`), { code: "STORE_MISSING" });
55385
+ }
55386
+ return new Database3(ref.source_db_path, { readonly: true, create: false });
55387
+ }
55388
+ function hasTable2(db, tableName) {
55389
+ const row = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(tableName);
55390
+ return Boolean(row);
55391
+ }
55392
+ function tableColumns2(db, tableName) {
55393
+ const rows = db.query(`PRAGMA table_info(${tableName})`).all();
55394
+ return new Set(rows.map((row) => row.name));
55395
+ }
55396
+ function listPendingTasksReadonly(db) {
55397
+ if (!hasTable2(db, "tasks")) {
55398
+ throw Object.assign(new Error("Store does not contain a tasks table"), { code: "STORE_INVALID" });
55399
+ }
55400
+ const columns = tableColumns2(db, "tasks");
55401
+ const conditions = ["status = 'pending'"];
55402
+ if (columns.has("archived_at"))
55403
+ conditions.push("archived_at IS NULL");
55404
+ const rows = db.query(`SELECT * FROM tasks WHERE ${conditions.join(" AND ")}
55405
+ ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END, created_at DESC`).all();
55406
+ return rows.map(rowToTask);
55407
+ }
55408
+ function isReadyTask(task2, db) {
55409
+ if (task2.locked_by && !isLockExpired(task2.locked_at))
55410
+ return false;
55411
+ return getBlockingDeps(task2.id, db).length === 0;
55412
+ }
55413
+ function metadataFingerprint(metadata) {
55414
+ const value = metadata.fingerprint;
55415
+ if (typeof value === "string" && value.trim())
55416
+ return value;
55417
+ if (typeof value === "number" && Number.isFinite(value))
55418
+ return String(value);
55419
+ return null;
55420
+ }
55421
+ function boundedMetadataValue(value, depth = 0) {
55422
+ if (depth > 6)
55423
+ return "[TRUNCATED]";
55424
+ if (typeof value === "string") {
55425
+ return value.length > 2000 ? `${value.slice(0, 2000)}[TRUNCATED]` : value;
55426
+ }
55427
+ if (Array.isArray(value)) {
55428
+ return value.slice(0, 50).map((item) => boundedMetadataValue(item, depth + 1));
55429
+ }
55430
+ if (value && typeof value === "object") {
55431
+ const result = {};
55432
+ for (const [key, child] of Object.entries(value).slice(0, 80)) {
55433
+ const normalized = key.toLowerCase();
55434
+ if (normalized === "comment" || normalized === "comments" || normalized === "task_comments") {
55435
+ result[key] = "[REDACTED_COMMENT]";
55436
+ continue;
55437
+ }
55438
+ result[key] = boundedMetadataValue(child, depth + 1);
55439
+ }
55440
+ return result;
55441
+ }
55442
+ return value;
55443
+ }
55444
+ function discoveryMetadata(metadata) {
55445
+ return redactValue(boundedMetadataValue(metadata));
55446
+ }
55447
+ function sourceCandidate(ref, task2, db) {
55448
+ const routeState = getTaskRouteState(task2, db);
55449
+ const autoRoute = task2.tags.includes("auto:route") || task2.tags.includes("route:enabled");
55450
+ return {
55451
+ source_store_id: ref.source_store_id,
55452
+ source_repo_path: ref.source_repo_path,
55453
+ source_db_path: ref.source_db_path,
55454
+ source_task_key: `${ref.source_store_id}:${task2.id}`,
55455
+ source_selected_by_input: true,
55456
+ task_id: task2.id,
55457
+ task_short_id: task2.short_id,
55458
+ title: task2.title,
55459
+ status: task2.status,
55460
+ priority: task2.priority,
55461
+ project_path: routeState.route.project_path ?? task2.working_dir ?? ref.source_repo_path,
55462
+ task_version: task2.version,
55463
+ task_updated_at: task2.updated_at,
55464
+ task_fingerprint: metadataFingerprint(task2.metadata),
55465
+ tags: task2.tags,
55466
+ task_intent: {
55467
+ auto_route: autoRoute
55468
+ },
55469
+ metadata: discoveryMetadata(task2.metadata),
55470
+ route_state: routeState
55471
+ };
55472
+ }
55473
+ function discoveryError(ref, code, error) {
55474
+ return {
55475
+ ...ref,
55476
+ code,
55477
+ message: error instanceof Error ? error.message : String(error)
55478
+ };
55479
+ }
55480
+ function errorCode(error) {
55481
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "STORE_MISSING") {
55482
+ return "STORE_MISSING";
55483
+ }
55484
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "STORE_INVALID") {
55485
+ return "STORE_INVALID";
55486
+ }
55487
+ return "STORE_UNREADABLE";
55488
+ }
55489
+ function discoverTaskRouteSources(input) {
55490
+ const include = normalizePatterns(input.include);
55491
+ const exclude = normalizePatterns(input.exclude);
55492
+ const sourceRoots = (input.sourceRoots ?? []).map(normalizePath5).sort();
55493
+ const sourceStores = (input.sourceStores ?? []).map(normalizePath5).sort();
55494
+ const limit = Number.isFinite(input.limit ?? NaN) && (input.limit ?? 0) >= 0 ? Math.floor(input.limit ?? 0) : null;
55495
+ const collected = collectStoreRefs(input);
55496
+ const stores = [];
55497
+ const errors2 = [...collected.errors];
55498
+ const candidates = [];
55499
+ let totalCandidateCount = 0;
55500
+ for (const ref of collected.stores) {
55501
+ if (!shouldIncludeStore(ref, include, exclude))
55502
+ continue;
55503
+ const storeErrors = [];
55504
+ let db = null;
55505
+ try {
55506
+ db = openReadonlyStore(ref);
55507
+ const readyTasks = listPendingTasksReadonly(db).filter((task2) => isReadyTask(task2, db));
55508
+ totalCandidateCount += readyTasks.length;
55509
+ const remaining = limit === null ? readyTasks.length : Math.max(0, limit - candidates.length);
55510
+ const selectedTasks = limit === null ? readyTasks : readyTasks.slice(0, remaining);
55511
+ candidates.push(...selectedTasks.map((task2) => sourceCandidate(ref, task2, db)));
55512
+ stores.push({
55513
+ ...ref,
55514
+ status: "ok",
55515
+ candidate_count: readyTasks.length,
55516
+ returned_candidate_count: selectedTasks.length,
55517
+ errors: []
55518
+ });
55519
+ } catch (error) {
55520
+ const storeError = discoveryError(ref, errorCode(error), error);
55521
+ storeErrors.push(storeError);
55522
+ errors2.push(storeError);
55523
+ stores.push({
55524
+ ...ref,
55525
+ status: storeError.code === "STORE_MISSING" ? "missing" : "error",
55526
+ candidate_count: 0,
55527
+ returned_candidate_count: 0,
55528
+ errors: storeErrors
55529
+ });
55530
+ } finally {
55531
+ db?.close();
55532
+ }
55533
+ }
55534
+ return {
55535
+ schema_version: TASK_ROUTE_SOURCE_DISCOVERY_SCHEMA_VERSION,
55536
+ sourceRoots,
55537
+ sourceStores,
55538
+ include,
55539
+ exclude,
55540
+ limit,
55541
+ total_candidate_count: totalCandidateCount,
55542
+ returned_candidate_count: candidates.length,
55543
+ truncated: limit !== null && totalCandidateCount > candidates.length,
55544
+ stores,
55545
+ candidates,
55546
+ errors: errors2
55547
+ };
55548
+ }
55549
+ var TASK_ROUTE_SOURCE_DISCOVERY_SCHEMA_VERSION = "todos.task_route_sources.v1", TODO_STORE_RELATIVE_PATH, ROOT_SCAN_MAX_DEPTH = 5, SKIPPED_SCAN_DIRS;
55550
+ var init_task_route_sources = __esm(() => {
55551
+ init_database();
55552
+ init_task_lifecycle();
55553
+ init_task_crud();
55554
+ init_redaction();
55555
+ init_task_routing();
55556
+ TODO_STORE_RELATIVE_PATH = join21(".hasna", "todos", "todos.db");
55557
+ SKIPPED_SCAN_DIRS = new Set([
55558
+ ".git",
55559
+ ".hg",
55560
+ ".svn",
55561
+ "node_modules",
55562
+ "dist",
55563
+ "build",
55564
+ ".next",
55565
+ ".turbo",
55566
+ ".cache"
55567
+ ]);
55568
+ });
55569
+
55041
55570
  // src/lib/tester-issue-reports.ts
55042
55571
  var exports_tester_issue_reports = {};
55043
55572
  __export(exports_tester_issue_reports, {
@@ -55050,7 +55579,7 @@ __export(exports_tester_issue_reports, {
55050
55579
  TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION: () => TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
55051
55580
  TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION: () => TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION
55052
55581
  });
55053
- import { createHash as createHash13 } from "crypto";
55582
+ import { createHash as createHash14 } from "crypto";
55054
55583
  function asObject3(value) {
55055
55584
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
55056
55585
  }
@@ -55222,7 +55751,7 @@ function fingerprintTesterIssueReport(report) {
55222
55751
  normalizeText4(report.failure?.message || report.summary || report.title).slice(0, 240),
55223
55752
  normalizeText4(stackTop).slice(0, 160)
55224
55753
  ].join("::");
55225
- return `testers:${createHash13("sha256").update(raw).digest("hex").slice(0, 16)}`;
55754
+ return `testers:${createHash14("sha256").update(raw).digest("hex").slice(0, 16)}`;
55226
55755
  }
55227
55756
  function priorityForSeverity(severity, fallback) {
55228
55757
  return PRIORITIES5.includes(severity) ? severity : fallback;
@@ -55541,6 +56070,15 @@ function parseCsvOption(value) {
55541
56070
  const values = value.split(",").map((item) => item.trim()).filter(Boolean);
55542
56071
  return values.length > 0 ? values : undefined;
55543
56072
  }
56073
+ function collectOption2(value, previous = []) {
56074
+ return [...previous, value];
56075
+ }
56076
+ function expandRepeatedCsvOption(value) {
56077
+ if (!value || value.length === 0)
56078
+ return;
56079
+ const values = value.flatMap((item) => item.split(",").map((part) => part.trim()).filter(Boolean));
56080
+ return values.length > 0 ? values : undefined;
56081
+ }
55544
56082
  function resolveOptionalId(table, value) {
55545
56083
  if (!value)
55546
56084
  return;
@@ -56179,13 +56717,13 @@ Repairs`));
56179
56717
  try {
56180
56718
  const db = getDatabase();
56181
56719
  const row = db.query("SELECT COUNT(*) as count FROM tasks").get();
56182
- const { statSync: statSync9 } = await import("fs");
56183
- const { join: join21 } = await import("path");
56720
+ const { statSync: statSync10 } = await import("fs");
56721
+ const { join: join22 } = await import("path");
56184
56722
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
56185
- const dbPath = process.env["HASNA_TODOS_DB_PATH"] || process.env["TODOS_DB_PATH"] || join21(home, ".hasna", "todos", "todos.db");
56723
+ const dbPath = process.env["HASNA_TODOS_DB_PATH"] || process.env["TODOS_DB_PATH"] || join22(home, ".hasna", "todos", "todos.db");
56186
56724
  let size = "unknown";
56187
56725
  try {
56188
- size = `${(statSync9(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
56726
+ size = `${(statSync10(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
56189
56727
  } catch {}
56190
56728
  checks.push({ name: "Database", ok: true, message: `${row.count} tasks \xB7 ${size} \xB7 ${chalk7.dim(dbPath)}` });
56191
56729
  } catch (e) {
@@ -56675,8 +57213,42 @@ Repairs`));
56675
57213
  console.log(` ${chalk7.dim(time2)} ${chalk7.cyan(entry.source)} ${chalk7.dim(ref)} ${entry.event_type}${message}${agent}`);
56676
57214
  }
56677
57215
  });
56678
- program2.command("ready").description("Show all tasks ready to be claimed (pending, unblocked, unlocked)").option("-j, --json", "Output as JSON").option("--project <id>", "Filter to project").option("--limit <n>", "Max tasks to show", "20").action(async (opts) => {
57216
+ program2.command("ready").description("Show all tasks ready to be claimed (pending, unblocked, unlocked)").option("-j, --json", "Output as JSON").option("--project <id>", "Filter to project").option("--limit <n>", "Max tasks to show", "20").option("--source-root <path>", "Read-only source root to scan for .hasna/todos/todos.db (repeatable)", collectOption2, []).option("--source-store <path>", "Read-only todos SQLite store path to scan (repeatable)", collectOption2, []).option("--include <pattern>", "Include source repo/store paths matching substring or glob (repeatable or comma-separated)", collectOption2, []).option("--exclude <pattern>", "Exclude source repo/store paths matching substring or glob (repeatable or comma-separated)", collectOption2, []).action(async (opts) => {
56679
57217
  const globalOpts = program2.opts();
57218
+ const sourceRoots = expandRepeatedCsvOption(opts.sourceRoot);
57219
+ const sourceStores = expandRepeatedCsvOption(opts.sourceStore);
57220
+ const include = expandRepeatedCsvOption(opts.include);
57221
+ const exclude = expandRepeatedCsvOption(opts.exclude);
57222
+ if (sourceRoots || sourceStores || include || exclude) {
57223
+ const { discoverTaskRouteSources: discoverTaskRouteSources2 } = await Promise.resolve().then(() => (init_task_route_sources(), exports_task_route_sources));
57224
+ const result = discoverTaskRouteSources2({
57225
+ sourceRoots,
57226
+ sourceStores,
57227
+ include,
57228
+ exclude,
57229
+ limit: parseInt(opts.limit, 10)
57230
+ });
57231
+ if (opts.json || globalOpts.json) {
57232
+ console.log(JSON.stringify(result));
57233
+ return;
57234
+ }
57235
+ if (result.candidates.length === 0) {
57236
+ console.log(chalk7.dim(" No source tasks ready to claim."));
57237
+ } else {
57238
+ console.log(chalk7.bold(`Ready source tasks (${result.candidates.length}):
57239
+ `));
57240
+ for (const candidate of result.candidates) {
57241
+ const source3 = candidate.source_repo_path ?? candidate.source_db_path;
57242
+ const pri = candidate.priority === "critical" ? chalk7.bgRed.white(" CRIT ") : candidate.priority === "high" ? chalk7.red("[high]") : candidate.priority === "medium" ? chalk7.yellow("[med]") : "";
57243
+ console.log(` ${chalk7.cyan(candidate.task_short_id || candidate.task_id.slice(0, 8))} ${candidate.title} ${pri}${chalk7.dim(` ${source3}`)}`);
57244
+ }
57245
+ }
57246
+ if (result.errors.length > 0) {
57247
+ console.log(chalk7.yellow(`
57248
+ ${result.errors.length} source error${result.errors.length === 1 ? "" : "s"} isolated; rerun with --json for details.`));
57249
+ }
57250
+ return;
57251
+ }
56680
57252
  const db = getDatabase();
56681
57253
  const { getBlockingDeps: getBlockingDeps2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
56682
57254
  const { isLockExpired: isLockExpired2 } = await Promise.resolve().then(() => (init_database(), exports_database));
@@ -57998,21 +58570,21 @@ __export(exports_mcp_hooks_commands, {
57998
58570
  });
57999
58571
  import chalk8 from "chalk";
58000
58572
  import { execSync as execSync3 } from "child_process";
58001
- import { existsSync as existsSync21, readFileSync as readFileSync17, writeFileSync as writeFileSync10, mkdirSync as mkdirSync11, chmodSync as chmodSync2 } from "fs";
58002
- import { dirname as dirname12, join as join21 } from "path";
58573
+ import { existsSync as existsSync22, readFileSync as readFileSync17, writeFileSync as writeFileSync10, mkdirSync as mkdirSync11, chmodSync as chmodSync2 } from "fs";
58574
+ import { dirname as dirname13, join as join22 } from "path";
58003
58575
  function getMcpBinaryPath() {
58004
58576
  try {
58005
58577
  const p = execSync3("which todos-mcp", { encoding: "utf-8" }).trim();
58006
58578
  if (p)
58007
58579
  return p;
58008
58580
  } catch {}
58009
- const bunBin = join21(HOME2, ".bun", "bin", "todos-mcp");
58010
- if (existsSync21(bunBin))
58581
+ const bunBin = join22(HOME2, ".bun", "bin", "todos-mcp");
58582
+ if (existsSync22(bunBin))
58011
58583
  return bunBin;
58012
58584
  return "todos-mcp";
58013
58585
  }
58014
58586
  function readJsonFile2(path) {
58015
- if (!existsSync21(path))
58587
+ if (!existsSync22(path))
58016
58588
  return {};
58017
58589
  try {
58018
58590
  return JSON.parse(readFileSync17(path, "utf-8"));
@@ -58021,20 +58593,20 @@ function readJsonFile2(path) {
58021
58593
  }
58022
58594
  }
58023
58595
  function writeJsonFile2(path, data) {
58024
- const dir = dirname12(path);
58025
- if (!existsSync21(dir))
58596
+ const dir = dirname13(path);
58597
+ if (!existsSync22(dir))
58026
58598
  mkdirSync11(dir, { recursive: true });
58027
58599
  writeFileSync10(path, JSON.stringify(data, null, 2) + `
58028
58600
  `);
58029
58601
  }
58030
58602
  function readTomlFile(path) {
58031
- if (!existsSync21(path))
58603
+ if (!existsSync22(path))
58032
58604
  return "";
58033
58605
  return readFileSync17(path, "utf-8");
58034
58606
  }
58035
58607
  function writeTomlFile(path, content) {
58036
- const dir = dirname12(path);
58037
- if (!existsSync21(dir))
58608
+ const dir = dirname13(path);
58609
+ if (!existsSync22(dir))
58038
58610
  mkdirSync11(dir, { recursive: true });
58039
58611
  writeFileSync10(path, content);
58040
58612
  }
@@ -58100,7 +58672,7 @@ function unregisterClaude(_global) {
58100
58672
  }
58101
58673
  }
58102
58674
  function registerCodex(binPath) {
58103
- const configPath = join21(HOME2, ".codex", "config.toml");
58675
+ const configPath = join22(HOME2, ".codex", "config.toml");
58104
58676
  let content = readTomlFile(configPath);
58105
58677
  content = removeTomlBlock(content, "mcp_servers.todos");
58106
58678
  const block = `
@@ -58114,7 +58686,7 @@ args = []
58114
58686
  console.log(chalk8.green(`Codex CLI: registered in ${configPath}`));
58115
58687
  }
58116
58688
  function unregisterCodex() {
58117
- const configPath = join21(HOME2, ".codex", "config.toml");
58689
+ const configPath = join22(HOME2, ".codex", "config.toml");
58118
58690
  let content = readTomlFile(configPath);
58119
58691
  if (!content.includes("[mcp_servers.todos]")) {
58120
58692
  console.log(chalk8.dim(`Codex CLI: todos not found in ${configPath}`));
@@ -58126,7 +58698,7 @@ function unregisterCodex() {
58126
58698
  console.log(chalk8.green(`Codex CLI: unregistered from ${configPath}`));
58127
58699
  }
58128
58700
  function registerGemini(binPath) {
58129
- const configPath = join21(HOME2, ".gemini", "settings.json");
58701
+ const configPath = join22(HOME2, ".gemini", "settings.json");
58130
58702
  const config = readJsonFile2(configPath);
58131
58703
  if (!config["mcpServers"]) {
58132
58704
  config["mcpServers"] = {};
@@ -58140,7 +58712,7 @@ function registerGemini(binPath) {
58140
58712
  console.log(chalk8.green(`Gemini CLI: registered in ${configPath}`));
58141
58713
  }
58142
58714
  function unregisterGemini() {
58143
- const configPath = join21(HOME2, ".gemini", "settings.json");
58715
+ const configPath = join22(HOME2, ".gemini", "settings.json");
58144
58716
  const config = readJsonFile2(configPath);
58145
58717
  const servers = config["mcpServers"];
58146
58718
  if (!servers || !("todos" in servers)) {
@@ -58197,8 +58769,8 @@ function registerMcpHooksCommands(program2) {
58197
58769
  if (p)
58198
58770
  todosBin = p;
58199
58771
  } catch {}
58200
- const hooksDir = join21(process.cwd(), ".claude", "hooks");
58201
- if (!existsSync21(hooksDir))
58772
+ const hooksDir = join22(process.cwd(), ".claude", "hooks");
58773
+ if (!existsSync22(hooksDir))
58202
58774
  mkdirSync11(hooksDir, { recursive: true });
58203
58775
  const hookScript = `#!/usr/bin/env bash
58204
58776
  # Auto-generated by: todos hooks install
@@ -58223,11 +58795,11 @@ esac
58223
58795
 
58224
58796
  exit 0
58225
58797
  `;
58226
- const hookPath = join21(hooksDir, "todos-sync.sh");
58798
+ const hookPath = join22(hooksDir, "todos-sync.sh");
58227
58799
  writeFileSync10(hookPath, hookScript);
58228
58800
  execSync3(`chmod +x "${hookPath}"`);
58229
58801
  console.log(chalk8.green(`Hook script created: ${hookPath}`));
58230
- const settingsPath = join21(process.cwd(), ".claude", "settings.json");
58802
+ const settingsPath = join22(process.cwd(), ".claude", "settings.json");
58231
58803
  const settings = readJsonFile2(settingsPath);
58232
58804
  if (!settings["hooks"]) {
58233
58805
  settings["hooks"] = {};
@@ -59096,7 +59668,7 @@ Artifacts:`));
59096
59668
  const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
59097
59669
  const hookPath = `${gitDir}/hooks/post-commit`;
59098
59670
  const marker = "# todos-auto-link";
59099
- if (existsSync21(hookPath)) {
59671
+ if (existsSync22(hookPath)) {
59100
59672
  const existing = readFileSync17(hookPath, "utf-8");
59101
59673
  if (existing.includes(marker)) {
59102
59674
  console.log(chalk8.yellow("Hook already installed."));
@@ -59124,7 +59696,7 @@ $(dirname "$0")/../../scripts/post-commit-hook.sh
59124
59696
  const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
59125
59697
  const hookPath = `${gitDir}/hooks/post-commit`;
59126
59698
  const marker = "# todos-auto-link";
59127
- if (!existsSync21(hookPath)) {
59699
+ if (!existsSync22(hookPath)) {
59128
59700
  console.log(chalk8.dim("No post-commit hook found."));
59129
59701
  return;
59130
59702
  }
@@ -59310,7 +59882,7 @@ import chalk10 from "chalk";
59310
59882
  import { execSync as execSync4 } from "child_process";
59311
59883
  import { readFileSync as readFileSync18, unlinkSync as unlinkSync2, writeFileSync as writeFileSync11 } from "fs";
59312
59884
  import { tmpdir as tmpdir4 } from "os";
59313
- import { join as join22 } from "path";
59885
+ import { join as join23 } from "path";
59314
59886
  function getOrCreateLocalMachineName() {
59315
59887
  return process.env["TODOS_MACHINE_NAME"] || __require("os").hostname() || "unknown";
59316
59888
  }
@@ -59348,7 +59920,7 @@ function remoteTempPath(sshAddress) {
59348
59920
  }
59349
59921
  function readRemoteBridgeBundle(sshAddress) {
59350
59922
  const remotePath = remoteTempPath(sshAddress);
59351
- const localPath = join22(tmpdir4(), `todos-bridge-pull-${uuid()}.json`);
59923
+ const localPath = join23(tmpdir4(), `todos-bridge-pull-${uuid()}.json`);
59352
59924
  try {
59353
59925
  runSsh(sshAddress, `todos export --format bridge --allow-plaintext-sensitive --output ${shellQuote(remotePath)}`, 120000);
59354
59926
  scpFromRemote(sshAddress, remotePath, localPath);
@@ -59363,7 +59935,7 @@ function readRemoteBridgeBundle(sshAddress) {
59363
59935
  }
59364
59936
  }
59365
59937
  function writeLocalBridgeBundle() {
59366
- const localPath = join22(tmpdir4(), `todos-bridge-push-${uuid()}.json`);
59938
+ const localPath = join23(tmpdir4(), `todos-bridge-push-${uuid()}.json`);
59367
59939
  writeFileSync11(localPath, JSON.stringify(createLocalBridgeBundle(), null, 2));
59368
59940
  return localPath;
59369
59941
  }
@@ -60427,7 +60999,7 @@ __export(exports_onboarding_commands, {
60427
60999
  registerOnboardingCommands: () => registerOnboardingCommands
60428
61000
  });
60429
61001
  import chalk17 from "chalk";
60430
- import { resolve as resolve21 } from "path";
61002
+ import { resolve as resolve22 } from "path";
60431
61003
  function registerOnboardingCommands(program2) {
60432
61004
  program2.command("onboarding").alias("demo-fixtures").description("List, show, write, or import bundled local onboarding fixtures").option("--show <name>", "Show one fixture bridge bundle as JSON").option("--write <dir>", "Write all bundled fixture bridge bundles to a directory").option("--import <name>", "Dry-run or apply an onboarding fixture import").option("--apply", "Apply an onboarding fixture import. Defaults to dry-run.").option("--resolve-conflicts", "Safely merge existing local tasks while preserving divergent fields").action(async (opts) => {
60433
61005
  const globalOpts = program2.opts();
@@ -60443,7 +61015,7 @@ function registerOnboardingCommands(program2) {
60443
61015
  return;
60444
61016
  }
60445
61017
  if (opts.write) {
60446
- const result = writeOnboardingFixtureFiles2(resolve21(opts.write));
61018
+ const result = writeOnboardingFixtureFiles2(resolve22(opts.write));
60447
61019
  if (globalOpts.json) {
60448
61020
  output(result, true);
60449
61021
  return;
@@ -63892,7 +64464,7 @@ __export(exports_sdk_integration_fixtures, {
63892
64464
  TODOS_SDK_INTEGRATION_FIXTURE_GENERATED_AT: () => TODOS_SDK_INTEGRATION_FIXTURE_GENERATED_AT
63893
64465
  });
63894
64466
  import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync12 } from "fs";
63895
- import { join as join23 } from "path";
64467
+ import { join as join24 } from "path";
63896
64468
  function source5(version) {
63897
64469
  return {
63898
64470
  packageName: "@hasna/todos",
@@ -63999,7 +64571,7 @@ function writeSdkIntegrationFixtures(directory, options = {}) {
63999
64571
  ];
64000
64572
  const written = [];
64001
64573
  for (const [name, payload] of files) {
64002
- const file = join23(directory, name);
64574
+ const file = join24(directory, name);
64003
64575
  writeFileSync12(file, `${JSON.stringify(payload, null, 2)}
64004
64576
  `, "utf-8");
64005
64577
  written.push(file);
@@ -64023,7 +64595,7 @@ __export(exports_sdk_fixture_commands, {
64023
64595
  registerSdkFixtureCommands: () => registerSdkFixtureCommands
64024
64596
  });
64025
64597
  import chalk19 from "chalk";
64026
- import { resolve as resolve22 } from "path";
64598
+ import { resolve as resolve23 } from "path";
64027
64599
  function registerSdkFixtureCommands(program2) {
64028
64600
  program2.command("sdk-fixtures").description("List, show, or write local SDK integration fixtures").option("--show", "Print the full fixture pack JSON").option("--write <dir>", "Write fixture pack, bridge fixture, contract snapshots, and example index to a directory").action(async (opts) => {
64029
64601
  const globalOpts = program2.opts();
@@ -64034,7 +64606,7 @@ function registerSdkFixtureCommands(program2) {
64034
64606
  writeSdkIntegrationFixtures: writeSdkIntegrationFixtures2
64035
64607
  } = await Promise.resolve().then(() => (init_sdk_integration_fixtures(), exports_sdk_integration_fixtures));
64036
64608
  if (opts.write) {
64037
- const result = writeSdkIntegrationFixtures2(resolve22(opts.write));
64609
+ const result = writeSdkIntegrationFixtures2(resolve23(opts.write));
64038
64610
  if (globalOpts.json) {
64039
64611
  console.log(JSON.stringify(result));
64040
64612
  return;
@@ -64856,7 +65428,7 @@ __export(exports_local_backup_commands, {
64856
65428
  registerLocalBackupCommands: () => registerLocalBackupCommands
64857
65429
  });
64858
65430
  import chalk26 from "chalk";
64859
- import { resolve as resolve23 } from "path";
65431
+ import { resolve as resolve24 } from "path";
64860
65432
  function globalOptions6(program2) {
64861
65433
  const command = program2;
64862
65434
  return command.optsWithGlobals?.() ?? program2.opts();
@@ -64878,10 +65450,10 @@ function registerLocalBackupCommands(program2) {
64878
65450
  const projectId = opts.projectId ?? autoProject(globalOpts);
64879
65451
  const backupBundle = createLocalBackup2({
64880
65452
  project_id: projectId,
64881
- output_path: opts.output ? resolve23(opts.output) : undefined
65453
+ output_path: opts.output ? resolve24(opts.output) : undefined
64882
65454
  });
64883
65455
  const result = {
64884
- output_path: opts.output ? resolve23(opts.output) : null,
65456
+ output_path: opts.output ? resolve24(opts.output) : null,
64885
65457
  backup: backupBundle
64886
65458
  };
64887
65459
  if (opts.json || globalOpts.json) {
@@ -66291,9 +66863,17 @@ async function updateProject2(id, input, store) {
66291
66863
  }
66292
66864
  async function createPlan2(input, store, context) {
66293
66865
  const timestamp3 = new Date().toISOString();
66866
+ const projectId = input.project_id ?? context?.projectId ?? null;
66867
+ const slug = await resolvePostgresPlanSlug({
66868
+ name: input.name,
66869
+ slug: input.slug,
66870
+ projectId,
66871
+ store
66872
+ });
66294
66873
  return store.upsert("plans", {
66295
66874
  id: randomUUID3(),
66296
- project_id: input.project_id ?? context?.projectId ?? null,
66875
+ slug,
66876
+ project_id: projectId,
66297
66877
  task_list_id: input.task_list_id ?? context?.taskListId ?? null,
66298
66878
  agent_id: input.agent_id ?? context?.agentId ?? null,
66299
66879
  name: input.name,
@@ -66307,7 +66887,17 @@ async function createPlan2(input, store, context) {
66307
66887
  }
66308
66888
  async function updatePlan2(id, input, store) {
66309
66889
  const plan = await requireRecord("plans", id, store);
66310
- return store.upsert("plans", { ...plan, ...definedPatch(input), updated_at: new Date().toISOString() });
66890
+ const patch = definedPatch(input);
66891
+ if (input.slug !== undefined) {
66892
+ patch.slug = await resolvePostgresPlanSlug({
66893
+ name: plan.name,
66894
+ slug: input.slug,
66895
+ projectId: plan.project_id,
66896
+ store,
66897
+ excludeId: id
66898
+ });
66899
+ }
66900
+ return store.upsert("plans", { ...plan, ...patch, updated_at: new Date().toISOString() });
66311
66901
  }
66312
66902
  async function registerAgent2(input, store, context) {
66313
66903
  const existing = (await store.list("agents")).find((agent2) => agent2.name === input.name && agent2.status !== "archived");
@@ -66560,8 +67150,38 @@ function matchesOne(value, expected) {
66560
67150
  function priorityRank2(priority) {
66561
67151
  return { critical: 0, high: 1, medium: 2, low: 3 }[priority];
66562
67152
  }
67153
+ function slugifyRaw(value) {
67154
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
67155
+ }
66563
67156
  function slugify2(value) {
66564
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "todos";
67157
+ return slugifyRaw(value) || "todos";
67158
+ }
67159
+ function normalizePlanSlug2(value) {
67160
+ const slug = slugifyRaw(value);
67161
+ if (!slug)
67162
+ throw new Error("Invalid plan slug");
67163
+ return slug;
67164
+ }
67165
+ function planSlugBase4(value) {
67166
+ return slugifyRaw(value) || "plan";
67167
+ }
67168
+ async function resolvePostgresPlanSlug(options) {
67169
+ const plans = await options.store.list("plans");
67170
+ const used = new Set(plans.filter((plan) => plan.project_id === options.projectId && plan.id !== options.excludeId && plan.slug).map((plan) => plan.slug));
67171
+ if (options.slug !== undefined) {
67172
+ const slug = normalizePlanSlug2(options.slug);
67173
+ if (used.has(slug))
67174
+ throw new Error(`Plan slug already exists in this scope: ${slug}`);
67175
+ return slug;
67176
+ }
67177
+ const base = planSlugBase4(options.name);
67178
+ let candidate = base;
67179
+ let suffix = 2;
67180
+ while (used.has(candidate)) {
67181
+ candidate = `${base}-${suffix}`;
67182
+ suffix += 1;
67183
+ }
67184
+ return candidate;
66565
67185
  }
66566
67186
  function definedPatch(value) {
66567
67187
  return Object.fromEntries(Object.entries(value).filter(([, entry2]) => entry2 !== undefined));
@@ -66637,7 +67257,7 @@ var init_factory = __esm(() => {
66637
67257
  });
66638
67258
 
66639
67259
  // src/storage/s3-artifacts.ts
66640
- import { createHash as createHash14, createHmac as createHmac2 } from "crypto";
67260
+ import { createHash as createHash15, createHmac as createHmac2 } from "crypto";
66641
67261
  function createTodosS3ArtifactStore(options) {
66642
67262
  const requestFetch = options.fetch ?? fetch;
66643
67263
  const now4 = options.now ?? (() => new Date);
@@ -66809,7 +67429,7 @@ function toAmzDate(date) {
66809
67429
  return date.toISOString().replace(/[:-]|\.\d{3}/g, "");
66810
67430
  }
66811
67431
  function sha256Hex(value) {
66812
- return createHash14("sha256").update(value).digest("hex");
67432
+ return createHash15("sha256").update(value).digest("hex");
66813
67433
  }
66814
67434
  function hmac(key, value) {
66815
67435
  return createHmac2("sha256", key).update(value).digest();