@hasna/todos 0.11.70 → 0.11.71

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.
@@ -1 +1 @@
1
- {"version":3,"file":"plan-template-commands.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/plan-template-commands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAczC,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,OAAO,QA6e5D"}
1
+ {"version":3,"file":"plan-template-commands.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/plan-template-commands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA8BzC,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,OAAO,QAqe5D"}
package/dist/cli/index.js CHANGED
@@ -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)
@@ -10333,15 +10379,76 @@ var init_task_relations = __esm(() => {
10333
10379
  });
10334
10380
 
10335
10381
  // src/db/plans.ts
10382
+ function planSlugBase2(value) {
10383
+ return slugify(value) || "plan";
10384
+ }
10385
+ function normalizePlanSlug(value) {
10386
+ const slug = slugify(value);
10387
+ if (!slug)
10388
+ throw new Error("Invalid plan slug");
10389
+ return slug;
10390
+ }
10391
+ function plansBySlug(slug, db, projectId) {
10392
+ if (projectId !== undefined) {
10393
+ if (projectId === null) {
10394
+ return db.query("SELECT * FROM plans WHERE slug = ? AND project_id IS NULL ORDER BY created_at ASC, id ASC").all(slug);
10395
+ }
10396
+ return db.query("SELECT * FROM plans WHERE slug = ? AND project_id = ? ORDER BY created_at ASC, id ASC").all(slug, projectId);
10397
+ }
10398
+ return db.query("SELECT * FROM plans WHERE slug = ? ORDER BY created_at ASC, id ASC").all(slug);
10399
+ }
10400
+ function planSlugExists(slug, projectId, db, excludeId) {
10401
+ const rows = plansBySlug(slug, db, projectId);
10402
+ return rows.some((plan) => plan.id !== excludeId);
10403
+ }
10404
+ function nextPlanSlug(base, projectId, db, excludeId) {
10405
+ let candidate = base;
10406
+ let suffix = 2;
10407
+ while (planSlugExists(candidate, projectId, db, excludeId)) {
10408
+ candidate = `${base}-${suffix}`;
10409
+ suffix += 1;
10410
+ }
10411
+ return candidate;
10412
+ }
10413
+ function resolveCreateSlug(input, projectId, db) {
10414
+ if (input.slug !== undefined) {
10415
+ const slug = normalizePlanSlug(input.slug);
10416
+ if (planSlugExists(slug, projectId, db)) {
10417
+ throw new Error(`Plan slug already exists in this scope: ${slug}`);
10418
+ }
10419
+ return slug;
10420
+ }
10421
+ return nextPlanSlug(planSlugBase2(input.name), projectId, db);
10422
+ }
10423
+ function resolvePlanRefDetailed(ref, db, projectId) {
10424
+ const d = db || getDatabase();
10425
+ const byId = d.query("SELECT * FROM plans WHERE id = ? OR id LIKE ? ORDER BY id").all(ref, `${ref}%`);
10426
+ if (byId.length === 1)
10427
+ return { id: byId[0].id, reason: "id", matches: byId };
10428
+ if (byId.length > 1)
10429
+ return { id: null, reason: "ambiguous", matches: byId };
10430
+ const slug = slugify(ref);
10431
+ if (!slug)
10432
+ return { id: null, reason: "not_found", matches: [] };
10433
+ const bySlug = plansBySlug(slug, d, projectId);
10434
+ if (bySlug.length === 1)
10435
+ return { id: bySlug[0].id, reason: "slug", matches: bySlug };
10436
+ if (bySlug.length > 1)
10437
+ return { id: null, reason: "ambiguous", matches: bySlug };
10438
+ return { id: null, reason: "not_found", matches: [] };
10439
+ }
10336
10440
  function createPlan(input, db) {
10337
10441
  const d = db || getDatabase();
10338
10442
  const id = uuid();
10339
10443
  const timestamp = now();
10444
+ const projectId = input.project_id || null;
10445
+ const slug = resolveCreateSlug(input, projectId, d);
10340
10446
  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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
10447
+ d.run(`INSERT INTO plans (id, slug, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at, machine_id)
10448
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
10343
10449
  id,
10344
- input.project_id || null,
10450
+ slug,
10451
+ projectId,
10345
10452
  input.task_list_id || null,
10346
10453
  input.agent_id || null,
10347
10454
  input.name,
@@ -10376,6 +10483,14 @@ function updatePlan(id, input, db) {
10376
10483
  sets.push("name = ?");
10377
10484
  params.push(input.name);
10378
10485
  }
10486
+ if (input.slug !== undefined) {
10487
+ const slug = normalizePlanSlug(input.slug);
10488
+ if (planSlugExists(slug, plan.project_id, d, id)) {
10489
+ throw new Error(`Plan slug already exists in this scope: ${slug}`);
10490
+ }
10491
+ sets.push("slug = ?");
10492
+ params.push(slug);
10493
+ }
10379
10494
  if (input.description !== undefined) {
10380
10495
  sets.push("description = ?");
10381
10496
  params.push(input.description);
@@ -10420,6 +10535,7 @@ var init_plans = __esm(() => {
10420
10535
  init_event_emission_safety();
10421
10536
  init_event_hooks();
10422
10537
  init_database();
10538
+ init_projects();
10423
10539
  init_storage_tombstones();
10424
10540
  });
10425
10541
 
@@ -13718,6 +13834,9 @@ function projectSlugMatches(project, ref) {
13718
13834
  const normalized = slugify(ref);
13719
13835
  return Boolean(normalized) && (project.task_list_id === normalized || slugify(project.name) === normalized);
13720
13836
  }
13837
+ function planArtifactSlug(plan) {
13838
+ return slugify(plan.slug || plan.name) || "plan";
13839
+ }
13721
13840
  function resolvePlanArtifactProject(input) {
13722
13841
  const db = input.db || getDatabase();
13723
13842
  const ref = input.project_id || input.project_ref;
@@ -13743,11 +13862,24 @@ function resolvePlanArtifactPaths(input) {
13743
13862
  const projectRoot = resolve10(project.path);
13744
13863
  const directory = join7(projectRoot, ".hasna", "todos", "plans", projectId);
13745
13864
  const planId = input.plan_id ? assertSafePathSegment(input.plan_id, "plan id") : null;
13865
+ const planSlug = input.plan_slug ? assertSafePathSegment(slugify(input.plan_slug), "plan slug") : null;
13866
+ const fileName = planId ? planSlug ? `${planSlug}--${planId.slice(0, 8)}.md` : `${planId}.md` : null;
13746
13867
  return {
13747
13868
  project_id: project.id,
13748
13869
  project_root: projectRoot,
13749
13870
  directory,
13750
- file_path: planId ? join7(directory, `${planId}.md`) : directory
13871
+ file_path: fileName ? join7(directory, fileName) : directory
13872
+ };
13873
+ }
13874
+ function resolvePlanArtifactCandidatePaths(plan, db) {
13875
+ return {
13876
+ primary: resolvePlanArtifactPaths({
13877
+ project_id: plan.project_id,
13878
+ plan_id: plan.id,
13879
+ plan_slug: planArtifactSlug(plan),
13880
+ db
13881
+ }),
13882
+ legacy: resolvePlanArtifactPaths({ project_id: plan.project_id, plan_id: plan.id, db })
13751
13883
  };
13752
13884
  }
13753
13885
  function buildPlanArtifactSnapshot(plan, tasks = [], artifactUpdatedAt = new Date().toISOString()) {
@@ -13764,6 +13896,7 @@ function buildPlanArtifactSnapshot(plan, tasks = [], artifactUpdatedAt = new Dat
13764
13896
  metadata: {
13765
13897
  schema: PLAN_MARKDOWN_SCHEMA,
13766
13898
  plan_id: plan.id,
13899
+ plan_slug: plan.slug ?? null,
13767
13900
  project_id: plan.project_id,
13768
13901
  task_list_id: plan.task_list_id ?? null,
13769
13902
  agent_id: plan.agent_id ?? null,
@@ -13803,6 +13936,7 @@ function renderPlanArtifactMarkdown(snapshot) {
13803
13936
  "---",
13804
13937
  `schema: ${frontmatterScalar(metadata.schema)}`,
13805
13938
  `plan_id: ${frontmatterScalar(metadata.plan_id)}`,
13939
+ `plan_slug: ${frontmatterScalar(metadata.plan_slug)}`,
13806
13940
  `project_id: ${frontmatterScalar(metadata.project_id)}`,
13807
13941
  `task_list_id: ${frontmatterScalar(metadata.task_list_id)}`,
13808
13942
  `agent_id: ${frontmatterScalar(metadata.agent_id)}`,
@@ -13848,6 +13982,7 @@ function parsePlanArtifactMarkdown(markdown) {
13848
13982
  metadata: {
13849
13983
  schema: PLAN_MARKDOWN_SCHEMA,
13850
13984
  plan_id: rawMetadata.plan_id,
13985
+ plan_slug: rawMetadata.plan_slug ?? null,
13851
13986
  project_id: rawMetadata.project_id,
13852
13987
  task_list_id: rawMetadata.task_list_id ?? null,
13853
13988
  agent_id: rawMetadata.agent_id ?? null,
@@ -13888,7 +14023,7 @@ function writePlanArtifact(plan, db) {
13888
14023
  return null;
13889
14024
  const d = db || getDatabase();
13890
14025
  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 });
14026
+ const paths = resolvePlanArtifactCandidatePaths(plan, d).primary;
13892
14027
  const snapshot = buildPlanArtifactSnapshot(plan, tasks);
13893
14028
  mkdirSync5(paths.directory, { recursive: true });
13894
14029
  writeFileSync3(paths.file_path, renderPlanArtifactMarkdown(snapshot), "utf8");
@@ -13898,12 +14033,13 @@ function readPlanArtifact(plan, db) {
13898
14033
  if (!plan.project_id)
13899
14034
  return null;
13900
14035
  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))
14036
+ const paths = resolvePlanArtifactCandidatePaths(plan, d);
14037
+ const path = existsSync8(paths.primary.file_path) ? paths.primary.file_path : existsSync8(paths.legacy.file_path) ? paths.legacy.file_path : null;
14038
+ if (!path)
13903
14039
  return null;
13904
- const markdown = readFileSync4(paths.file_path, "utf8");
14040
+ const markdown = readFileSync4(path, "utf8");
13905
14041
  return {
13906
- path: paths.file_path,
14042
+ path,
13907
14043
  markdown,
13908
14044
  ...parsePlanArtifactMarkdown(markdown)
13909
14045
  };
@@ -13912,10 +14048,11 @@ function inspectPlanArtifact(plan, db) {
13912
14048
  if (!plan.project_id)
13913
14049
  return null;
13914
14050
  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)) {
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) {
13917
14054
  return {
13918
- path: paths.file_path,
14055
+ path: paths.primary.file_path,
13919
14056
  exists: false,
13920
14057
  parse_error: null,
13921
14058
  metadata: null,
@@ -13924,9 +14061,9 @@ function inspectPlanArtifact(plan, db) {
13924
14061
  };
13925
14062
  }
13926
14063
  try {
13927
- const artifact = parsePlanArtifactMarkdown(readFileSync4(paths.file_path, "utf8"));
14064
+ const artifact = parsePlanArtifactMarkdown(readFileSync4(path, "utf8"));
13928
14065
  return {
13929
- path: paths.file_path,
14066
+ path,
13930
14067
  exists: true,
13931
14068
  parse_error: null,
13932
14069
  metadata: artifact.metadata,
@@ -13935,7 +14072,7 @@ function inspectPlanArtifact(plan, db) {
13935
14072
  };
13936
14073
  } catch (error) {
13937
14074
  return {
13938
- path: paths.file_path,
14075
+ path,
13939
14076
  exists: true,
13940
14077
  parse_error: error instanceof Error ? error.message : String(error),
13941
14078
  metadata: null,
@@ -13947,6 +14084,9 @@ function inspectPlanArtifact(plan, db) {
13947
14084
  function comparePlanArtifact(plan, artifact, tasks) {
13948
14085
  const conflicts = [];
13949
14086
  compare("plan_id", plan.id, artifact.metadata.plan_id, conflicts);
14087
+ if (artifact.metadata.plan_slug !== null) {
14088
+ compare("plan_slug", plan.slug ?? null, artifact.metadata.plan_slug, conflicts);
14089
+ }
13950
14090
  compare("project_id", plan.project_id ?? null, artifact.metadata.project_id, conflicts);
13951
14091
  compare("name", plan.name, artifact.metadata.name, conflicts);
13952
14092
  compare("status", plan.status, artifact.metadata.status, conflicts);
@@ -14316,22 +14456,44 @@ __export(exports_plan_template_commands, {
14316
14456
  registerPlanTemplateCommands: () => registerPlanTemplateCommands
14317
14457
  });
14318
14458
  import chalk3 from "chalk";
14459
+ function resolvePlanCliRef(ref, projectId) {
14460
+ const db = getDatabase();
14461
+ const resolved = resolvePlanRefDetailed(ref, db, projectId);
14462
+ if (resolved.id)
14463
+ return resolved.id;
14464
+ if (resolved.reason === "ambiguous") {
14465
+ console.error(chalk3.red(`Ambiguous plan reference: ${ref}`));
14466
+ if (resolved.matches.length > 0) {
14467
+ console.error(chalk3.dim(`Matches: ${resolved.matches.map((plan) => `${plan.slug ?? plan.name} (${plan.id.slice(0, 8)})`).join(", ")}`));
14468
+ }
14469
+ } else {
14470
+ console.error(chalk3.red(`Could not resolve plan ID or slug: ${ref}`));
14471
+ }
14472
+ process.exit(1);
14473
+ }
14319
14474
  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) => {
14475
+ 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
14476
  const globalOpts = program2.opts();
14322
14477
  const projectId = autoProject(globalOpts);
14323
14478
  if (opts.add) {
14324
- const plan = createPlan({
14325
- name: opts.add,
14326
- description: opts.description,
14327
- project_id: projectId
14328
- });
14479
+ let plan;
14480
+ try {
14481
+ plan = createPlan({
14482
+ name: opts.add,
14483
+ slug: opts.slug,
14484
+ description: opts.description,
14485
+ project_id: projectId
14486
+ });
14487
+ } catch (error) {
14488
+ handleError(error);
14489
+ }
14329
14490
  const artifact = writePlanArtifact(plan);
14330
14491
  if (globalOpts.json) {
14331
14492
  output(plan, true);
14332
14493
  } else {
14333
14494
  console.log(chalk3.green("Plan created:"));
14334
14495
  console.log(`${chalk3.dim(plan.id.slice(0, 8))} ${chalk3.bold(plan.name)} ${chalk3.cyan(`[${plan.status}]`)}`);
14496
+ console.log(`${chalk3.dim("Slug:")} ${plan.slug}`);
14335
14497
  if (artifact)
14336
14498
  console.log(`${chalk3.dim("Artifact:")} ${artifact.path}`);
14337
14499
  }
@@ -14339,11 +14501,7 @@ function registerPlanTemplateCommands(program2) {
14339
14501
  }
14340
14502
  if (opts.artifact) {
14341
14503
  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
- }
14504
+ const resolvedId = resolvePlanCliRef(opts.artifact, projectId);
14347
14505
  const plan = getPlan(resolvedId);
14348
14506
  if (!plan) {
14349
14507
  console.error(chalk3.red(`Plan not found: ${opts.artifact}`));
@@ -14396,11 +14554,7 @@ function registerPlanTemplateCommands(program2) {
14396
14554
  }
14397
14555
  if (opts.show) {
14398
14556
  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
- }
14557
+ const resolvedId = resolvePlanCliRef(opts.show, projectId);
14404
14558
  const plan = getPlan(resolvedId);
14405
14559
  if (!plan) {
14406
14560
  console.error(chalk3.red(`Plan not found: ${opts.show}`));
@@ -14425,6 +14579,8 @@ function registerPlanTemplateCommands(program2) {
14425
14579
  console.log(chalk3.bold(`Plan Details:
14426
14580
  `));
14427
14581
  console.log(` ${chalk3.dim("ID:")} ${plan.id}`);
14582
+ if (plan.slug)
14583
+ console.log(` ${chalk3.dim("Slug:")} ${plan.slug}`);
14428
14584
  console.log(` ${chalk3.dim("Name:")} ${plan.name}`);
14429
14585
  console.log(` ${chalk3.dim("Status:")} ${chalk3.cyan(plan.status)}`);
14430
14586
  if (plan.description)
@@ -14447,12 +14603,7 @@ function registerPlanTemplateCommands(program2) {
14447
14603
  return;
14448
14604
  }
14449
14605
  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
- }
14606
+ const resolvedId = resolvePlanCliRef(opts.delete, projectId);
14456
14607
  const deleted = deletePlan(resolvedId);
14457
14608
  if (globalOpts.json) {
14458
14609
  output({ deleted }, true);
@@ -14465,12 +14616,7 @@ function registerPlanTemplateCommands(program2) {
14465
14616
  return;
14466
14617
  }
14467
14618
  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
- }
14619
+ const resolvedId = resolvePlanCliRef(opts.complete, projectId);
14474
14620
  try {
14475
14621
  const plan = updatePlan(resolvedId, { status: "completed" });
14476
14622
  const artifact = writePlanArtifact(plan);
@@ -14500,7 +14646,8 @@ function registerPlanTemplateCommands(program2) {
14500
14646
  `));
