@hasna/todos 0.11.69 → 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.
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
- } else {}
1014
+ }
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)
@@ -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
 
@@ -13681,6 +13797,323 @@ var init_task_commands = __esm(() => {
13681
13797
  init_types();
13682
13798
  });
13683
13799
 
13800
+ // src/lib/plan-artifacts.ts
13801
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
13802
+ import { join as join7, resolve as resolve10 } from "path";
13803
+ function assertSafePathSegment(value, label) {
13804
+ const trimmed = value.trim();
13805
+ if (!trimmed || trimmed === "." || trimmed === ".." || trimmed.includes("/") || trimmed.includes("\\")) {
13806
+ throw new Error(`Invalid ${label} for plan artifact path`);
13807
+ }
13808
+ if (!/^[A-Za-z0-9._-]+$/.test(trimmed)) {
13809
+ throw new Error(`Invalid ${label} for plan artifact path`);
13810
+ }
13811
+ return trimmed;
13812
+ }
13813
+ function frontmatterScalar(value) {
13814
+ return JSON.stringify(value);
13815
+ }
13816
+ function parseFrontmatterScalar(value) {
13817
+ const trimmed = value.trim();
13818
+ if (trimmed === "null")
13819
+ return null;
13820
+ try {
13821
+ const parsed = JSON.parse(trimmed);
13822
+ if (parsed === null || typeof parsed === "string")
13823
+ return parsed;
13824
+ } catch {}
13825
+ return trimmed.replace(/^["']|["']$/g, "") || null;
13826
+ }
13827
+ function markdownEscape(text) {
13828
+ return text.replace(/<!--[\s\S]*?-->/g, "").trim();
13829
+ }
13830
+ function markdownLine(text) {
13831
+ return markdownEscape(text).replace(/\s+/g, " ").trim();
13832
+ }
13833
+ function projectSlugMatches(project, ref) {
13834
+ const normalized = slugify(ref);
13835
+ return Boolean(normalized) && (project.task_list_id === normalized || slugify(project.name) === normalized);
13836
+ }
13837
+ function planArtifactSlug(plan) {
13838
+ return slugify(plan.slug || plan.name) || "plan";
13839
+ }
13840
+ function resolvePlanArtifactProject(input) {
13841
+ const db = input.db || getDatabase();
13842
+ const ref = input.project_id || input.project_ref;
13843
+ if (!ref)
13844
+ throw new Error("Plan artifacts require a project id or project reference");
13845
+ const byPath = getProjectByPath(resolve10(ref), db);
13846
+ if (byPath)
13847
+ return byPath;
13848
+ const resolvedId = resolvePartialId(db, "projects", ref);
13849
+ if (resolvedId) {
13850
+ const project2 = getProject(resolvedId, db);
13851
+ if (project2)
13852
+ return project2;
13853
+ }
13854
+ const project = listProjects(db).find((candidate) => projectSlugMatches(candidate, ref));
13855
+ if (project)
13856
+ return project;
13857
+ throw new Error(`Project not found for plan artifacts: ${ref}`);
13858
+ }
13859
+ function resolvePlanArtifactPaths(input) {
13860
+ const project = resolvePlanArtifactProject(input);
13861
+ const projectId = assertSafePathSegment(project.id, "project id");
13862
+ const projectRoot = resolve10(project.path);
13863
+ const directory = join7(projectRoot, ".hasna", "todos", "plans", projectId);
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;
13867
+ return {
13868
+ project_id: project.id,
13869
+ project_root: projectRoot,
13870
+ 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 })
13883
+ };
13884
+ }
13885
+ function buildPlanArtifactSnapshot(plan, tasks = [], artifactUpdatedAt = new Date().toISOString()) {
13886
+ if (!plan.project_id)
13887
+ throw new Error("Plan artifacts require a project-scoped plan");
13888
+ const taskReferences = tasks.map((task) => ({
13889
+ task_id: task.id,
13890
+ title: task.title,
13891
+ status: task.status,
13892
+ priority: task.priority
13893
+ }));
13894
+ const body = renderPlanArtifactBody(plan, taskReferences);
13895
+ return {
13896
+ metadata: {
13897
+ schema: PLAN_MARKDOWN_SCHEMA,
13898
+ plan_id: plan.id,
13899
+ plan_slug: plan.slug ?? null,
13900
+ project_id: plan.project_id,
13901
+ task_list_id: plan.task_list_id ?? null,
13902
+ agent_id: plan.agent_id ?? null,
13903
+ stable_id: plan.id,
13904
+ name: plan.name,
13905
+ status: plan.status,
13906
+ created_at: plan.created_at,
13907
+ updated_at: plan.updated_at,
13908
+ artifact_updated_at: artifactUpdatedAt
13909
+ },
13910
+ task_references: taskReferences,
13911
+ body
13912
+ };
13913
+ }
13914
+ function renderPlanArtifactBody(plan, tasks) {
13915
+ const lines = [`# ${markdownLine(plan.name) || plan.id}`, ""];
13916
+ if (plan.description?.trim()) {
13917
+ lines.push(markdownEscape(plan.description), "");
13918
+ }
13919
+ lines.push("## Tasks", "");
13920
+ if (tasks.length === 0) {
13921
+ lines.push("_No tasks are currently attached to this plan._", "");
13922
+ } else {
13923
+ for (const task of tasks) {
13924
+ const check = task.status === "completed" ? "x" : " ";
13925
+ lines.push(`- [${check}] ${markdownLine(task.title) || task.task_id}`);
13926
+ lines.push(` <!-- todos: task_id=${task.task_id} status=${task.status} priority=${task.priority} -->`);
13927
+ }
13928
+ lines.push("");
13929
+ }
13930
+ return lines.join(`
13931
+ `);
13932
+ }
13933
+ function renderPlanArtifactMarkdown(snapshot) {
13934
+ const metadata = snapshot.metadata;
13935
+ const lines = [
13936
+ "---",
13937
+ `schema: ${frontmatterScalar(metadata.schema)}`,
13938
+ `plan_id: ${frontmatterScalar(metadata.plan_id)}`,
13939
+ `plan_slug: ${frontmatterScalar(metadata.plan_slug)}`,
13940
+ `project_id: ${frontmatterScalar(metadata.project_id)}`,
13941
+ `task_list_id: ${frontmatterScalar(metadata.task_list_id)}`,
13942
+ `agent_id: ${frontmatterScalar(metadata.agent_id)}`,
13943
+ `stable_id: ${frontmatterScalar(metadata.stable_id)}`,
13944
+ `name: ${frontmatterScalar(metadata.name)}`,
13945
+ `status: ${frontmatterScalar(metadata.status)}`,
13946
+ `created_at: ${frontmatterScalar(metadata.created_at)}`,
13947
+ `updated_at: ${frontmatterScalar(metadata.updated_at)}`,
13948
+ `artifact_updated_at: ${frontmatterScalar(metadata.artifact_updated_at)}`,
13949
+ "---",
13950
+ "",
13951
+ snapshot.body
13952
+ ];
13953
+ return `${lines.join(`
13954
+ `).replace(/\n{3,}/g, `
13955
+
13956
+ `).trimEnd()}
13957
+ `;
13958
+ }
13959
+ function parsePlanArtifactMarkdown(markdown) {
13960
+ const match = markdown.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
13961
+ if (!match)
13962
+ throw new Error("Invalid plan artifact: missing frontmatter");
13963
+ const rawMetadata = {};
13964
+ for (const line of match[1].split(/\r?\n/)) {
13965
+ const separator = line.indexOf(":");
13966
+ if (separator === -1)
13967
+ continue;
13968
+ const key = line.slice(0, separator).trim();
13969
+ const value = line.slice(separator + 1);
13970
+ rawMetadata[key] = parseFrontmatterScalar(value);
13971
+ }
13972
+ if (rawMetadata.schema !== PLAN_MARKDOWN_SCHEMA) {
13973
+ throw new Error(`Unsupported plan artifact schema: ${rawMetadata.schema ?? "unknown"}`);
13974
+ }
13975
+ const required = ["plan_id", "project_id", "stable_id", "name", "status", "created_at", "updated_at", "artifact_updated_at"];
13976
+ for (const key of required) {
13977
+ if (!rawMetadata[key])
13978
+ throw new Error(`Invalid plan artifact: missing ${key}`);
13979
+ }
13980
+ const body = match[2] ?? "";
13981
+ return {
13982
+ metadata: {
13983
+ schema: PLAN_MARKDOWN_SCHEMA,
13984
+ plan_id: rawMetadata.plan_id,
13985
+ plan_slug: rawMetadata.plan_slug ?? null,
13986
+ project_id: rawMetadata.project_id,
13987
+ task_list_id: rawMetadata.task_list_id ?? null,
13988
+ agent_id: rawMetadata.agent_id ?? null,
13989
+ stable_id: rawMetadata.stable_id,
13990
+ name: rawMetadata.name,
13991
+ status: rawMetadata.status,
13992
+ created_at: rawMetadata.created_at,
13993
+ updated_at: rawMetadata.updated_at,
13994
+ artifact_updated_at: rawMetadata.artifact_updated_at
13995
+ },
13996
+ task_references: parseTaskReferences(body),
13997
+ body
13998
+ };
13999
+ }
14000
+ function parseTaskReferences(body) {
14001
+ const references = [];
14002
+ const taskLine = /^\s*-\s+\[[ xX]\]\s+(.+)$/;
14003
+ const metadataLine = /<!--\s*todos:\s*task_id=([A-Za-z0-9._-]+)\s+status=([A-Za-z_]+)\s+priority=([A-Za-z_]+)\s*-->/;
14004
+ const lines = body.split(/\r?\n/);
14005
+ for (let index = 0;index < lines.length; index++) {
14006
+ const titleMatch = lines[index].match(taskLine);
14007
+ if (!titleMatch)
14008
+ continue;
14009
+ const metadataMatch = lines[index + 1]?.match(metadataLine);
14010
+ if (!metadataMatch)
14011
+ continue;
14012
+ references.push({
14013
+ task_id: metadataMatch[1],
14014
+ title: titleMatch[1].trim(),
14015
+ status: metadataMatch[2],
14016
+ priority: metadataMatch[3]
14017
+ });
14018
+ }
14019
+ return references;
14020
+ }
14021
+ function writePlanArtifact(plan, db) {
14022
+ if (!plan.project_id)
14023
+ return null;
14024
+ const d = db || getDatabase();
14025
+ const tasks = listTasks({ plan_id: plan.id, include_archived: true }, d);
14026
+ const paths = resolvePlanArtifactCandidatePaths(plan, d).primary;
14027
+ const snapshot = buildPlanArtifactSnapshot(plan, tasks);
14028
+ mkdirSync5(paths.directory, { recursive: true });
14029
+ writeFileSync3(paths.file_path, renderPlanArtifactMarkdown(snapshot), "utf8");
14030
+ return { path: paths.file_path, snapshot };
14031
+ }
14032
+ function readPlanArtifact(plan, db) {
14033
+ if (!plan.project_id)
14034
+ return null;
14035
+ const d = db || getDatabase();
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)
14039
+ return null;
14040
+ const markdown = readFileSync4(path, "utf8");
14041
+ return {
14042
+ path,
14043
+ markdown,
14044
+ ...parsePlanArtifactMarkdown(markdown)
14045
+ };
14046
+ }
14047
+ function inspectPlanArtifact(plan, db) {
14048
+ if (!plan.project_id)
14049
+ return null;
14050
+ const d = db || getDatabase();
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) {
14054
+ return {
14055
+ path: paths.primary.file_path,
14056
+ exists: false,
14057
+ parse_error: null,
14058
+ metadata: null,
14059
+ task_references: [],
14060
+ conflicts: []
14061
+ };
14062
+ }
14063
+ try {
14064
+ const artifact = parsePlanArtifactMarkdown(readFileSync4(path, "utf8"));
14065
+ return {
14066
+ path,
14067
+ exists: true,
14068
+ parse_error: null,
14069
+ metadata: artifact.metadata,
14070
+ task_references: artifact.task_references,
14071
+ conflicts: comparePlanArtifact(plan, artifact, listTasks({ plan_id: plan.id, include_archived: true }, d))
14072
+ };
14073
+ } catch (error) {
14074
+ return {
14075
+ path,
14076
+ exists: true,
14077
+ parse_error: error instanceof Error ? error.message : String(error),
14078
+ metadata: null,
14079
+ task_references: [],
14080
+ conflicts: []
14081
+ };
14082
+ }
14083
+ }
14084
+ function comparePlanArtifact(plan, artifact, tasks) {
14085
+ const conflicts = [];
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
+ }
14090
+ compare("project_id", plan.project_id ?? null, artifact.metadata.project_id, conflicts);
14091
+ compare("name", plan.name, artifact.metadata.name, conflicts);
14092
+ compare("status", plan.status, artifact.metadata.status, conflicts);
14093
+ compare("updated_at", plan.updated_at, artifact.metadata.updated_at, conflicts);
14094
+ const dbTaskIds = tasks.map((task) => task.id).sort();
14095
+ const artifactTaskIds = artifact.task_references.map((task) => task.task_id).sort();
14096
+ if (dbTaskIds.join(",") !== artifactTaskIds.join(",")) {
14097
+ conflicts.push({
14098
+ field: "task_references",
14099
+ database: dbTaskIds.join(",") || null,
14100
+ artifact: artifactTaskIds.join(",") || null
14101
+ });
14102
+ }
14103
+ return conflicts;
14104
+ }
14105
+ function compare(field, database, artifact, conflicts) {
14106
+ if ((database ?? null) !== (artifact ?? null)) {
14107
+ conflicts.push({ field, database: database ?? null, artifact: artifact ?? null });
14108
+ }
14109
+ }
14110
+ var PLAN_MARKDOWN_SCHEMA = "hasna.todos.plan/v1";
14111
+ var init_plan_artifacts = __esm(() => {
14112
+ init_database();
14113
+ init_projects();
14114
+ init_tasks();
14115
+ });
14116
+
13684
14117
  // src/db/builtin-templates.ts
13685
14118
  var exports_builtin_templates = {};
13686
14119
  __export(exports_builtin_templates, {
@@ -13695,8 +14128,8 @@ __export(exports_builtin_templates, {
13695
14128
  BUILTIN_TEMPLATE_LIBRARY_SOURCE: () => BUILTIN_TEMPLATE_LIBRARY_SOURCE,
13696
14129
  BUILTIN_TEMPLATES: () => BUILTIN_TEMPLATES
13697
14130
  });
13698
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3 } from "fs";
13699
- import { join as join7 } from "path";
14131
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync4 } from "fs";
14132
+ import { join as join8 } from "path";
13700
14133
  function templateMetadata(template) {
13701
14134
  return {
13702
14135
  source: BUILTIN_TEMPLATE_LIBRARY_SOURCE,
@@ -13752,11 +14185,11 @@ function exportBuiltinTemplateFiles() {
13752
14185
  }));