14501
14647
  for (const p of plans) {
14502
14648
  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}`);
14649
+ const slug = p.slug ? chalk3.dim(` ${p.slug}`) : "";
14650
+ console.log(`${chalk3.dim(p.id.slice(0, 8))}${slug} ${chalk3.bold(p.name)} ${chalk3.cyan(`[${p.status}]`)}${desc}`);
14504
14651
  }
14505
14652
  });
14506
14653
  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 +22221,36 @@ function prepareValue(column, value) {
22074
22221
  return JSON.stringify(value ?? (column === "tags" || column === "files_changed" ? [] : {}));
22075
22222
  return value === undefined ? null : value;
22076
22223
  }
22224
+ function slugifyPlanValue(value) {
22225
+ return typeof value === "string" ? value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") : "";
22226
+ }
22227
+ function planSlugBase3(plan) {
22228
+ return slugifyPlanValue(plan.slug) || slugifyPlanValue(plan.name) || "plan";
22229
+ }
22230
+ function planSlugScope(projectId) {
22231
+ return typeof projectId === "string" && projectId ? projectId : "__global__";
22232
+ }
22233
+ function planSlugKey(projectId, slug) {
22234
+ return `${planSlugScope(projectId)}:${slug}`;
22235
+ }
22236
+ function normalizeBridgePlanSlugs(plans, db) {
22237
+ const existingRows = db.query("SELECT id, project_id, slug FROM plans WHERE slug IS NOT NULL").all();
22238
+ const existingIds = new Set(existingRows.map((row) => row.id));
22239
+ const used = new Set(existingRows.filter((row) => row.slug).map((row) => planSlugKey(row.project_id, row.slug)));
22240
+ return plans.map((plan) => {
22241
+ if (existingIds.has(plan.id))
22242
+ return plan;
22243
+ const base = planSlugBase3(plan);
22244
+ let candidate = base;
22245
+ let suffix = 2;
22246
+ while (used.has(planSlugKey(plan.project_id, candidate))) {
22247
+ candidate = `${base}-${suffix}`;
22248
+ suffix += 1;
22249
+ }
22250
+ used.add(planSlugKey(plan.project_id, candidate));
22251
+ return { ...plan, slug: candidate };
22252
+ });
22253
+ }
22077
22254
  function insertRecord(db, tableKey, row) {
22078
22255
  const table = tableByKey[tableKey];
22079
22256
  const columns = insertColumns[tableKey];
@@ -22210,6 +22387,7 @@ function importLocalBridgeBundle(bundle, options = {}, db) {
22210
22387
  const conflictStrategy = options.conflictStrategy ?? "skip";
22211
22388
  const data = {
22212
22389
  ...bundle.data,
22390
+ plans: normalizeBridgePlanSlugs(bundle.data.plans, d),
22213
22391
  tasks: sortedTasks(bundle.data.tasks),
22214
22392
  saved_views: bundle.data.saved_views ?? [],
22215
22393
  task_boards: bundle.data.task_boards ?? [],
@@ -22294,7 +22472,7 @@ var init_local_bridge = __esm(() => {
22294
22472
  insertColumns = {
22295
22473
  projects: ["id", "name", "path", "description", "task_list_id", "task_prefix", "task_counter", "created_at", "updated_at", "machine_id", "synced_at"],
22296
22474
  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"],
22475
+ plans: ["id", "slug", "project_id", "task_list_id", "agent_id", "name", "description", "status", "created_at", "updated_at", "machine_id", "synced_at"],
22298
22476
  tasks: [
22299
22477
  "id",
22300
22478
  "short_id",
@@ -28389,6 +28567,7 @@ async function handleCreatePlan(req, _ctx, json2) {
28389
28567
  return json2({ error: "Missing 'name'" }, 400);
28390
28568
  const plan = createPlan({
28391
28569
  name: body.name,
28570
+ slug: body.slug,
28392
28571
  description: body.description,
28393
28572
  project_id: body.project_id,
28394
28573
  task_list_id: body.task_list_id,
@@ -37738,6 +37917,7 @@ Tasks:` : null,
37738
37917
  if (shouldRegisterTool("create_plan")) {
37739
37918
  server.tool("create_plan", "Create a new plan (sprint/milestone).", {
37740
37919
  name: exports_external2.string().describe("Plan name"),
37920
+ slug: exports_external2.string().optional().describe("Readable plan slug"),
37741
37921
  project_id: exports_external2.string().optional().describe("Project ID"),
37742
37922
  description: exports_external2.string().optional(),
37743
37923
  start_date: exports_external2.string().optional().describe("ISO date"),
@@ -44898,6 +45078,7 @@ function createAgentProjectDemoBundle() {
44898
45078
  });
44899
45079
  data.plans.push({
44900
45080
  id: ids.plan,
45081
+ slug: "ship-local-demo-workflow",
44901
45082
  project_id: ids.project,
44902
45083
  task_list_id: ids.list,
44903
45084
  agent_id: "demo-agent",
@@ -66291,9 +66472,17 @@ async function updateProject2(id, input, store) {
66291
66472
  }
66292
66473
  async function createPlan2(input, store, context) {
66293
66474
  const timestamp3 = new Date().toISOString();
66475
+ const projectId = input.project_id ?? context?.projectId ?? null;
66476
+ const slug = await resolvePostgresPlanSlug({
66477
+ name: input.name,
66478
+ slug: input.slug,
66479
+ projectId,
66480
+ store
66481
+ });
66294
66482
  return store.upsert("plans", {
66295
66483
  id: randomUUID3(),
66296
- project_id: input.project_id ?? context?.projectId ?? null,
66484
+ slug,
66485
+ project_id: projectId,
66297
66486
  task_list_id: input.task_list_id ?? context?.taskListId ?? null,
66298
66487
  agent_id: input.agent_id ?? context?.agentId ?? null,
66299
66488
  name: input.name,
@@ -66307,7 +66496,17 @@ async function createPlan2(input, store, context) {
66307
66496
  }
66308
66497
  async function updatePlan2(id, input, store) {
66309
66498
  const plan = await requireRecord("plans", id, store);
66310
- return store.upsert("plans", { ...plan, ...definedPatch(input), updated_at: new Date().toISOString() });
66499
+ const patch = definedPatch(input);
66500
+ if (input.slug !== undefined) {
66501
+ patch.slug = await resolvePostgresPlanSlug({
66502
+ name: plan.name,
66503
+ slug: input.slug,
66504
+ projectId: plan.project_id,
66505
+ store,
66506
+ excludeId: id
66507
+ });
66508
+ }
66509
+ return store.upsert("plans", { ...plan, ...patch, updated_at: new Date().toISOString() });
66311
66510
  }
66312
66511
  async function registerAgent2(input, store, context) {
66313
66512
  const existing = (await store.list("agents")).find((agent2) => agent2.name === input.name && agent2.status !== "archived");
@@ -66560,8 +66759,38 @@ function matchesOne(value, expected) {
66560
66759
  function priorityRank2(priority) {
66561
66760
  return { critical: 0, high: 1, medium: 2, low: 3 }[priority];
66562
66761
  }
66762
+ function slugifyRaw(value) {
66763
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
66764
+ }
66563
66765
  function slugify2(value) {
66564
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "todos";
66766
+ return slugifyRaw(value) || "todos";
66767
+ }
66768
+ function normalizePlanSlug2(value) {
66769
+ const slug = slugifyRaw(value);
66770
+ if (!slug)
66771
+ throw new Error("Invalid plan slug");
66772
+ return slug;
66773
+ }
66774
+ function planSlugBase4(value) {
66775
+ return slugifyRaw(value) || "plan";
66776
+ }
66777
+ async function resolvePostgresPlanSlug(options) {
66778
+ const plans = await options.store.list("plans");
66779
+ const used = new Set(plans.filter((plan) => plan.project_id === options.projectId && plan.id !== options.excludeId && plan.slug).map((plan) => plan.slug));
66780
+ if (options.slug !== undefined) {
66781
+ const slug = normalizePlanSlug2(options.slug);
66782
+ if (used.has(slug))
66783
+ throw new Error(`Plan slug already exists in this scope: ${slug}`);
66784
+ return slug;
66785
+ }
66786
+ const base = planSlugBase4(options.name);
66787
+ let candidate = base;
66788
+ let suffix = 2;
66789
+ while (used.has(candidate)) {
66790
+ candidate = `${base}-${suffix}`;
66791
+ suffix += 1;
66792
+ }
66793
+ return candidate;
66565
66794
  }
66566
66795
  function definedPatch(value) {
66567
66796
  return Object.fromEntries(Object.entries(value).filter(([, entry2]) => entry2 !== undefined));