13753
14186
  }
13754
14187
  function writeBuiltinTemplateFiles(directory) {
13755
- mkdirSync5(directory, { recursive: true });
14188
+ mkdirSync6(directory, { recursive: true });
13756
14189
  const files = [];
13757
14190
  for (const entry of exportBuiltinTemplateFiles()) {
13758
- const path = join7(directory, entry.filename);
13759
- writeFileSync3(path, `${JSON.stringify(entry.template, null, 2)}
14191
+ const path = join8(directory, entry.filename);
14192
+ writeFileSync4(path, `${JSON.stringify(entry.template, null, 2)}
13760
14193
  `, "utf-8");
13761
14194
  files.push(path);
13762
14195
  }
@@ -14023,31 +14456,105 @@ __export(exports_plan_template_commands, {
14023
14456
  registerPlanTemplateCommands: () => registerPlanTemplateCommands
14024
14457
  });
14025
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
+ }
14026
14474
  function registerPlanTemplateCommands(program2) {
14027
- 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("--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) => {
14028
14476
  const globalOpts = program2.opts();
14029
14477
  const projectId = autoProject(globalOpts);
14030
14478
  if (opts.add) {
14031
- const plan = createPlan({
14032
- name: opts.add,
14033
- description: opts.description,
14034
- project_id: projectId
14035
- });
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
+ }
14490
+ const artifact = writePlanArtifact(plan);
14036
14491
  if (globalOpts.json) {
14037
14492
  output(plan, true);
14038
14493
  } else {
14039
14494
  console.log(chalk3.green("Plan created:"));
14040
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}`);
14497
+ if (artifact)
14498
+ console.log(`${chalk3.dim("Artifact:")} ${artifact.path}`);
14041
14499
  }
14042
14500
  return;
14043
14501
  }
14044
- if (opts.show) {
14502
+ if (opts.artifact) {
14045
14503
  const db = getDatabase();
14046
- const resolvedId = resolvePartialId(db, "plans", opts.show);
14047
- if (!resolvedId) {
14048
- console.error(chalk3.red(`Could not resolve plan ID: ${opts.show}`));
14504
+ const resolvedId = resolvePlanCliRef(opts.artifact, projectId);
14505
+ const plan = getPlan(resolvedId);
14506
+ if (!plan) {
14507
+ console.error(chalk3.red(`Plan not found: ${opts.artifact}`));
14049
14508
  process.exit(1);
14050
14509
  }
14510
+ const inspection = inspectPlanArtifact(plan, db);
14511
+ if (!inspection) {
14512
+ const result = { plan_id: plan.id, artifact: null, reason: "plan is not project-scoped" };
14513
+ if (globalOpts.json)
14514
+ output(result, true);
14515
+ else
14516
+ console.log(chalk3.dim("Plan is not project-scoped; no local Markdown artifact path is available."));
14517
+ return;
14518
+ }
14519
+ if (globalOpts.json) {
14520
+ output({ plan, artifact: inspection }, true);
14521
+ return;
14522
+ }
14523
+ console.log(chalk3.bold(`Plan Artifact:
14524
+ `));
14525
+ console.log(` ${chalk3.dim("Plan:")} ${plan.id}`);
14526
+ console.log(` ${chalk3.dim("Path:")} ${inspection.path}`);
14527
+ console.log(` ${chalk3.dim("Exists:")} ${inspection.exists ? "yes" : "no"}`);
14528
+ if (inspection.parse_error)
14529
+ console.log(` ${chalk3.dim("Parse:")} ${chalk3.red(inspection.parse_error)}`);
14530
+ console.log(` ${chalk3.dim("Conflicts:")} ${inspection.conflicts.length}`);
14531
+ for (const conflict of inspection.conflicts) {
14532
+ console.log(` ${conflict.field}: db=${conflict.database ?? "null"} artifact=${conflict.artifact ?? "null"}`);
14533
+ }
14534
+ return;
14535
+ }
14536
+ if (opts.writeArtifacts) {
14537
+ const plans2 = listPlans(projectId);
14538
+ const written = plans2.map((plan) => ({ plan, artifact: writePlanArtifact(plan) })).filter((entry) => entry.artifact);
14539
+ const result = {
14540
+ count: written.length,
14541
+ artifacts: written.map((entry) => ({
14542
+ plan_id: entry.plan.id,
14543
+ path: entry.artifact.path
14544
+ }))
14545
+ };
14546
+ if (globalOpts.json) {
14547
+ output(result, true);
14548
+ } else {
14549
+ console.log(chalk3.green(`Wrote ${written.length} plan artifact(s).`));
14550
+ for (const artifact of result.artifacts)
14551
+ console.log(`${chalk3.dim(artifact.plan_id.slice(0, 8))} ${artifact.path}`);
14552
+ }
14553
+ return;
14554
+ }
14555
+ if (opts.show) {
14556
+ const db = getDatabase();
14557
+ const resolvedId = resolvePlanCliRef(opts.show, projectId);
14051
14558
  const plan = getPlan(resolvedId);
14052
14559
  if (!plan) {
14053
14560
  console.error(chalk3.red(`Plan not found: ${opts.show}`));
@@ -14055,19 +14562,33 @@ function registerPlanTemplateCommands(program2) {
14055
14562
  }
14056
14563
  const { listTasks: listTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
14057
14564
  const tasks = listTasks2({ plan_id: resolvedId });
14565
+ const artifact = readPlanArtifact(plan, db);
14058
14566
  if (globalOpts.json) {
14059
- output({ plan, tasks }, true);
14567
+ output({
14568
+ plan,
14569
+ tasks,
14570
+ artifact: artifact ? {
14571
+ path: artifact.path,
14572
+ metadata: artifact.metadata,
14573
+ task_references: artifact.task_references,
14574
+ body: artifact.body
14575
+ } : null
14576
+ }, true);
14060
14577
  return;
14061
14578
  }
14062
14579
  console.log(chalk3.bold(`Plan Details:
14063
14580
  `));
14064
14581
  console.log(` ${chalk3.dim("ID:")} ${plan.id}`);
14582
+ if (plan.slug)
14583
+ console.log(` ${chalk3.dim("Slug:")} ${plan.slug}`);
14065
14584
  console.log(` ${chalk3.dim("Name:")} ${plan.name}`);
14066
14585
  console.log(` ${chalk3.dim("Status:")} ${chalk3.cyan(plan.status)}`);
14067
14586
  if (plan.description)
14068
14587
  console.log(` ${chalk3.dim("Desc:")} ${plan.description}`);
14069
14588
  if (plan.project_id)
14070
14589
  console.log(` ${chalk3.dim("Project:")} ${plan.project_id}`);
14590
+ if (artifact)
14591
+ console.log(` ${chalk3.dim("Artifact:")} ${artifact.path}`);
14071
14592
  console.log(` ${chalk3.dim("Created:")} ${plan.created_at}`);
14072
14593
  if (tasks.length > 0) {
14073
14594
  console.log(chalk3.bold(`
@@ -14082,12 +14603,7 @@ function registerPlanTemplateCommands(program2) {
14082
14603
  return;
14083
14604
  }
14084
14605
  if (opts.delete) {
14085
- const db = getDatabase();
14086
- const resolvedId = resolvePartialId(db, "plans", opts.delete);
14087
- if (!resolvedId) {
14088
- console.error(chalk3.red(`Could not resolve plan ID: ${opts.delete}`));
14089
- process.exit(1);
14090
- }
14606
+ const resolvedId = resolvePlanCliRef(opts.delete, projectId);
14091
14607
  const deleted = deletePlan(resolvedId);
14092
14608
  if (globalOpts.json) {
14093
14609
  output({ deleted }, true);
@@ -14100,19 +14616,17 @@ function registerPlanTemplateCommands(program2) {
14100
14616
  return;
14101
14617
  }
14102
14618
  if (opts.complete) {
14103
- const db = getDatabase();
14104
- const resolvedId = resolvePartialId(db, "plans", opts.complete);
14105
- if (!resolvedId) {
14106
- console.error(chalk3.red(`Could not resolve plan ID: ${opts.complete}`));
14107
- process.exit(1);
14108
- }
14619
+ const resolvedId = resolvePlanCliRef(opts.complete, projectId);
14109
14620
  try {
14110
14621
  const plan = updatePlan(resolvedId, { status: "completed" });
14622
+ const artifact = writePlanArtifact(plan);
14111
14623
  if (globalOpts.json) {
14112
14624
  output(plan, true);
14113
14625
  } else {
14114
14626
  console.log(chalk3.green("Plan completed:"));
14115
14627
  console.log(`${chalk3.dim(plan.id.slice(0, 8))} ${chalk3.bold(plan.name)} ${chalk3.cyan(`[${plan.status}]`)}`);
14628
+ if (artifact)
14629
+ console.log(`${chalk3.dim("Artifact:")} ${artifact.path}`);
14116
14630
  }
14117
14631
  } catch (e) {
14118
14632
  handleError(e);
@@ -14132,7 +14646,8 @@ function registerPlanTemplateCommands(program2) {
14132
14646
  `));
14133
14647
  for (const p of plans) {
14134
14648
  const desc = p.description ? chalk3.dim(` - ${p.description}`) : "";
14135
- 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}`);
14136
14651
  }
14137
14652
  });
14138
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) => {
@@ -14390,14 +14905,14 @@ function registerPlanTemplateCommands(program2) {
14390
14905
  program2.command("template-import [file]").alias("templates-import").description("Import a template from a JSON file").option("--file <path>", "Path to template JSON file (alternative to positional arg)").action(async (file, opts) => {
14391
14906
  const globalOpts = program2.opts();
14392
14907
  const { importTemplate: importTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
14393
- const { readFileSync: readFileSync4 } = await import("fs");
14908
+ const { readFileSync: readFileSync5 } = await import("fs");
14394
14909
  try {
14395
14910
  const filePath = file || opts.file;
14396
14911
  if (!filePath) {
14397
14912
  console.error(chalk3.red("Provide a file path: todos template-import <file> or --file <path>"));
14398
14913
  process.exit(1);
14399
14914
  }
14400
- const content = readFileSync4(filePath, "utf-8");
14915
+ const content = readFileSync5(filePath, "utf-8");
14401
14916
  const json = JSON.parse(content);
14402
14917
  const template = importTemplate2(json);
14403
14918
  if (globalOpts.json) {
@@ -14441,6 +14956,7 @@ var init_plan_template_commands = __esm(() => {
14441
14956
  init_database();
14442
14957
  init_plans();
14443
14958
  init_tasks();
14959
+ init_plan_artifacts();
14444
14960
  init_helpers();
14445
14961
  });
14446
14962
 
@@ -15009,16 +15525,16 @@ var init_saved_search_views = __esm(() => {
15009
15525
  });
15010
15526
 
15011
15527
  // src/lib/claude-tasks.ts
15012
- import { existsSync as existsSync8, readFileSync as readFileSync4, readdirSync as readdirSync2, writeFileSync as writeFileSync4 } from "fs";
15013
- import { join as join8 } from "path";
15528
+ import { existsSync as existsSync9, readFileSync as readFileSync5, readdirSync as readdirSync2, writeFileSync as writeFileSync5 } from "fs";
15529
+ import { join as join9 } from "path";
15014
15530
  function getTaskListDir(taskListId) {
15015
- return join8(HOME, ".claude", "tasks", taskListId);
15531
+ return join9(HOME, ".claude", "tasks", taskListId);
15016
15532
  }
15017
15533
  function readClaudeTask(dir, filename) {
15018
- return readJsonFile(join8(dir, filename));
15534
+ return readJsonFile(join9(dir, filename));
15019
15535
  }
15020
15536
  function writeClaudeTask(dir, task) {
15021
- writeJsonFile(join8(dir, `${task.id}.json`), task);
15537
+ writeJsonFile(join9(dir, `${task.id}.json`), task);
15022
15538
  }
15023
15539
  function toClaudeStatus(status) {
15024
15540
  if (status === "pending" || status === "in_progress" || status === "completed") {
@@ -15030,14 +15546,14 @@ function toSqliteStatus(status) {
15030
15546
  return status;
15031
15547
  }
15032
15548
  function readPrefixCounter(dir) {
15033
- const path = join8(dir, ".prefix-counter");
15034
- if (!existsSync8(path))
15549
+ const path = join9(dir, ".prefix-counter");
15550
+ if (!existsSync9(path))
15035
15551
  return 0;
15036
- const val = parseInt(readFileSync4(path, "utf-8").trim(), 10);
15552
+ const val = parseInt(readFileSync5(path, "utf-8").trim(), 10);
15037
15553
  return isNaN(val) ? 0 : val;
15038
15554
  }
15039
15555
  function writePrefixCounter(dir, value) {
15040
- writeFileSync4(join8(dir, ".prefix-counter"), String(value));
15556
+ writeFileSync5(join9(dir, ".prefix-counter"), String(value));
15041
15557
  }
15042
15558
  function formatPrefixedSubject(title, prefix, counter) {
15043
15559
  const padded = String(counter).padStart(5, "0");
@@ -15064,7 +15580,7 @@ function taskToClaudeTask(task, claudeTaskId, existingMeta) {
15064
15580
  }
15065
15581
  function pushToClaudeTaskList(taskListId, projectId, options = {}) {
15066
15582
  const dir = getTaskListDir(taskListId);
15067
- if (!existsSync8(dir))
15583
+ if (!existsSync9(dir))
15068
15584
  ensureDir2(dir);
15069
15585
  const filter = {};
15070
15586
  if (projectId)
@@ -15073,7 +15589,7 @@ function pushToClaudeTaskList(taskListId, projectId, options = {}) {
15073
15589
  const existingByTodosId = new Map;
15074
15590
  const files = listJsonFiles(dir);
15075
15591
  for (const f of files) {
15076
- const path = join8(dir, f);
15592
+ const path = join9(dir, f);
15077
15593
  const ct = readClaudeTask(dir, f);
15078
15594
  if (ct?.metadata?.["todos_id"]) {
15079
15595
  existingByTodosId.set(ct.metadata["todos_id"], { task: ct, mtimeMs: getFileMtimeMs(path) });
@@ -15160,7 +15676,7 @@ function pushToClaudeTaskList(taskListId, projectId, options = {}) {
15160
15676
  }
15161
15677
  function pullFromClaudeTaskList(taskListId, projectId, options = {}) {
15162
15678
  const dir = getTaskListDir(taskListId);
15163
- if (!existsSync8(dir)) {
15679
+ if (!existsSync9(dir)) {
15164
15680
  return { pushed: 0, pulled: 0, errors: [`Task list directory not found: ${dir}`] };
15165
15681
  }
15166
15682
  const files = readdirSync2(dir).filter((f) => f.endsWith(".json"));
@@ -15180,7 +15696,7 @@ function pullFromClaudeTaskList(taskListId, projectId, options = {}) {
15180
15696
  }
15181
15697
  for (const f of files) {
15182
15698
  try {
15183
- const filePath = join8(dir, f);
15699
+ const filePath = join9(dir, f);
15184
15700
  const ct = readClaudeTask(dir, f);
15185
15701
  if (!ct)
15186
15702
  continue;
@@ -15253,20 +15769,20 @@ var init_claude_tasks = __esm(() => {
15253
15769
  });
15254
15770
 
15255
15771
  // src/lib/agent-tasks.ts
15256
- import { existsSync as existsSync9 } from "fs";
15257
- import { join as join9 } from "path";
15772
+ import { existsSync as existsSync10 } from "fs";
15773
+ import { join as join10 } from "path";
15258
15774
  function agentBaseDir(agent) {
15259
15775
  const key = `TODOS_${agent.toUpperCase()}_TASKS_DIR`;
15260
- return process.env[key] || getAgentTasksDir(agent) || process.env["TODOS_AGENT_TASKS_DIR"] || join9(getTodosGlobalDir(), "agents");
15776
+ return process.env[key] || getAgentTasksDir(agent) || process.env["TODOS_AGENT_TASKS_DIR"] || join10(getTodosGlobalDir(), "agents");
15261
15777
  }
15262
15778
  function getTaskListDir2(agent, taskListId) {
15263
- return join9(agentBaseDir(agent), agent, taskListId);
15779
+ return join10(agentBaseDir(agent), agent, taskListId);
15264
15780
  }
15265
15781
  function readAgentTask(dir, filename) {
15266
- return readJsonFile(join9(dir, filename));
15782
+ return readJsonFile(join10(dir, filename));
15267
15783
  }
15268
15784
  function writeAgentTask(dir, task) {
15269
- writeJsonFile(join9(dir, `${task.id}.json`), task);
15785
+ writeJsonFile(join10(dir, `${task.id}.json`), task);
15270
15786
  }
15271
15787
  function taskToAgentTask(task, externalId, existingMeta) {
15272
15788
  return {
@@ -15291,7 +15807,7 @@ function metadataKey(agent) {
15291
15807
  }
15292
15808
  function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
15293
15809
  const dir = getTaskListDir2(agent, taskListId);
15294
- if (!existsSync9(dir))
15810
+ if (!existsSync10(dir))
15295
15811
  ensureDir2(dir);
15296
15812
  const filter = {};
15297
15813
  if (projectId)
@@ -15300,7 +15816,7 @@ function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
15300
15816
  const existingByTodosId = new Map;
15301
15817
  const files = listJsonFiles(dir);
15302
15818
  for (const f of files) {
15303
- const path = join9(dir, f);
15819
+ const path = join10(dir, f);
15304
15820
  const at = readAgentTask(dir, f);
15305
15821
  if (at?.metadata?.["todos_id"]) {
15306
15822
  existingByTodosId.set(at.metadata["todos_id"], { task: at, mtimeMs: getFileMtimeMs(path) });
@@ -15374,7 +15890,7 @@ function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
15374
15890
  }
15375
15891
  function pullFromAgentTaskList(agent, taskListId, projectId, options = {}) {
15376
15892
  const dir = getTaskListDir2(agent, taskListId);
15377
- if (!existsSync9(dir)) {
15893
+ if (!existsSync10(dir)) {
15378
15894
  return { pushed: 0, pulled: 0, errors: [`Task list directory not found: ${dir}`] };
15379
15895
  }
15380
15896
  const files = listJsonFiles(dir);
@@ -15393,7 +15909,7 @@ function pullFromAgentTaskList(agent, taskListId, projectId, options = {}) {
15393
15909
  }
15394
15910
  for (const f of files) {
15395
15911
  try {
15396
- const filePath = join9(dir, f);
15912
+ const filePath = join10(dir, f);
15397
15913
  const at = readAgentTask(dir, f);
15398
15914
  if (!at)
15399
15915
  continue;
@@ -15546,8 +16062,8 @@ __export(exports_project_bootstrap, {
15546
16062
  discoverProjectWorkspace: () => discoverProjectWorkspace,
15547
16063
  bootstrapProject: () => bootstrapProject
15548
16064
  });
15549
- import { existsSync as existsSync10, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
15550
- import { basename as basename4, dirname as dirname6, resolve as resolve10 } from "path";
16065
+ import { existsSync as existsSync11, readFileSync as readFileSync6, statSync as statSync3 } from "fs";
16066
+ import { basename as basename4, dirname as dirname6, resolve as resolve11 } from "path";
15551
16067
  function safeStat(path) {
15552
16068
  try {
15553
16069
  return statSync3(path);
@@ -15556,7 +16072,7 @@ function safeStat(path) {
15556
16072
  }
15557
16073
  }
15558
16074
  function canonicalPath(input) {
15559
- const resolved = resolve10(input);
16075
+ const resolved = resolve11(input);
15560
16076
  const stats = safeStat(resolved);
15561
16077
  if (stats?.isFile())
15562
16078
  return dirname6(resolved);
@@ -15565,7 +16081,7 @@ function canonicalPath(input) {
15565
16081
  function findUp(start, marker) {
15566
16082
  let current = canonicalPath(start);
15567
16083
  while (true) {
15568
- if (existsSync10(resolve10(current, marker)))
16084
+ if (existsSync11(resolve11(current, marker)))
15569
16085
  return current;
15570
16086
  const parent = dirname6(current);
15571
16087
  if (parent === current)
@@ -15576,11 +16092,11 @@ function findUp(start, marker) {
15576
16092
  function readPackageJson(path) {
15577
16093
  if (!path)
15578
16094
  return null;
15579
- const file = resolve10(path, "package.json");
15580
- if (!existsSync10(file))
16095
+ const file = resolve11(path, "package.json");
16096
+ if (!existsSync11(file))
15581
16097
  return null;
15582
16098
  try {
15583
- const parsed = JSON.parse(readFileSync5(file, "utf-8"));
16099
+ const parsed = JSON.parse(readFileSync6(file, "utf-8"));
15584
16100
  return parsed && typeof parsed === "object" ? parsed : null;
15585
16101
  } catch {
15586
16102
  return null;
@@ -15599,7 +16115,7 @@ function workspaceMarker(root, rootPackage) {
15599
16115
  if (rootPackage?.workspaces)
15600
16116
  markers.push("package.json#workspaces");
15601
16117
  for (const marker of ["pnpm-workspace.yaml", "turbo.json", "nx.json", "lerna.json", "rush.json", "bun.lock", "bun.lockb"]) {
15602
- if (existsSync10(resolve10(root, marker)))
16118
+ if (existsSync11(resolve11(root, marker)))
15603
16119
  markers.push(marker);
15604
16120
  }
15605
16121
  const kind = markers.find((marker) => marker !== "bun.lock" && marker !== "bun.lockb") ?? null;
@@ -21122,9 +21638,9 @@ __export(exports_extract, {
21122
21638
  buildCodebaseIndex: () => buildCodebaseIndex,
21123
21639
  EXTRACT_TAGS: () => EXTRACT_TAGS
21124
21640
  });
21125
- import { existsSync as existsSync11, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
21641
+ import { existsSync as existsSync12, readFileSync as readFileSync7, statSync as statSync4 } from "fs";
21126
21642
  import { createHash as createHash3 } from "crypto";
21127
- import { relative as relative3, resolve as resolve11, join as join10 } from "path";
21643
+ import { relative as relative3, resolve as resolve12, join as join11 } from "path";
21128
21644
  function stableHash(value) {
21129
21645
  return createHash3("sha256").update(value).digest("hex");
21130
21646
  }
@@ -21132,12 +21648,12 @@ function normalizePathForMatch(value) {
21132
21648
  return value.replace(/\\/g, "/").replace(/^\.\//, "");
21133
21649
  }
21134
21650
  function readGitignorePatterns(basePath) {
21135
- const root = statSync4(basePath).isFile() ? resolve11(basePath, "..") : basePath;
21136
- const gitignorePath = join10(root, ".gitignore");
21137
- if (!existsSync11(gitignorePath))
21651
+ const root = statSync4(basePath).isFile() ? resolve12(basePath, "..") : basePath;
21652
+ const gitignorePath = join11(root, ".gitignore");
21653
+ if (!existsSync12(gitignorePath))
21138
21654
  return [];
21139
21655
  try {
21140
- return readFileSync6(gitignorePath, "utf-8").split(`
21656
+ return readFileSync7(gitignorePath, "utf-8").split(`
21141
21657
  `).map((line) => line.trim()).filter((line) => line && !line.startsWith("#") && !line.startsWith("!"));
21142
21658
  } catch {
21143
21659
  return [];
@@ -21268,7 +21784,7 @@ function collectFiles(basePath, extensions, excludes, respectGitignore) {
21268
21784
  return files.sort();
21269
21785
  }
21270
21786
  function buildCodebaseIndex(options) {
21271
- const basePath = resolve11(options.path);
21787
+ const basePath = resolve12(options.path);
21272
21788
  const tags = options.patterns || [...EXTRACT_TAGS];
21273
21789
  const extensions = options.extensions ? new Set(options.extensions.map((e) => e.startsWith(".") ? e : `.${e}`)) : DEFAULT_EXTENSIONS;
21274
21790
  const excludes = options.exclude || [];
@@ -21276,10 +21792,10 @@ function buildCodebaseIndex(options) {
21276
21792
  const files = collectFiles(basePath, extensions, excludes, respectGitignore);
21277
21793
  const indexed = [];
21278
21794
  for (const file of files) {
21279
- const fullPath = statSync4(basePath).isFile() ? basePath : join10(basePath, file);
21795
+ const fullPath = statSync4(basePath).isFile() ? basePath : join11(basePath, file);
21280
21796
  try {
21281
- const source = readFileSync6(fullPath, "utf-8");
21282
- const relPath = statSync4(basePath).isFile() ? relative3(resolve11(basePath, ".."), fullPath) : file;
21797
+ const source = readFileSync7(fullPath, "utf-8");
21798
+ const relPath = statSync4(basePath).isFile() ? relative3(resolve12(basePath, ".."), fullPath) : file;
21283
21799
  indexed.push({
21284
21800
  file: relPath,
21285
21801
  checksum: stableHash(source).slice(0, 24),
@@ -21299,7 +21815,7 @@ function buildCodebaseIndex(options) {
21299
21815
  };
21300
21816
  }
21301
21817
  function extractTodos(options, db) {
21302
- const basePath = resolve11(options.path);
21818
+ const basePath = resolve12(options.path);
21303
21819
  const tags = options.patterns || [...EXTRACT_TAGS];
21304
21820
  const extensions = options.extensions ? new Set(options.extensions.map((e) => e.startsWith(".") ? e : `.${e}`)) : DEFAULT_EXTENSIONS;
21305
21821
  const excludes = options.exclude || [];
@@ -21307,10 +21823,10 @@ function extractTodos(options, db) {
21307
21823
  const files = collectFiles(basePath, extensions, excludes, respectGitignore);
21308
21824
  const allComments = [];
21309
21825
  for (const file of files) {
21310
- const fullPath = statSync4(basePath).isFile() ? basePath : join10(basePath, file);
21826
+ const fullPath = statSync4(basePath).isFile() ? basePath : join11(basePath, file);
21311
21827
  try {
21312
- const source = readFileSync6(fullPath, "utf-8");
21313
- const relPath = statSync4(basePath).isFile() ? relative3(resolve11(basePath, ".."), fullPath) : file;
21828
+ const source = readFileSync7(fullPath, "utf-8");
21829
+ const relPath = statSync4(basePath).isFile() ? relative3(resolve12(basePath, ".."), fullPath) : file;
21314
21830
  const comments = extractFromSource(source, relPath, tags);
21315
21831
  allComments.push(...comments);
21316
21832
  } catch {}
@@ -21404,7 +21920,7 @@ async function watchSourceTodos(options, onRun) {
21404
21920
  const interval = Math.max(100, options.interval_ms || 2000);
21405
21921
  const once = options.once !== false && (!options.max_runs || options.max_runs <= 1);
21406
21922
  const maxRuns = options.max_runs ?? (once ? 1 : Number.POSITIVE_INFINITY);
21407
- const root = resolve11(options.path);
21923
+ const root = resolve12(options.path);
21408
21924
  const runs = [];
21409
21925
  let previous = new Map;
21410
21926
  for (let runNumber = 1;runNumber <= maxRuns; runNumber++) {
@@ -21705,6 +22221,36 @@ function prepareValue(column, value) {
21705
22221
  return JSON.stringify(value ?? (column === "tags" || column === "files_changed" ? [] : {}));
21706
22222
  return value === undefined ? null : value;
21707
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
+ }
21708
22254
  function insertRecord(db, tableKey, row) {
21709
22255
  const table = tableByKey[tableKey];
21710
22256
  const columns = insertColumns[tableKey];
@@ -21841,6 +22387,7 @@ function importLocalBridgeBundle(bundle, options = {}, db) {
21841
22387
  const conflictStrategy = options.conflictStrategy ?? "skip";
21842
22388
  const data = {
21843
22389
  ...bundle.data,
22390
+ plans: normalizeBridgePlanSlugs(bundle.data.plans, d),
21844
22391
  tasks: sortedTasks(bundle.data.tasks),
21845
22392
  saved_views: bundle.data.saved_views ?? [],
21846
22393
  task_boards: bundle.data.task_boards ?? [],
@@ -21925,7 +22472,7 @@ var init_local_bridge = __esm(() => {
21925
22472
  insertColumns = {
21926
22473
  projects: ["id", "name", "path", "description", "task_list_id", "task_prefix", "task_counter", "created_at", "updated_at", "machine_id", "synced_at"],
21927
22474
  task_lists: ["id", "project_id", "slug", "name", "description", "metadata", "created_at", "updated_at", "machine_id", "synced_at"],
21928
- 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"],
21929
22476
  tasks: [
21930
22477
  "id",
21931
22478
  "short_id",
@@ -22575,7 +23122,7 @@ __export(exports_project_commands, {
22575
23122
  registerProjectCommands: () => registerProjectCommands
22576
23123
  });
22577
23124
  import chalk4 from "chalk";
22578
- import { basename as basename5, resolve as resolve12 } from "path";
23125
+ import { basename as basename5, resolve as resolve13 } from "path";
22579
23126
  function collectOption(value, previous = []) {
22580
23127
  return [...previous, value];
22581
23128
  }
@@ -22917,7 +23464,7 @@ function registerProjectCommands(program2) {
22917
23464
  program2.command("projects").description("List and manage projects").option("--add <path>", "Register a project by path").option("--name <name>", "Project name (with --add)").option("--task-list-id <id>", "Custom task list ID (with --add)").action(async (opts) => {
22918
23465
  const globalOpts = program2.opts();
22919
23466
  if (opts.add) {
22920
- const projectPath = resolve12(opts.add);
23467
+ const projectPath = resolve13(opts.add);
22921
23468
  const name = opts.name || basename5(projectPath);
22922
23469
  const existing = getProjectByPath(projectPath);
22923
23470
  let project;
@@ -23025,7 +23572,7 @@ function registerProjectCommands(program2) {
23025
23572
  console.error(chalk4.red(`Project not found: ${projectId}`));
23026
23573
  process.exit(1);
23027
23574
  }
23028
- const entry = setMachineLocalPath2(resolved, resolve12(projectPath));
23575
+ const entry = setMachineLocalPath2(resolved, resolve13(projectPath));
23029
23576
  if (useJson) {
23030
23577
  output(entry, true);
23031
23578
  } else {
@@ -23092,7 +23639,7 @@ function registerProjectCommands(program2) {
23092
23639
  const patterns = opts.pattern ? opts.pattern.split(",").map((t) => t.trim().toUpperCase()) : undefined;
23093
23640
  const taskListId = opts.list ? resolveTaskListId(opts.list) : undefined;
23094
23641
  const result = extractTodos2({
23095
- path: resolve12(scanPath),
23642
+ path: resolve13(scanPath),
23096
23643
  patterns,
23097
23644
  project_id: projectId,
23098
23645
  task_list_id: taskListId,
@@ -23153,7 +23700,7 @@ Indexed ${result.index.files.length} file(s), ${result.index.total_symbols} symb
23153
23700
  const taskListId = opts.list ? resolveTaskListId(opts.list) : undefined;
23154
23701
  const maxRuns = opts.maxRuns ? parseInt(opts.maxRuns, 10) : 1;
23155
23702
  const result = await watchSourceTodos2({
23156
- path: resolve12(scanPath),
23703
+ path: resolve13(scanPath),
23157
23704
  patterns,
23158
23705
  project_id: projectId,
23159
23706
  task_list_id: taskListId,
@@ -23189,8 +23736,8 @@ Indexed ${result.index.files.length} file(s), ${result.index.total_symbols} symb
23189
23736
  const projectId = autoProject(globalOpts);
23190
23737
  const writeOutput = async (content) => {
23191
23738
  if (opts.output) {
23192
- const { writeFileSync: writeFileSync5 } = await import("fs");
23193
- writeFileSync5(resolve12(opts.output), content.endsWith(`
23739
+ const { writeFileSync: writeFileSync6 } = await import("fs");
23740
+ writeFileSync6(resolve13(opts.output), content.endsWith(`
23194
23741
  `) ? content : `${content}
23195
23742
  `);
23196
23743
  } else {
@@ -23205,12 +23752,12 @@ Indexed ${result.index.files.length} file(s), ${result.index.total_symbols} symb
23205
23752
  const exported = opts.encrypt ? createEncryptedBridgeBundle2(bundle, { profile: opts.encryptionProfile }) : bundle;
23206
23753
  const json = JSON.stringify(exported, null, 2);
23207
23754
  await writeOutput(json);
23208
- emitLocalEventHooksQuiet3({ type: "export.finished", payload: { format: "bridge", encrypted: Boolean(opts.encrypt), project_id: projectId, output: opts.output ? resolve12(opts.output) : null, stats: bundle.stats } });
23755
+ emitLocalEventHooksQuiet3({ type: "export.finished", payload: { format: "bridge", encrypted: Boolean(opts.encrypt), project_id: projectId, output: opts.output ? resolve13(opts.output) : null, stats: bundle.stats } });
23209
23756
  if (!opts.encrypt && !opts.allowPlaintextSensitive) {
23210
23757
  console.error(chalk4.yellow("Warning: bridge exports are plaintext JSON. Use --encrypt for sensitive metadata, evidence, and artifact bundles."));
23211
23758
  }
23212
23759
  if (opts.output && !globalOpts.json) {
23213
- console.log(chalk4.green(`${opts.encrypt ? "Encrypted bridge export" : "Bridge export"} written to ${resolve12(opts.output)}`));
23760
+ console.log(chalk4.green(`${opts.encrypt ? "Encrypted bridge export" : "Bridge export"} written to ${resolve13(opts.output)}`));
23214
23761
  }
23215
23762
  return;
23216
23763
  }
@@ -23223,21 +23770,21 @@ Indexed ${result.index.files.length} file(s), ${result.index.total_symbols} symb
23223
23770
  await writeOutput(JSON.stringify(tasks, null, 2));
23224
23771
  }
23225
23772
  const { emitLocalEventHooksQuiet: emitLocalEventHooksQuiet2 } = await Promise.resolve().then(() => (init_event_hooks(), exports_event_hooks));
23226
- emitLocalEventHooksQuiet2({ type: "export.finished", payload: { format: opts.format, project_id: projectId, output: opts.output ? resolve12(opts.output) : null, count: exportedCount } });
23773
+ emitLocalEventHooksQuiet2({ type: "export.finished", payload: { format: opts.format, project_id: projectId, output: opts.output ? resolve13(opts.output) : null, count: exportedCount } });
23227
23774
  });
23228
23775
  program2.command("bridge-import <file>").description("Dry-run or apply a local hasna/todos bridge export bundle").option("--apply", "Apply the import. Defaults to dry-run.").option("--decrypt", "Decrypt an encrypted bridge export before importing").option("--resolve-conflicts", "Safely merge existing local tasks by filling blank fields, unioning tags, and recording unresolved divergences").action(async (file, opts) => {
23229
23776
  const globalOpts = program2.opts();
23230
23777
  try {
23231
- const { readFileSync: readFileSync7 } = await import("fs");
23778
+ const { readFileSync: readFileSync8 } = await import("fs");
23232
23779
  const { importLocalBridgeBundle: importLocalBridgeBundle2 } = await Promise.resolve().then(() => (init_local_bridge(), exports_local_bridge));
23233
23780
  const { decryptBridgeBundle: decryptBridgeBundle2, isEncryptedBridgeBundle: isEncryptedBridgeBundle2 } = await Promise.resolve().then(() => (init_local_encryption(), exports_local_encryption));
23234
- const parsed = JSON.parse(readFileSync7(resolve12(file), "utf-8"));
23781
+ const parsed = JSON.parse(readFileSync8(resolve13(file), "utf-8"));
23235
23782
  const bundle = isEncryptedBridgeBundle2(parsed) ? opts.decrypt ? decryptBridgeBundle2(parsed) : (() => {
23236
23783
  throw new Error("Bridge bundle is encrypted. Re-run with --decrypt and the configured key environment variable set.");
23237
23784
  })() : parsed;
23238
23785
  const result = importLocalBridgeBundle2(bundle, { dryRun: !opts.apply, conflictStrategy: opts.resolveConflicts ? "safe_merge" : "skip" });
23239
23786
  const { emitLocalEventHooksQuiet: emitLocalEventHooksQuiet2 } = await Promise.resolve().then(() => (init_event_hooks(), exports_event_hooks));
23240
- emitLocalEventHooksQuiet2({ type: "import.finished", payload: { file: resolve12(file), dry_run: result.dry_run, ok: result.ok, inserted: result.inserted, skipped: result.skipped, conflicts: result.conflicts.length, issues: result.issues.length } });
23787
+ emitLocalEventHooksQuiet2({ type: "import.finished", payload: { file: resolve13(file), dry_run: result.dry_run, ok: result.ok, inserted: result.inserted, skipped: result.skipped, conflicts: result.conflicts.length, issues: result.issues.length } });
23241
23788
  if (globalOpts.json) {
23242
23789
  output(result, true);
23243
23790
  return;
@@ -23265,11 +23812,11 @@ Indexed ${result.index.files.length} file(s), ${result.index.total_symbols} symb
23265
23812
  program2.command("todos-md-import <file>").alias("markdown-import").alias("import-md").description("Dry-run or apply a local todos.md Markdown import").option("--apply", "Apply the import. Defaults to dry-run.").option("--resolve-conflicts", "Safely merge embedded bridge task conflicts while preserving local divergent fields").action(async (file, opts) => {
23266
23813
  const globalOpts = program2.opts();
23267
23814
  try {
23268
- const { readFileSync: readFileSync7 } = await import("fs");
23815
+ const { readFileSync: readFileSync8 } = await import("fs");
23269
23816
  const { importTodosMarkdown: importTodosMarkdown2 } = await Promise.resolve().then(() => (init_todos_md(), exports_todos_md));
23270
- const result = importTodosMarkdown2(readFileSync7(resolve12(file), "utf-8"), { dryRun: !opts.apply, conflictStrategy: opts.resolveConflicts ? "safe_merge" : "skip" });
23817
+ const result = importTodosMarkdown2(readFileSync8(resolve13(file), "utf-8"), { dryRun: !opts.apply, conflictStrategy: opts.resolveConflicts ? "safe_merge" : "skip" });
23271
23818
  const { emitLocalEventHooksQuiet: emitLocalEventHooksQuiet2 } = await Promise.resolve().then(() => (init_event_hooks(), exports_event_hooks));
23272
- emitLocalEventHooksQuiet2({ type: "import.finished", payload: { file: resolve12(file), format: "todos.md", dry_run: result.dry_run, ok: result.ok, inserted: result.inserted, skipped: result.skipped, issues: result.issues.length } });
23819
+ emitLocalEventHooksQuiet2({ type: "import.finished", payload: { file: resolve13(file), format: "todos.md", dry_run: result.dry_run, ok: result.ok, inserted: result.inserted, skipped: result.skipped, issues: result.issues.length } });
23273
23820
  if (globalOpts.json) {
23274
23821
  output(result, true);
23275
23822
  return;
@@ -24397,7 +24944,7 @@ __export(exports_retention_cleanup, {
24397
24944
  applyRetentionCleanup: () => applyRetentionCleanup,
24398
24945
  RETENTION_CLEANUP_CONFIRMATION: () => RETENTION_CLEANUP_CONFIRMATION
24399
24946
  });
24400
- import { existsSync as existsSync12, unlinkSync } from "fs";
24947
+ import { existsSync as existsSync13, unlinkSync } from "fs";
24401
24948
  function normalizeScopes(scopes) {
24402
24949
  if (!scopes || scopes.length === 0)
24403
24950
  return [...ALL_SCOPES];
@@ -24600,7 +25147,7 @@ function applyRetentionCleanup(input, db) {
24600
25147
  for (const artifact of report.candidates.artifact_files) {
24601
25148
  try {
24602
25149
  const path = artifactStorePath(artifact.relative_path);
24603
- if (!existsSync12(path)) {
25150
+ if (!existsSync13(path)) {
24604
25151
  report.warnings.push(`stored artifact already missing: ${artifact.relative_path}`);
24605
25152
  continue;
24606
25153
  }
@@ -25278,8 +25825,8 @@ __export(exports_local_extensions, {
25278
25825
  discoverLocalExtensions: () => discoverLocalExtensions
25279
25826
  });
25280
25827
  import { createHash as createHash5, createVerify } from "crypto";
25281
- import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync5 } from "fs";
25282
- import { basename as basename6, join as join11, resolve as resolve13 } from "path";
25828
+ import { existsSync as existsSync14, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync5 } from "fs";
25829
+ import { basename as basename6, join as join12, resolve as resolve14 } from "path";
25283
25830
  function isObject(value) {
25284
25831
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
25285
25832
  }
@@ -25360,7 +25907,7 @@ function normalizeManifest(input) {
25360
25907
  };
25361
25908
  }
25362
25909
  function parseJson(path) {
25363
- return JSON.parse(readFileSync7(path, "utf8"));
25910
+ return JSON.parse(readFileSync8(path, "utf8"));
25364
25911
  }
25365
25912
  function sha2563(bytes) {
25366
25913
  return `sha256:${createHash5("sha256").update(bytes).digest("hex")}`;
@@ -25537,14 +26084,14 @@ function verifyExtensionSignature(input) {
25537
26084
  return verifier.verify(input.public_key, decodeSignature(input.signature));
25538
26085
  }
25539
26086
  function inspectExtensionSource(source2) {
25540
- const resolved = resolve13(source2);
25541
- if (!existsSync13(resolved))
26087
+ const resolved = resolve14(source2);
26088
+ if (!existsSync14(resolved))
25542
26089
  throw new Error(`extension source not found: ${source2}`);
25543
26090
  const stat = statSync5(resolved);
25544
- const manifestPath = stat.isDirectory() ? [join11(resolved, "todos.extension.json"), join11(resolved, "extension.json")].find(existsSync13) : resolved;
26091
+ const manifestPath = stat.isDirectory() ? [join12(resolved, "todos.extension.json"), join12(resolved, "extension.json")].find(existsSync14) : resolved;
25545
26092
  if (!manifestPath)
25546
26093
  throw new Error(`extension directory ${source2} is missing todos.extension.json`);
25547
- const raw = readFileSync7(manifestPath);
26094
+ const raw = readFileSync8(manifestPath);
25548
26095
  const parsed = parseJson(manifestPath);
25549
26096
  const bundle = isObject(parsed) && isObject(parsed["manifest"]);
25550
26097
  const manifest = normalizeManifest(bundle ? parsed["manifest"] : parsed);
@@ -25635,26 +26182,26 @@ function testExtensionCompatibility(sourceOrManifest) {
25635
26182
  function projectExtensionSources(projectPath) {
25636
26183
  if (!projectPath)
25637
26184
  return [];
25638
- const root = resolve13(projectPath);
26185
+ const root = resolve14(projectPath);
25639
26186
  const candidates = [
25640
- join11(root, "todos.extension.json"),
25641
- join11(root, ".todos", "todos.extension.json")
26187
+ join12(root, "todos.extension.json"),
26188
+ join12(root, ".todos", "todos.extension.json")
25642
26189
  ];
25643
- const extensionDir = join11(root, ".todos", "extensions");
25644
- if (existsSync13(extensionDir)) {
26190
+ const extensionDir = join12(root, ".todos", "extensions");
26191
+ if (existsSync14(extensionDir)) {
25645
26192
  for (const entry of readdirSync3(extensionDir)) {
25646
26193
  if (entry.startsWith("."))
25647
26194
  continue;
25648
- const full = join11(extensionDir, entry);
26195
+ const full = join12(extensionDir, entry);
25649
26196
  if (statSync5(full).isDirectory() || entry.endsWith(".json"))
25650
26197
  candidates.push(full);
25651
26198
  }
25652
26199
  }
25653
- return candidates.filter(existsSync13);
26200
+ return candidates.filter(existsSync14);
25654
26201
  }
25655
26202
  function discoverLocalExtensions(options = {}) {
25656
26203
  const config = loadConfig();
25657
- const projectPath = options.project_path ? resolve13(options.project_path) : null;
26204
+ const projectPath = options.project_path ? resolve14(options.project_path) : null;
25658
26205
  const configuredSources = [
25659
26206
  ...config.extension_sources || [],
25660
26207
  ...projectPath ? config.project_overrides?.[projectPath]?.extension_sources || [] : []
@@ -25662,7 +26209,7 @@ function discoverLocalExtensions(options = {}) {
25662
26209
  const sources = Array.from(new Set([
25663
26210
  ...configuredSources,
25664
26211
  ...projectExtensionSources(projectPath || undefined)
25665
- ])).map((source2) => projectPath && !source2.startsWith("/") ? resolve13(projectPath, source2) : resolve13(source2));
26212
+ ])).map((source2) => projectPath && !source2.startsWith("/") ? resolve14(projectPath, source2) : resolve14(source2));
25666
26213
  const warnings = [];
25667
26214
  const discovered = [];
25668
26215
  for (const source2 of sources) {
@@ -25922,9 +26469,9 @@ __export(exports_policy_packs, {
25922
26469
  getPolicyPack: () => getPolicyPack,
25923
26470
  explainPolicyPack: () => explainPolicyPack
25924
26471
  });
25925
- import { relative as relative4, resolve as resolve14 } from "path";
26472
+ import { relative as relative4, resolve as resolve15 } from "path";
25926
26473
  function normalizePath3(path) {
25927
- return resolve14(path);
26474
+ return resolve15(path);
25928
26475
  }
25929
26476
  function unique5(values) {
25930
26477
  return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
@@ -25979,7 +26526,7 @@ function commandMatches(commands, pattern) {
25979
26526
  }
25980
26527
  function pathMatches(paths, pattern, root) {
25981
26528
  return paths.filter((path) => {
25982
- const candidate = path.startsWith("/") ? path : resolve14(root, path);
26529
+ const candidate = path.startsWith("/") ? path : resolve15(root, path);
25983
26530
  if (!isPathInside3(root, candidate))
25984
26531
  return matchesPattern3(path, pattern);
25985
26532
  return matchesPattern3(path, pattern) || matchesPattern3(relative4(root, candidate), pattern);
@@ -26945,8 +27492,8 @@ var exports_doctor = {};
26945
27492
  __export(exports_doctor, {
26946
27493
  runTodosDoctor: () => runTodosDoctor
26947
27494
  });
26948
- import { chmodSync, copyFileSync, existsSync as existsSync14, mkdirSync as mkdirSync6, statSync as statSync6 } from "fs";
26949
- import { basename as basename7, dirname as dirname7, join as join12 } from "path";
27495
+ import { chmodSync, copyFileSync, existsSync as existsSync15, mkdirSync as mkdirSync7, statSync as statSync6 } from "fs";
27496
+ import { basename as basename7, dirname as dirname7, join as join13 } from "path";
26950
27497
  function tableExists(db, table) {
26951
27498
  return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
26952
27499
  }
@@ -27040,7 +27587,7 @@ function findMissingProjectRoots(db) {
27040
27587
  continue;
27041
27588
  if (!row.path.startsWith("/"))
27042
27589
  continue;
27043
- if (!existsSync14(row.path))
27590
+ if (!existsSync15(row.path))
27044
27591
  missing++;
27045
27592
  }
27046
27593
  return missing;
@@ -27100,16 +27647,16 @@ function databasePermissionsAreUnsafe(dbPath) {
27100
27647
  function createBackup(dbPath) {
27101
27648
  if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
27102
27649
  return;
27103
- if (!existsSync14(dbPath))
27650
+ if (!existsSync15(dbPath))
27104
27651
  return;
27105
27652
  const stamp = now().replace(/[:.]/g, "-");
27106
- const backupDir = join12(dirname7(dbPath), `${basename7(dbPath)}.backup-${stamp}`);
27653
+ const backupDir = join13(dirname7(dbPath), `${basename7(dbPath)}.backup-${stamp}`);
27107
27654
  const files = [];
27108
- mkdirSync6(backupDir, { recursive: true });
27655
+ mkdirSync7(backupDir, { recursive: true });
27109
27656
  for (const source2 of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
27110
- if (!existsSync14(source2))
27657
+ if (!existsSync15(source2))
27111
27658
  continue;
27112
- const target = join12(backupDir, basename7(source2));
27659
+ const target = join13(backupDir, basename7(source2));
27113
27660
  copyFileSync(source2, target);
27114
27661
  files.push(target);
27115
27662
  }
@@ -27335,7 +27882,7 @@ var init_doctor = __esm(() => {
27335
27882
  });
27336
27883
 
27337
27884
  // src/server/routes.ts
27338
- import { join as join13, resolve as resolve15, sep as sep2 } from "path";
27885
+ import { join as join14, resolve as resolve16, sep as sep2 } from "path";
27339
27886
  function parseFieldsParam(url) {
27340
27887
  const fieldsParam = url.searchParams.get("fields");
27341
27888
  return fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
@@ -28020,6 +28567,7 @@ async function handleCreatePlan(req, _ctx, json2) {
28020
28567
  return json2({ error: "Missing 'name'" }, 400);
28021
28568
  const plan = createPlan({
28022
28569
  name: body.name,
28570
+ slug: body.slug,
28023
28571
  description: body.description,
28024
28572
  project_id: body.project_id,
28025
28573
  task_list_id: body.task_list_id,
@@ -28072,9 +28620,9 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
28072
28620
  if (!ctx.dashboardExists || method !== "GET" && method !== "HEAD")
28073
28621
  return null;
28074
28622
  if (path !== "/") {
28075
- const filePath = join13(ctx.dashboardDir, path);
28076
- const resolvedFile = resolve15(filePath);
28077
- const resolvedBase = resolve15(ctx.dashboardDir);
28623
+ const filePath = join14(ctx.dashboardDir, path);
28624
+ const resolvedFile = resolve16(filePath);
28625
+ const resolvedBase = resolve16(ctx.dashboardDir);
28078
28626
  if (!resolvedFile.startsWith(resolvedBase + sep2) && resolvedFile !== resolvedBase) {
28079
28627
  return json2({ error: "Forbidden" }, 403);
28080
28628
  }
@@ -28082,7 +28630,7 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
28082
28630
  if (res2)
28083
28631
  return res2;
28084
28632
  }
28085
- const indexPath = join13(ctx.dashboardDir, "index.html");
28633
+ const indexPath = join14(ctx.dashboardDir, "index.html");
28086
28634
  const res = serveStaticFile2(indexPath);
28087
28635
  if (res)
28088
28636
  return res;
@@ -33077,8 +33625,8 @@ var exports_mention_resolver = {};
33077
33625
  __export(exports_mention_resolver, {
33078
33626
  resolveMentions: () => resolveMentions
33079
33627
  });
33080
- import { existsSync as existsSync15, readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as statSync7 } from "fs";
33081
- import { basename as basename8, isAbsolute, join as join14, relative as relative5, resolve as resolve16, sep as sep3 } from "path";
33628
+ import { existsSync as existsSync16, readdirSync as readdirSync4, readFileSync as readFileSync9, statSync as statSync7 } from "fs";
33629
+ import { basename as basename8, isAbsolute, join as join15, relative as relative5, resolve as resolve17, sep as sep3 } from "path";
33082
33630
  function blankResolution(parsed) {
33083
33631
  return {
33084
33632
  input: parsed.input,
@@ -33101,7 +33649,7 @@ function backlink(kind, key, label, target = key) {
33101
33649
  return { kind, key, label, target };
33102
33650
  }
33103
33651
  function normalizeWorkspace(workspace) {
33104
- return resolve16(workspace || process.cwd());
33652
+ return resolve17(workspace || process.cwd());
33105
33653
  }
33106
33654
  function isInside(root, absolutePath) {
33107
33655
  const rel = relative5(root, absolutePath);
@@ -33169,14 +33717,14 @@ function resolveFile(parsed, workspace) {
33169
33717
  resolution.warnings.push("path is empty or escapes the workspace");
33170
33718
  return resolution;
33171
33719
  }
33172
- const absolutePath = resolve16(workspace, relPath);
33720
+ const absolutePath = resolve17(workspace, relPath);
33173
33721
  if (!isInside(workspace, absolutePath)) {
33174
33722
  resolution.path = relPath;
33175
33723
  resolution.warnings.push("path escapes the workspace");
33176
33724
  return resolution;
33177
33725
  }
33178
33726
  resolution.path = relPath;
33179
- if (!existsSync15(absolutePath)) {
33727
+ if (!existsSync16(absolutePath)) {
33180
33728
  resolution.warnings.push("file does not exist in the local workspace");
33181
33729
  return resolution;
33182
33730
  }
@@ -33186,7 +33734,7 @@ function resolveFile(parsed, workspace) {
33186
33734
  return resolution;
33187
33735
  }
33188
33736
  if (parsed.line !== undefined) {
33189
- const lineCount = readFileSync8(absolutePath, "utf-8").split(/\r?\n/).length;
33737
+ const lineCount = readFileSync9(absolutePath, "utf-8").split(/\r?\n/).length;
33190
33738
  if (parsed.line < 1 || parsed.line > lineCount) {
33191
33739
  resolution.warnings.push(`line ${parsed.line} is outside the file range 1-${lineCount}`);
33192
33740
  return resolution;
@@ -33209,7 +33757,7 @@ function walkSourceFiles(root, current = root, files = []) {
33209
33757
  if (SKIP_DIRS3.has(entry.name))
33210
33758
  continue;
33211
33759
  }
33212
- const absolutePath = join14(current, entry.name);
33760
+ const absolutePath = join15(current, entry.name);
33213
33761
  if (entry.isDirectory()) {
33214
33762
  if (!SKIP_DIRS3.has(entry.name))
33215
33763
  walkSourceFiles(root, absolutePath, files);
@@ -33239,7 +33787,7 @@ function resolveSymbol(parsed, workspace, maxMatches) {
33239
33787
  const pattern = symbolPattern(name);
33240
33788
  const matches = [];
33241
33789
  for (const file of walkSourceFiles(workspace)) {
33242
- const lines = readFileSync8(file, "utf-8").split(/\r?\n/);
33790
+ const lines = readFileSync9(file, "utf-8").split(/\r?\n/);
33243
33791
  for (let index = 0;index < lines.length; index += 1) {
33244
33792
  const line = lines[index];
33245
33793
  const found = pattern.exec(line);
@@ -35973,8 +36521,8 @@ __export(exports_release_compatibility, {
35973
36521
  createReleaseCompatibilityReport: () => createReleaseCompatibilityReport,
35974
36522
  LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION: () => LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION
35975
36523
  });
35976
- import { readFileSync as readFileSync9 } from "fs";
35977
- import { join as join15, resolve as resolve17 } from "path";
36524
+ import { readFileSync as readFileSync10 } from "fs";
36525
+ import { join as join16, resolve as resolve18 } from "path";
35978
36526
  import { Database as Database2 } from "bun:sqlite";
35979
36527
  function pass(id, message, details) {
35980
36528
  return { id, status: "passed", message, details };
@@ -35986,7 +36534,7 @@ function warn(id, message, details) {
35986
36534
  return { id, status: "warning", message, details };
35987
36535
  }
35988
36536
  function readPackageJson2(root) {
35989
- return JSON.parse(readFileSync9(join15(root, "package.json"), "utf8"));
36537
+ return JSON.parse(readFileSync10(join16(root, "package.json"), "utf8"));
35990
36538
  }
35991
36539
  function sortedKeys(value) {
35992
36540
  return Object.keys(value ?? {}).sort((left, right) => left.localeCompare(right));
@@ -36082,7 +36630,7 @@ function checkChangelog() {
36082
36630
  ];
36083
36631
  }
36084
36632
  function createReleaseCompatibilityReport(options = {}) {
36085
- const root = resolve17(options.root ?? process.cwd());
36633
+ const root = resolve18(options.root ?? process.cwd());
36086
36634
  const packageJson = readPackageJson2(root);
36087
36635
  const simulatedLevels = options.simulated_levels ?? defaultSimulationLevels();
36088
36636
  const checks = [
@@ -37369,6 +37917,7 @@ Tasks:` : null,
37369
37917
  if (shouldRegisterTool("create_plan")) {
37370
37918
  server.tool("create_plan", "Create a new plan (sprint/milestone).", {
37371
37919
  name: exports_external2.string().describe("Plan name"),
37920
+ slug: exports_external2.string().optional().describe("Readable plan slug"),
37372
37921
  project_id: exports_external2.string().optional().describe("Project ID"),
37373
37922
  description: exports_external2.string().optional(),
37374
37923
  start_date: exports_external2.string().optional().describe("ISO date"),
@@ -41665,7 +42214,7 @@ __export(exports_verification_providers, {
41665
42214
  getVerificationRecord: () => getVerificationRecord,
41666
42215
  discoverVerificationProviderCapabilities: () => discoverVerificationProviderCapabilities
41667
42216
  });
41668
- import { existsSync as existsSync16, readFileSync as readFileSync10 } from "fs";
42217
+ import { existsSync as existsSync17, readFileSync as readFileSync11 } from "fs";
41669
42218
  function normalizeName6(name) {
41670
42219
  const normalized = name.trim().toLowerCase();
41671
42220
  if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(normalized)) {
@@ -41762,7 +42311,7 @@ function classifyLog(text) {
41762
42311
  async function sleep3(ms) {
41763
42312
  if (ms <= 0)
41764
42313
  return;
41765
- await new Promise((resolve18) => setTimeout(resolve18, ms));
42314
+ await new Promise((resolve19) => setTimeout(resolve19, ms));
41766
42315
  }
41767
42316
  async function runCommandProvider(provider, input) {
41768
42317
  const commandTemplate = input.command || provider.command;
@@ -41817,7 +42366,7 @@ Timed out after ${provider.timeout_ms}ms`);
41817
42366
  };
41818
42367
  }
41819
42368
  function runCiLogProvider(input) {
41820
- const text = input.log_text ?? (input.log_path && existsSync16(input.log_path) ? readFileSync10(input.log_path, "utf-8") : "");
42369
+ const text = input.log_text ?? (input.log_path && existsSync17(input.log_path) ? readFileSync11(input.log_path, "utf-8") : "");
41821
42370
  return {
41822
42371
  status: classifyLog(text),
41823
42372
  attempts: 1,
@@ -41829,7 +42378,7 @@ function runBrowserProvider(input) {
41829
42378
  if (!input.artifact_path) {
41830
42379
  return { status: "unknown", attempts: 1, exit_code: null, output_summary: "browser provider needs a screenshot or artifact path" };
41831
42380
  }
41832
- if (!existsSync16(input.artifact_path)) {
42381
+ if (!existsSync17(input.artifact_path)) {
41833
42382
  return { status: "failed", attempts: 1, exit_code: null, output_summary: `artifact not found: ${input.artifact_path}` };
41834
42383
  }
41835
42384
  return {
@@ -44111,9 +44660,9 @@ __export(exports_local_backups, {
44111
44660
  LOCAL_BACKUP_CHECKSUM_ALGORITHM: () => LOCAL_BACKUP_CHECKSUM_ALGORITHM
44112
44661
  });
44113
44662
  import { createHash as createHash8 } from "crypto";
44114
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "fs";
44115
- import { dirname as dirname8, resolve as resolve18 } from "path";
44116
- import { mkdirSync as mkdirSync7 } from "fs";
44663
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
44664
+ import { dirname as dirname8, resolve as resolve19 } from "path";
44665
+ import { mkdirSync as mkdirSync8 } from "fs";
44117
44666
  function stableJson(value) {
44118
44667
  if (value === null || typeof value !== "object")
44119
44668
  return JSON.stringify(value);
@@ -44214,14 +44763,14 @@ function createLocalBackup(options = {}, db) {
44214
44763
  return backup;
44215
44764
  }
44216
44765
  function writeLocalBackupFile(backup, outputPath) {
44217
- const path = resolve18(outputPath);
44218
- mkdirSync7(dirname8(path), { recursive: true });
44219
- writeFileSync5(path, `${JSON.stringify(backup, null, 2)}
44766
+ const path = resolve19(outputPath);
44767
+ mkdirSync8(dirname8(path), { recursive: true });
44768
+ writeFileSync6(path, `${JSON.stringify(backup, null, 2)}
44220
44769
  `);
44221
44770
  return path;
44222
44771
  }
44223
44772
  function readLocalBackupFile(path) {
44224
- return JSON.parse(readFileSync11(resolve18(path), "utf-8"));
44773
+ return JSON.parse(readFileSync12(resolve19(path), "utf-8"));
44225
44774
  }
44226
44775
  function verifyLocalBackup(value, options = {}, db) {
44227
44776
  const verifiedAt = options.verified_at ?? now();
@@ -44417,8 +44966,8 @@ __export(exports_onboarding_fixtures, {
44417
44966
  TODOS_ONBOARDING_FIXTURE_SOURCE: () => TODOS_ONBOARDING_FIXTURE_SOURCE,
44418
44967
  TODOS_ONBOARDING_FIXTURE_LIBRARY_VERSION: () => TODOS_ONBOARDING_FIXTURE_LIBRARY_VERSION
44419
44968
  });
44420
- import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync6 } from "fs";
44421
- import { join as join16 } from "path";
44969
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync7 } from "fs";
44970
+ import { join as join17 } from "path";
44422
44971
  function emptyData() {
44423
44972
  return {
44424
44973
  projects: [],
@@ -44529,6 +45078,7 @@ function createAgentProjectDemoBundle() {
44529
45078
  });
44530
45079
  data.plans.push({
44531
45080
  id: ids.plan,
45081
+ slug: "ship-local-demo-workflow",
44532
45082
  project_id: ids.project,
44533
45083
  task_list_id: ids.list,
44534
45084
  agent_id: "demo-agent",
@@ -44748,11 +45298,11 @@ function getOnboardingFixtureBundle(name = "agent-project-demo") {
44748
45298
  return getOnboardingFixture(name).bundle;
44749
45299
  }
44750
45300
  function writeOnboardingFixtureFiles(directory) {
44751
- mkdirSync8(directory, { recursive: true });
45301
+ mkdirSync9(directory, { recursive: true });
44752
45302
  const files = [];
44753
45303
  for (const fixture of allFixtures()) {
44754
- const path = join16(directory, `${fixture.summary.name}.bridge.json`);
44755
- writeFileSync6(path, `${JSON.stringify(fixture.bundle, null, 2)}
45304
+ const path = join17(directory, `${fixture.summary.name}.bridge.json`);
45305
+ writeFileSync7(path, `${JSON.stringify(fixture.bundle, null, 2)}
44756
45306
  `, "utf-8");
44757
45307
  files.push(path);
44758
45308
  }
@@ -45399,7 +45949,7 @@ __export(exports_agent_replay_simulator, {
45399
45949
  renderAgentReplaySimulationMarkdown: () => renderAgentReplaySimulationMarkdown
45400
45950
  });
45401
45951
  import { createHash as createHash10 } from "crypto";
45402
- import { readFileSync as readFileSync12 } from "fs";
45952
+ import { readFileSync as readFileSync13 } from "fs";
45403
45953
  function isObject2(value) {
45404
45954
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
45405
45955
  }
@@ -45632,7 +46182,7 @@ function simulateAgentReplay(input, options = {}) {
45632
46182
  };
45633
46183
  }
45634
46184
  function simulateAgentReplayFile(path, options = {}) {
45635
- const parsed = JSON.parse(readFileSync12(path, "utf8"));
46185
+ const parsed = JSON.parse(readFileSync13(path, "utf8"));
45636
46186
  return simulateAgentReplay(parsed, options);
45637
46187
  }
45638
46188
  function renderAgentReplaySimulationMarkdown(simulation) {
@@ -50919,28 +51469,28 @@ __export(exports_environment_snapshots, {
50919
51469
  captureEnvironmentSnapshot: () => captureEnvironmentSnapshot
50920
51470
  });
50921
51471
  import { createHash as createHash12 } from "crypto";
50922
- import { existsSync as existsSync17, readFileSync as readFileSync13, statSync as statSync8 } from "fs";
51472
+ import { existsSync as existsSync18, readFileSync as readFileSync14, statSync as statSync8 } from "fs";
50923
51473
  import { hostname as hostname2, platform, arch } from "os";
50924
- import { dirname as dirname9, join as join17, resolve as resolve19 } from "path";
51474
+ import { dirname as dirname9, join as join18, resolve as resolve20 } from "path";
50925
51475
  import { tmpdir as tmpdir3 } from "os";
50926
51476
  function sha2566(value) {
50927
51477
  return createHash12("sha256").update(value).digest("hex");
50928
51478
  }
50929
51479
  function fileRecord(root, relativePath) {
50930
- const path = join17(root, relativePath);
50931
- if (!existsSync17(path))
51480
+ const path = join18(root, relativePath);
51481
+ if (!existsSync18(path))
50932
51482
  return null;
50933
51483
  const stat = statSync8(path);
50934
51484
  if (!stat.isFile())
50935
51485
  return null;
50936
- const content = readFileSync13(path);
51486
+ const content = readFileSync14(path);
50937
51487
  return { path: relativePath, sha256: sha2566(content), size_bytes: content.length };
50938
51488
  }
50939
51489
  function manifestRecord(root, relativePath) {
50940
51490
  const base = fileRecord(root, relativePath);
50941
51491
  if (!base)
50942
51492
  return null;
50943
- const parsed = readJsonFile(join17(root, relativePath));
51493
+ const parsed = readJsonFile(join18(root, relativePath));
50944
51494
  if (!parsed)
50945
51495
  return { ...base, redacted: {} };
50946
51496
  const redacted = redactValue({
@@ -51035,15 +51585,15 @@ function commandEnv(env, includeValues) {
51035
51585
  function defaultSnapshotDir() {
51036
51586
  const dbPath = getDatabasePath();
51037
51587
  if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
51038
- return join17(tmpdir3(), "hasna-todos", "environment-snapshots");
51039
- return join17(dirname9(resolve19(dbPath)), "environment-snapshots");
51588
+ return join18(tmpdir3(), "hasna-todos", "environment-snapshots");
51589
+ return join18(dirname9(resolve20(dbPath)), "environment-snapshots");
51040
51590
  }
51041
51591
  function snapshotWithId(snapshot) {
51042
51592
  const digest = sha2566(JSON.stringify(snapshot)).slice(0, 24);
51043
51593
  return { id: `env_${digest}`, ...snapshot };
51044
51594
  }
51045
51595
  function captureEnvironmentSnapshot(input = {}) {
51046
- const root = resolve19(input.root || process.cwd());
51596
+ const root = resolve20(input.root || process.cwd());
51047
51597
  const env = input.env || process.env;
51048
51598
  const warnings = [];
51049
51599
  const manifests = MANIFEST_FILES.map((file) => manifestRecord(root, file)).filter((file) => Boolean(file));
@@ -51083,13 +51633,13 @@ function captureEnvironmentSnapshot(input = {}) {
51083
51633
  });
51084
51634
  }
51085
51635
  function writeEnvironmentSnapshot(snapshot, outputPath) {
51086
- const path = outputPath ? resolve19(outputPath) : join17(defaultSnapshotDir(), `${snapshot.id}.json`);
51636
+ const path = outputPath ? resolve20(outputPath) : join18(defaultSnapshotDir(), `${snapshot.id}.json`);
51087
51637
  ensureDir2(dirname9(path));
51088
51638
  writeJsonFile(path, snapshot);
51089
51639
  return path;
51090
51640
  }
51091
51641
  function readEnvironmentSnapshot(path) {
51092
- const snapshot = readJsonFile(resolve19(path));
51642
+ const snapshot = readJsonFile(resolve20(path));
51093
51643
  if (!snapshot || snapshot.schema_version !== 1 || typeof snapshot.id !== "string") {
51094
51644
  throw new Error(`Invalid environment snapshot: ${path}`);
51095
51645
  }
@@ -51614,27 +52164,27 @@ __export(exports_serve, {
51614
52164
  SECURITY_HEADERS: () => SECURITY_HEADERS,
51615
52165
  MIME_TYPES: () => MIME_TYPES
51616
52166
  });
51617
- import { existsSync as existsSync18 } from "fs";
51618
- import { join as join18, dirname as dirname10, extname } from "path";
52167
+ import { existsSync as existsSync19 } from "fs";
52168
+ import { join as join19, dirname as dirname10, extname } from "path";
51619
52169
  import { fileURLToPath as fileURLToPath2 } from "url";
51620
52170
  function resolveDashboardDir() {
51621
52171
  const candidates = [];
51622
52172
  try {
51623
52173
  const scriptDir = dirname10(fileURLToPath2(import.meta.url));
51624
- candidates.push(join18(scriptDir, "..", "dashboard", "dist"));
51625
- candidates.push(join18(scriptDir, "..", "..", "dashboard", "dist"));
52174
+ candidates.push(join19(scriptDir, "..", "dashboard", "dist"));
52175
+ candidates.push(join19(scriptDir, "..", "..", "dashboard", "dist"));
51626
52176
  } catch {}
51627
52177
  if (process.argv[1]) {
51628
52178
  const mainDir = dirname10(process.argv[1]);
51629
- candidates.push(join18(mainDir, "..", "dashboard", "dist"));
51630
- candidates.push(join18(mainDir, "..", "..", "dashboard", "dist"));
52179
+ candidates.push(join19(mainDir, "..", "dashboard", "dist"));
52180
+ candidates.push(join19(mainDir, "..", "..", "dashboard", "dist"));
51631
52181
  }
51632
- candidates.push(join18(process.cwd(), "dashboard", "dist"));
52182
+ candidates.push(join19(process.cwd(), "dashboard", "dist"));
51633
52183
  for (const candidate of candidates) {
51634
- if (existsSync18(candidate))
52184
+ if (existsSync19(candidate))
51635
52185
  return candidate;
51636
52186
  }
51637
- return join18(process.cwd(), "dashboard", "dist");
52187
+ return join19(process.cwd(), "dashboard", "dist");
51638
52188
  }
51639
52189
  function getProvidedApiKey(req) {
51640
52190
  const headerKey = req.headers.get("x-api-key");
@@ -51684,7 +52234,7 @@ function json(data, status = 200, headers) {
51684
52234
  });
51685
52235
  }
51686
52236
  function serveStaticFile(filePath) {
51687
- if (!existsSync18(filePath))
52237
+ if (!existsSync19(filePath))
51688
52238
  return null;
51689
52239
  const ext = extname(filePath);
51690
52240
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
@@ -51765,7 +52315,7 @@ data: ${data}
51765
52315
  filteredSseClients.delete(client);
51766
52316
  }
51767
52317
  const dashboardDir = resolveDashboardDir();
51768
- const dashboardExists = existsSync18(dashboardDir);
52318
+ const dashboardExists = existsSync19(dashboardDir);
51769
52319
  if (!dashboardExists) {
51770
52320
  console.error(`
51771
52321
  Dashboard not found at: ${dashboardDir}`);
@@ -53595,12 +54145,12 @@ __export(exports_config_serve_commands, {
53595
54145
  registerConfigServeCommands: () => registerConfigServeCommands
53596
54146
  });
53597
54147
  import chalk6 from "chalk";
53598
- import { existsSync as existsSync19, mkdirSync as mkdirSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
53599
- import { dirname as dirname11, join as join19 } from "path";
54148
+ import { existsSync as existsSync20, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "fs";
54149
+ import { dirname as dirname11, join as join20 } from "path";
53600
54150
  function registerConfigServeCommands(program2) {
53601
54151
  program2.command("config").description("View or update configuration").option("--get <key>", "Get a config value").option("--set <key=value>", "Set a config value (e.g. completion_guard.enabled=true)").action((opts) => {
53602
54152
  const globalOpts = program2.opts();
53603
- const configPath = join19(getTodosGlobalDir(), "config.json");
54153
+ const configPath = join20(getTodosGlobalDir(), "config.json");
53604
54154
  if (opts.get) {
53605
54155
  const config2 = loadConfig();
53606
54156
  const keys = opts.get.split(".");
@@ -53626,7 +54176,7 @@ function registerConfigServeCommands(program2) {
53626
54176
  }
53627
54177
  let config2 = {};
53628
54178
  try {
53629
- config2 = JSON.parse(readFileSync14(configPath, "utf-8"));
54179
+ config2 = JSON.parse(readFileSync15(configPath, "utf-8"));
53630
54180
  } catch {}
53631
54181
  const keys = key.split(".");
53632
54182
  let obj = config2;
@@ -53637,9 +54187,9 @@ function registerConfigServeCommands(program2) {
53637
54187
  }
53638
54188
  obj[keys[keys.length - 1]] = parsedValue;
53639
54189
  const dir = dirname11(configPath);
53640
- if (!existsSync19(dir))
53641
- mkdirSync9(dir, { recursive: true });
53642
- writeFileSync7(configPath, JSON.stringify(config2, null, 2));
54190
+ if (!existsSync20(dir))
54191
+ mkdirSync10(dir, { recursive: true });
54192
+ writeFileSync8(configPath, JSON.stringify(config2, null, 2));
53643
54193
  if (globalOpts.json) {
53644
54194
  output({ key, value: parsedValue }, true);
53645
54195
  } else {
@@ -53766,7 +54316,7 @@ function registerConfigServeCommands(program2) {
53766
54316
  redaction.command("scan [text]").description("Scan text or a file for secret-like values without printing values").option("--file <path>", "File to scan").action(async (text2, opts) => {
53767
54317
  const globalOpts = program2.opts();
53768
54318
  const { listSecretFindings: listSecretFindings2 } = await Promise.resolve().then(() => (init_redaction(), exports_redaction));
53769
- const value = opts.file ? readFileSync14(opts.file, "utf-8") : text2 || "";
54319
+ const value = opts.file ? readFileSync15(opts.file, "utf-8") : text2 || "";
53770
54320
  const findings = listSecretFindings2(value);
53771
54321
  if (globalOpts.json) {
53772
54322
  output({ ok: findings.length === 0, findings }, true);
@@ -55152,7 +55702,7 @@ __export(exports_query_commands, {
55152
55702
  registerQueryCommands: () => registerQueryCommands
55153
55703
  });
55154
55704
  import chalk7 from "chalk";
55155
- import { readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "fs";
55705
+ import { readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "fs";
55156
55706
  function parseJsonObjectOption2(value, label) {
55157
55707
  if (!value)
55158
55708
  return;
@@ -55811,9 +56361,9 @@ Repairs`));
55811
56361
  const db = getDatabase();
55812
56362
  const row = db.query("SELECT COUNT(*) as count FROM tasks").get();
55813
56363
  const { statSync: statSync9 } = await import("fs");
55814
- const { join: join20 } = await import("path");
56364
+ const { join: join21 } = await import("path");
55815
56365
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
55816
- const dbPath = process.env["HASNA_TODOS_DB_PATH"] || process.env["TODOS_DB_PATH"] || join20(home, ".hasna", "todos", "todos.db");
56366
+ const dbPath = process.env["HASNA_TODOS_DB_PATH"] || process.env["TODOS_DB_PATH"] || join21(home, ".hasna", "todos", "todos.db");
55817
56367
  let size = "unknown";
55818
56368
  try {
55819
56369
  size = `${(statSync9(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
@@ -56430,7 +56980,7 @@ Repairs`));
56430
56980
  const sessionId = opts.session || globalOpts.session || undefined;
56431
56981
  try {
56432
56982
  if (opts.import) {
56433
- const bundle = JSON.parse(readFileSync15(opts.import, "utf-8"));
56983
+ const bundle = JSON.parse(readFileSync16(opts.import, "utf-8"));
56434
56984
  const result = importHandoffBundle(bundle, { apply: opts.apply }, db);
56435
56985
  if (opts.json || globalOpts.json) {
56436
56986
  console.log(JSON.stringify(result));
@@ -56446,7 +56996,7 @@ Repairs`));
56446
56996
  const bundle = exportHandoffBundle(opts.export, db);
56447
56997
  const json2 = JSON.stringify(bundle, null, 2);
56448
56998
  if (opts.output) {
56449
- writeFileSync8(opts.output, `${json2}
56999
+ writeFileSync9(opts.output, `${json2}
56450
57000
  `);
56451
57001
  if (opts.json || globalOpts.json) {
56452
57002
  console.log(JSON.stringify({ path: opts.output, handoff_id: bundle.handoff.id }));
@@ -56677,7 +57227,7 @@ Repairs`));
56677
57227
  });
56678
57228
  const content = format === "json" ? JSON.stringify(document, null, 2) : renderReleaseNotesMarkdown2(document);
56679
57229
  if (opts.out) {
56680
- writeFileSync8(opts.out, content);
57230
+ writeFileSync9(opts.out, content);
56681
57231
  if (format !== "json")
56682
57232
  console.log(chalk7.green(`Wrote release notes to ${opts.out}`));
56683
57233
  return;
@@ -56792,7 +57342,7 @@ Repairs`));
56792
57342
  redact: Boolean(opts.redact)
56793
57343
  });
56794
57344
  if (opts.out) {
56795
- writeFileSync8(opts.out, exported.content);
57345
+ writeFileSync9(opts.out, exported.content);
56796
57346
  if (!(opts.json || globalOpts.json))
56797
57347
  console.log(chalk7.green(`Wrote ${exported.events.length} events to ${opts.out}`));
56798
57348
  }
@@ -56808,7 +57358,7 @@ Repairs`));
56808
57358
  });
56809
57359
  calendar.command("import <path>").description("Import VEVENT entries from an ICS file as local imported calendar items").option("-j, --json", "Output JSON").action((path, opts) => {
56810
57360
  try {
56811
- const result = importCalendarIcs(readFileSync15(path, "utf-8"));
57361
+ const result = importCalendarIcs(readFileSync16(path, "utf-8"));
56812
57362
  if (opts.json || program2.opts().json) {
56813
57363
  output(result, true);
56814
57364
  return;
@@ -56947,7 +57497,7 @@ Repairs`));
56947
57497
  const bundle = exportTaskBoardBundle(boardId);
56948
57498
  const json2 = JSON.stringify(bundle, null, 2);
56949
57499
  if (opts.out) {
56950
- writeFileSync8(opts.out, json2);
57500
+ writeFileSync9(opts.out, json2);
56951
57501
  if (!(opts.json || program2.opts().json))
56952
57502
  console.log(chalk7.green(`Wrote ${bundle.boards.length} board(s) to ${opts.out}`));
56953
57503
  }
@@ -56959,7 +57509,7 @@ Repairs`));
56959
57509
  });
56960
57510
  board.command("import <path>").description("Import local board definitions from a JSON bundle").option("-j, --json", "Output JSON").action((path, opts) => {
56961
57511
  try {
56962
- const bundle = JSON.parse(readFileSync15(path, "utf-8"));
57512
+ const bundle = JSON.parse(readFileSync16(path, "utf-8"));
56963
57513
  const result = importTaskBoardBundle(bundle);
56964
57514
  if (opts.json || program2.opts().json) {
56965
57515
  output(result, true);
@@ -57335,7 +57885,7 @@ Repairs`));
57335
57885
  const { importExternalIssues: importExternalIssues2 } = await Promise.resolve().then(() => (init_external_issue_importers(), exports_external_issue_importers));
57336
57886
  let body = text2 || "";
57337
57887
  if (opts.file)
57338
- body = readFileSync15(opts.file, "utf-8");
57888
+ body = readFileSync16(opts.file, "utf-8");
57339
57889
  if (!body && !opts.url && !process.stdin.isTTY)
57340
57890
  body = await Bun.stdin.text();
57341
57891
  if (!body.trim() && !opts.url) {
@@ -57393,7 +57943,7 @@ Repairs`));
57393
57943
  } = await Promise.resolve().then(() => (init_tester_issue_reports(), exports_tester_issue_reports));
57394
57944
  let body = jsonText || "";
57395
57945
  if (opts.file)
57396
- body = readFileSync15(opts.file, "utf-8");
57946
+ body = readFileSync16(opts.file, "utf-8");
57397
57947
  if (!body && !process.stdin.isTTY)
57398
57948
  body = await Bun.stdin.text();
57399
57949
  if (!body.trim()) {
@@ -57436,11 +57986,11 @@ Repairs`));
57436
57986
  const inbox = program2.command("inbox").description("Capture local inbox items from pasted errors, CI logs, git context, files, or GitHub issue URLs");
57437
57987
  inbox.command("add [text]").description("Create a local inbox item and linked task from text, stdin, or a file").option("--file <path>", "Read captured context from a file").option("--source-type <type>", "pasted_error, ci_log, git_context, github_issue, file, or other").option("--source-name <name>", "Human-readable source name").option("--source-url <url>", "Source URL, including GitHub issue URLs").option("--title <title>", "Task/inbox title").option("--priority <priority>", "Task priority").option("--tags <tags>", "Comma-separated extra tags").option("--metadata <json>", "Additional JSON metadata").option("--no-task", "Only store inbox item; do not create a linked task").option("-j, --json", "Output as JSON").action(async (text2, opts) => {
57438
57988
  const globalOpts = program2.opts();
57439
- const { readFileSync: readFileSync16 } = await import("fs");
57989
+ const { readFileSync: readFileSync17 } = await import("fs");
57440
57990
  const { createInboxItem: createInboxItem2 } = await Promise.resolve().then(() => (init_inbox(), exports_inbox));
57441
57991
  let body = text2 || "";
57442
57992
  if (opts.file)
57443
- body = readFileSync16(opts.file, "utf-8");
57993
+ body = readFileSync17(opts.file, "utf-8");
57444
57994
  if (!body && !process.stdin.isTTY)
57445
57995
  body = await Bun.stdin.text();
57446
57996
  if (!body.trim()) {
@@ -57501,11 +58051,11 @@ ${diff}` : null].filter(Boolean).join(`
57501
58051
  });
57502
58052
  inbox.command("parse [text]").description("Preview or apply deterministic local natural-language task intake").option("--file <path>", "Read natural-language input from a file").option("--priority <priority>", "Default priority for parsed tasks", "medium").option("--project <id>", "Project ID for applied tasks").option("--list <id>", "Task list ID for applied tasks").option("--reference-date <iso>", "Reference date for due today/tomorrow/next week").option("--apply", "Create parsed tasks; default is dry-run preview").option("-j, --json", "Output as JSON").action(async (text2, opts) => {
57503
58053
  const globalOpts = program2.opts();
57504
- const { readFileSync: readFileSync16 } = await import("fs");
58054
+ const { readFileSync: readFileSync17 } = await import("fs");
57505
58055
  const { previewNaturalLanguageIntake: previewNaturalLanguageIntake2 } = await Promise.resolve().then(() => (init_natural_language_intake(), exports_natural_language_intake));
57506
58056
  let body = text2 || "";
57507
58057
  if (opts.file)
57508
- body = readFileSync16(opts.file, "utf-8");
58058
+ body = readFileSync17(opts.file, "utf-8");
57509
58059
  if (!body && !process.stdin.isTTY)
57510
58060
  body = await Bun.stdin.text();
57511
58061
  if (!body.trim()) {
@@ -57629,45 +58179,45 @@ __export(exports_mcp_hooks_commands, {
57629
58179
  });
57630
58180
  import chalk8 from "chalk";
57631
58181
  import { execSync as execSync3 } from "child_process";
57632
- import { existsSync as existsSync20, readFileSync as readFileSync16, writeFileSync as writeFileSync9, mkdirSync as mkdirSync10, chmodSync as chmodSync2 } from "fs";
57633
- import { dirname as dirname12, join as join20 } from "path";
58182
+ import { existsSync as existsSync21, readFileSync as readFileSync17, writeFileSync as writeFileSync10, mkdirSync as mkdirSync11, chmodSync as chmodSync2 } from "fs";
58183
+ import { dirname as dirname12, join as join21 } from "path";
57634
58184
  function getMcpBinaryPath() {
57635
58185
  try {
57636
58186
  const p = execSync3("which todos-mcp", { encoding: "utf-8" }).trim();
57637
58187
  if (p)
57638
58188
  return p;
57639
58189
  } catch {}
57640
- const bunBin = join20(HOME2, ".bun", "bin", "todos-mcp");
57641
- if (existsSync20(bunBin))
58190
+ const bunBin = join21(HOME2, ".bun", "bin", "todos-mcp");
58191
+ if (existsSync21(bunBin))
57642
58192
  return bunBin;
57643
58193
  return "todos-mcp";
57644
58194
  }
57645
58195
  function readJsonFile2(path) {
57646
- if (!existsSync20(path))
58196
+ if (!existsSync21(path))
57647
58197
  return {};
57648
58198
  try {
57649
- return JSON.parse(readFileSync16(path, "utf-8"));
58199
+ return JSON.parse(readFileSync17(path, "utf-8"));
57650
58200
  } catch {
57651
58201
  return {};
57652
58202
  }
57653
58203
  }
57654
58204
  function writeJsonFile2(path, data) {
57655
58205
  const dir = dirname12(path);
57656
- if (!existsSync20(dir))
57657
- mkdirSync10(dir, { recursive: true });
57658
- writeFileSync9(path, JSON.stringify(data, null, 2) + `
58206
+ if (!existsSync21(dir))
58207
+ mkdirSync11(dir, { recursive: true });
58208
+ writeFileSync10(path, JSON.stringify(data, null, 2) + `
57659
58209
  `);
57660
58210
  }
57661
58211
  function readTomlFile(path) {
57662
- if (!existsSync20(path))
58212
+ if (!existsSync21(path))
57663
58213
  return "";
57664
- return readFileSync16(path, "utf-8");
58214
+ return readFileSync17(path, "utf-8");
57665
58215
  }
57666
58216
  function writeTomlFile(path, content) {
57667
58217
  const dir = dirname12(path);
57668
- if (!existsSync20(dir))
57669
- mkdirSync10(dir, { recursive: true });
57670
- writeFileSync9(path, content);
58218
+ if (!existsSync21(dir))
58219
+ mkdirSync11(dir, { recursive: true });
58220
+ writeFileSync10(path, content);
57671
58221
  }
57672
58222
  function removeTomlBlock(content, blockName) {
57673
58223
  const lines = content.split(`
@@ -57731,7 +58281,7 @@ function unregisterClaude(_global) {
57731
58281
  }
57732
58282
  }
57733
58283
  function registerCodex(binPath) {
57734
- const configPath = join20(HOME2, ".codex", "config.toml");
58284
+ const configPath = join21(HOME2, ".codex", "config.toml");
57735
58285
  let content = readTomlFile(configPath);
57736
58286
  content = removeTomlBlock(content, "mcp_servers.todos");
57737
58287
  const block = `
@@ -57745,7 +58295,7 @@ args = []
57745
58295
  console.log(chalk8.green(`Codex CLI: registered in ${configPath}`));
57746
58296
  }
57747
58297
  function unregisterCodex() {
57748
- const configPath = join20(HOME2, ".codex", "config.toml");
58298
+ const configPath = join21(HOME2, ".codex", "config.toml");
57749
58299
  let content = readTomlFile(configPath);
57750
58300
  if (!content.includes("[mcp_servers.todos]")) {
57751
58301
  console.log(chalk8.dim(`Codex CLI: todos not found in ${configPath}`));
@@ -57757,7 +58307,7 @@ function unregisterCodex() {
57757
58307
  console.log(chalk8.green(`Codex CLI: unregistered from ${configPath}`));
57758
58308
  }
57759
58309
  function registerGemini(binPath) {
57760
- const configPath = join20(HOME2, ".gemini", "settings.json");
58310
+ const configPath = join21(HOME2, ".gemini", "settings.json");
57761
58311
  const config = readJsonFile2(configPath);
57762
58312
  if (!config["mcpServers"]) {
57763
58313
  config["mcpServers"] = {};
@@ -57771,7 +58321,7 @@ function registerGemini(binPath) {
57771
58321
  console.log(chalk8.green(`Gemini CLI: registered in ${configPath}`));
57772
58322
  }
57773
58323
  function unregisterGemini() {
57774
- const configPath = join20(HOME2, ".gemini", "settings.json");
58324
+ const configPath = join21(HOME2, ".gemini", "settings.json");
57775
58325
  const config = readJsonFile2(configPath);
57776
58326
  const servers = config["mcpServers"];
57777
58327
  if (!servers || !("todos" in servers)) {
@@ -57828,9 +58378,9 @@ function registerMcpHooksCommands(program2) {
57828
58378
  if (p)
57829
58379
  todosBin = p;
57830
58380
  } catch {}
57831
- const hooksDir = join20(process.cwd(), ".claude", "hooks");
57832
- if (!existsSync20(hooksDir))
57833
- mkdirSync10(hooksDir, { recursive: true });
58381
+ const hooksDir = join21(process.cwd(), ".claude", "hooks");
58382
+ if (!existsSync21(hooksDir))
58383
+ mkdirSync11(hooksDir, { recursive: true });
57834
58384
  const hookScript = `#!/usr/bin/env bash
57835
58385
  # Auto-generated by: todos hooks install
57836
58386
  # Syncs todos with Claude Code task list on tool use events.
@@ -57854,11 +58404,11 @@ esac
57854
58404
 
57855
58405
  exit 0
57856
58406
  `;
57857
- const hookPath = join20(hooksDir, "todos-sync.sh");
57858
- writeFileSync9(hookPath, hookScript);
58407
+ const hookPath = join21(hooksDir, "todos-sync.sh");
58408
+ writeFileSync10(hookPath, hookScript);
57859
58409
  execSync3(`chmod +x "${hookPath}"`);
57860
58410
  console.log(chalk8.green(`Hook script created: ${hookPath}`));
57861
- const settingsPath = join20(process.cwd(), ".claude", "settings.json");
58411
+ const settingsPath = join21(process.cwd(), ".claude", "settings.json");
57862
58412
  const settings = readJsonFile2(settingsPath);
57863
58413
  if (!settings["hooks"]) {
57864
58414
  settings["hooks"] = {};
@@ -58727,18 +59277,18 @@ Artifacts:`));
58727
59277
  const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
58728
59278
  const hookPath = `${gitDir}/hooks/post-commit`;
58729
59279
  const marker = "# todos-auto-link";
58730
- if (existsSync20(hookPath)) {
58731
- const existing = readFileSync16(hookPath, "utf-8");
59280
+ if (existsSync21(hookPath)) {
59281
+ const existing = readFileSync17(hookPath, "utf-8");
58732
59282
  if (existing.includes(marker)) {
58733
59283
  console.log(chalk8.yellow("Hook already installed."));
58734
59284
  return;
58735
59285
  }
58736
- writeFileSync9(hookPath, existing + `
59286
+ writeFileSync10(hookPath, existing + `
58737
59287
  ${marker}
58738
59288
  $(dirname "$0")/../../scripts/post-commit-hook.sh
58739
59289
  `);
58740
59290
  } else {
58741
- writeFileSync9(hookPath, `#!/usr/bin/env bash
59291
+ writeFileSync10(hookPath, `#!/usr/bin/env bash
58742
59292
  ${marker}
58743
59293
  $(dirname "$0")/../../scripts/post-commit-hook.sh
58744
59294
  `);
@@ -58755,11 +59305,11 @@ $(dirname "$0")/../../scripts/post-commit-hook.sh
58755
59305
  const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
58756
59306
  const hookPath = `${gitDir}/hooks/post-commit`;
58757
59307
  const marker = "# todos-auto-link";
58758
- if (!existsSync20(hookPath)) {
59308
+ if (!existsSync21(hookPath)) {
58759
59309
  console.log(chalk8.dim("No post-commit hook found."));
58760
59310
  return;
58761
59311
  }
58762
- const content = readFileSync16(hookPath, "utf-8");
59312
+ const content = readFileSync17(hookPath, "utf-8");
58763
59313
  if (!content.includes(marker)) {
58764
59314
  console.log(chalk8.dim("Hook not managed by todos."));
58765
59315
  return;
@@ -58770,7 +59320,7 @@ $(dirname "$0")/../../scripts/post-commit-hook.sh
58770
59320
  if (cleaned === "#!/usr/bin/env bash" || cleaned === "") {
58771
59321
  (await import("fs")).unlinkSync(hookPath);
58772
59322
  } else {
58773
- writeFileSync9(hookPath, cleaned + `
59323
+ writeFileSync10(hookPath, cleaned + `
58774
59324
  `);
58775
59325
  }
58776
59326
  console.log(chalk8.green("Post-commit hook removed."));
@@ -58939,9 +59489,9 @@ __export(exports_machines, {
58939
59489
  });
58940
59490
  import chalk10 from "chalk";
58941
59491
  import { execSync as execSync4 } from "child_process";
58942
- import { readFileSync as readFileSync17, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "fs";
59492
+ import { readFileSync as readFileSync18, unlinkSync as unlinkSync2, writeFileSync as writeFileSync11 } from "fs";
58943
59493
  import { tmpdir as tmpdir4 } from "os";
58944
- import { join as join21 } from "path";
59494
+ import { join as join22 } from "path";
58945
59495
  function getOrCreateLocalMachineName() {
58946
59496
  return process.env["TODOS_MACHINE_NAME"] || __require("os").hostname() || "unknown";
58947
59497
  }
@@ -58979,11 +59529,11 @@ function remoteTempPath(sshAddress) {
58979
59529
  }
58980
59530
  function readRemoteBridgeBundle(sshAddress) {
58981
59531
  const remotePath = remoteTempPath(sshAddress);
58982
- const localPath = join21(tmpdir4(), `todos-bridge-pull-${uuid()}.json`);
59532
+ const localPath = join22(tmpdir4(), `todos-bridge-pull-${uuid()}.json`);
58983
59533
  try {
58984
59534
  runSsh(sshAddress, `todos export --format bridge --allow-plaintext-sensitive --output ${shellQuote(remotePath)}`, 120000);
58985
59535
  scpFromRemote(sshAddress, remotePath, localPath);
58986
- return JSON.parse(readFileSync17(localPath, "utf-8"));
59536
+ return JSON.parse(readFileSync18(localPath, "utf-8"));
58987
59537
  } finally {
58988
59538
  try {
58989
59539
  runSsh(sshAddress, `rm -f ${shellQuote(remotePath)}`, 1e4);
@@ -58994,8 +59544,8 @@ function readRemoteBridgeBundle(sshAddress) {
58994
59544
  }
58995
59545
  }
58996
59546
  function writeLocalBridgeBundle() {
58997
- const localPath = join21(tmpdir4(), `todos-bridge-push-${uuid()}.json`);
58998
- writeFileSync10(localPath, JSON.stringify(createLocalBridgeBundle(), null, 2));
59547
+ const localPath = join22(tmpdir4(), `todos-bridge-push-${uuid()}.json`);
59548
+ writeFileSync11(localPath, JSON.stringify(createLocalBridgeBundle(), null, 2));
58999
59549
  return localPath;
59000
59550
  }
59001
59551
  function pushLocalBridgeBundle(sshAddress, dryRun) {
@@ -60058,7 +60608,7 @@ __export(exports_onboarding_commands, {
60058
60608
  registerOnboardingCommands: () => registerOnboardingCommands
60059
60609
  });
60060
60610
  import chalk17 from "chalk";
60061
- import { resolve as resolve20 } from "path";
60611
+ import { resolve as resolve21 } from "path";
60062
60612
  function registerOnboardingCommands(program2) {
60063
60613
  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) => {
60064
60614
  const globalOpts = program2.opts();
@@ -60074,7 +60624,7 @@ function registerOnboardingCommands(program2) {
60074
60624
  return;
60075
60625
  }
60076
60626
  if (opts.write) {
60077
- const result = writeOnboardingFixtureFiles2(resolve20(opts.write));
60627
+ const result = writeOnboardingFixtureFiles2(resolve21(opts.write));
60078
60628
  if (globalOpts.json) {
60079
60629
  output(result, true);
60080
60630
  return;
@@ -63522,8 +64072,8 @@ __export(exports_sdk_integration_fixtures, {
63522
64072
  TODOS_SDK_INTEGRATION_FIXTURE_SCHEMA_VERSION: () => TODOS_SDK_INTEGRATION_FIXTURE_SCHEMA_VERSION,
63523
64073
  TODOS_SDK_INTEGRATION_FIXTURE_GENERATED_AT: () => TODOS_SDK_INTEGRATION_FIXTURE_GENERATED_AT
63524
64074
  });
63525
- import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
63526
- import { join as join22 } from "path";
64075
+ import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync12 } from "fs";
64076
+ import { join as join23 } from "path";
63527
64077
  function source5(version) {
63528
64078
  return {
63529
64079
  packageName: "@hasna/todos",
@@ -63619,7 +64169,7 @@ function createSdkIntegrationFixturePack(options = {}) {
63619
64169
  };
63620
64170
  }
63621
64171
  function writeSdkIntegrationFixtures(directory, options = {}) {
63622
- mkdirSync11(directory, { recursive: true });
64172
+ mkdirSync12(directory, { recursive: true });
63623
64173
  const pack = createSdkIntegrationFixturePack(options);
63624
64174
  const bundle = getOnboardingFixtureBundle("agent-project-demo");
63625
64175
  const files = [
@@ -63630,8 +64180,8 @@ function writeSdkIntegrationFixtures(directory, options = {}) {
63630
64180
  ];
63631
64181
  const written = [];
63632
64182
  for (const [name, payload] of files) {
63633
- const file = join22(directory, name);
63634
- writeFileSync11(file, `${JSON.stringify(payload, null, 2)}
64183
+ const file = join23(directory, name);
64184
+ writeFileSync12(file, `${JSON.stringify(payload, null, 2)}
63635
64185
  `, "utf-8");
63636
64186
  written.push(file);
63637
64187
  }
@@ -63654,7 +64204,7 @@ __export(exports_sdk_fixture_commands, {
63654
64204
  registerSdkFixtureCommands: () => registerSdkFixtureCommands
63655
64205
  });
63656
64206
  import chalk19 from "chalk";
63657
- import { resolve as resolve21 } from "path";
64207
+ import { resolve as resolve22 } from "path";
63658
64208
  function registerSdkFixtureCommands(program2) {
63659
64209
  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) => {
63660
64210
  const globalOpts = program2.opts();
@@ -63665,7 +64215,7 @@ function registerSdkFixtureCommands(program2) {
63665
64215
  writeSdkIntegrationFixtures: writeSdkIntegrationFixtures2
63666
64216
  } = await Promise.resolve().then(() => (init_sdk_integration_fixtures(), exports_sdk_integration_fixtures));
63667
64217
  if (opts.write) {
63668
- const result = writeSdkIntegrationFixtures2(resolve21(opts.write));
64218
+ const result = writeSdkIntegrationFixtures2(resolve22(opts.write));
63669
64219
  if (globalOpts.json) {
63670
64220
  console.log(JSON.stringify(result));
63671
64221
  return;
@@ -63895,7 +64445,7 @@ var exports_roadmap_commands = {};
63895
64445
  __export(exports_roadmap_commands, {
63896
64446
  registerRoadmapCommands: () => registerRoadmapCommands
63897
64447
  });
63898
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync12 } from "fs";
64448
+ import { readFileSync as readFileSync19, writeFileSync as writeFileSync13 } from "fs";
63899
64449
  import chalk21 from "chalk";
63900
64450
  function splitList3(value) {
63901
64451
  return value?.split(";").flatMap((part) => part.split(",")).map((item) => item.trim()).filter(Boolean);
@@ -64106,7 +64656,7 @@ function registerRoadmapCommands(program2) {
64106
64656
  const { exportRoadmapBundle: exportRoadmapBundle2, renderRoadmapMarkdown: renderRoadmapMarkdown2 } = await Promise.resolve().then(() => (init_roadmaps(), exports_roadmaps));
64107
64657
  const content = opts.format === "markdown" ? renderRoadmapMarkdown2(roadmap) : JSON.stringify(exportRoadmapBundle2(roadmap), null, 2);
64108
64658
  if (opts.out) {
64109
- writeFileSync12(opts.out, content);
64659
+ writeFileSync13(opts.out, content);
64110
64660
  if (!globalOpts.json)
64111
64661
  console.log(chalk21.green(`Wrote roadmap export to ${opts.out}`));
64112
64662
  }
@@ -64124,7 +64674,7 @@ function registerRoadmapCommands(program2) {
64124
64674
  const globalOpts = globalOptions(program2);
64125
64675
  try {
64126
64676
  const { importRoadmapBundle: importRoadmapBundle2 } = await Promise.resolve().then(() => (init_roadmaps(), exports_roadmaps));
64127
- const bundle = JSON.parse(readFileSync18(path, "utf8"));
64677
+ const bundle = JSON.parse(readFileSync19(path, "utf8"));
64128
64678
  const result = importRoadmapBundle2(bundle, { apply: Boolean(opts.apply) });
64129
64679
  if (globalOpts.json) {
64130
64680
  output(result, true);
@@ -64487,7 +65037,7 @@ __export(exports_local_backup_commands, {
64487
65037
  registerLocalBackupCommands: () => registerLocalBackupCommands
64488
65038
  });
64489
65039
  import chalk26 from "chalk";
64490
- import { resolve as resolve22 } from "path";
65040
+ import { resolve as resolve23 } from "path";
64491
65041
  function globalOptions6(program2) {
64492
65042
  const command = program2;
64493
65043
  return command.optsWithGlobals?.() ?? program2.opts();
@@ -64509,10 +65059,10 @@ function registerLocalBackupCommands(program2) {
64509
65059
  const projectId = opts.projectId ?? autoProject(globalOpts);
64510
65060
  const backupBundle = createLocalBackup2({
64511
65061
  project_id: projectId,
64512
- output_path: opts.output ? resolve22(opts.output) : undefined
65062
+ output_path: opts.output ? resolve23(opts.output) : undefined
64513
65063
  });
64514
65064
  const result = {
64515
- output_path: opts.output ? resolve22(opts.output) : null,
65065
+ output_path: opts.output ? resolve23(opts.output) : null,
64516
65066
  backup: backupBundle
64517
65067
  };
64518
65068
  if (opts.json || globalOpts.json) {
@@ -65922,9 +66472,17 @@ async function updateProject2(id, input, store) {
65922
66472
  }
65923
66473
  async function createPlan2(input, store, context) {
65924
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
+ });
65925
66482
  return store.upsert("plans", {
65926
66483
  id: randomUUID3(),
65927
- project_id: input.project_id ?? context?.projectId ?? null,
66484
+ slug,
66485
+ project_id: projectId,
65928
66486
  task_list_id: input.task_list_id ?? context?.taskListId ?? null,
65929
66487
  agent_id: input.agent_id ?? context?.agentId ?? null,
65930
66488
  name: input.name,
@@ -65938,7 +66496,17 @@ async function createPlan2(input, store, context) {
65938
66496
  }
65939
66497
  async function updatePlan2(id, input, store) {
65940
66498
  const plan = await requireRecord("plans", id, store);
65941
- 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() });
65942
66510
  }
65943
66511
  async function registerAgent2(input, store, context) {
65944
66512
  const existing = (await store.list("agents")).find((agent2) => agent2.name === input.name && agent2.status !== "archived");
@@ -66191,8 +66759,38 @@ function matchesOne(value, expected) {
66191
66759
  function priorityRank2(priority) {
66192
66760
  return { critical: 0, high: 1, medium: 2, low: 3 }[priority];
66193
66761
  }
66762
+ function slugifyRaw(value) {
66763
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
66764
+ }
66194
66765
  function slugify2(value) {
66195
- 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;
66196
66794
  }
66197
66795
  function definedPatch(value) {
66198
66796
  return Object.fromEntries(Object.entries(value).filter(([, entry2]) => entry2 !== undefined));