@hasna/todos 0.15.34 → 0.15.35

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.
@@ -2900,6 +2900,10 @@ var init_migrations = __esm(() => {
2900
2900
  );
2901
2901
  INSERT OR IGNORE INTO _migrations (id) VALUES (70);
2902
2902
  COMMIT;
2903
+ `,
2904
+ `BEGIN;
2905
+ INSERT OR IGNORE INTO _migrations (id) VALUES (71);
2906
+ COMMIT;
2903
2907
  `
2904
2908
  ];
2905
2909
  });
@@ -3573,6 +3577,8 @@ function ensureSchema(db) {
3573
3577
  ensureColumn("projects", "task_list_id", "TEXT");
3574
3578
  ensureColumn("projects", "task_prefix", "TEXT");
3575
3579
  ensureColumn("projects", "task_counter", "INTEGER NOT NULL DEFAULT 0");
3580
+ ensureColumn("projects", "parent_id", "TEXT REFERENCES projects(id) ON DELETE SET NULL");
3581
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_projects_parent_id ON projects(parent_id)");
3576
3582
  ensureColumn("tasks", "plan_id", "TEXT REFERENCES plans(id) ON DELETE SET NULL");
3577
3583
  ensureColumn("tasks", "task_list_id", "TEXT REFERENCES task_lists(id) ON DELETE SET NULL");
3578
3584
  ensureColumn("tasks", "short_id", "TEXT");
@@ -5574,17 +5580,24 @@ function createProject2(input, db) {
5574
5580
  const id = uuid();
5575
5581
  const timestamp2 = now();
5576
5582
  const derivedSlug = slugify(input.name);
5577
- const taskListId = input.task_list_id === undefined ? `todos-${derivedSlug}` : slugify(input.task_list_id);
5583
+ const taskListId = input.task_list_id === undefined ? derivedSlug : slugify(input.task_list_id);
5578
5584
  if (!derivedSlug || !taskListId)
5579
5585
  throw new Error("Project name and task-list slug must be non-empty");
5580
5586
  const slugConflict = d.query("SELECT id FROM projects WHERE task_list_id = ? LIMIT 1").get(taskListId);
5581
5587
  if (slugConflict || !claimCanonicalSlug("project", "global", taskListId, id, d)) {
5582
5588
  throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${taskListId}" already exists`);
5583
5589
  }
5590
+ const parentId = input.parent_id ?? null;
5591
+ if (parentId !== null) {
5592
+ const parent = getProject(parentId, d);
5593
+ if (!parent)
5594
+ throw new ProjectNotFoundError(parentId);
5595
+ assertNotProjectAncestor(id, parentId, d);
5596
+ }
5584
5597
  const taskPrefix = input.task_prefix || generatePrefix(input.name, d);
5585
5598
  const machineId = currentStorageMachineId(d);
5586
- d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at, machine_id)
5587
- VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp2, timestamp2, machineId]);
5599
+ d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, parent_id, created_at, updated_at, machine_id)
5600
+ VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, parentId, timestamp2, timestamp2, machineId]);
5588
5601
  return getProject(id, d);
5589
5602
  })();
5590
5603
  }
@@ -5609,6 +5622,60 @@ function listProjects(db) {
5609
5622
  const d = db || getDatabase();
5610
5623
  return d.query("SELECT * FROM projects ORDER BY name").all();
5611
5624
  }
5625
+ function orderProjectsParentFirst(projects) {
5626
+ const projectId = (project) => {
5627
+ const id = project.id;
5628
+ return typeof id === "string" && id.length > 0 ? id : null;
5629
+ };
5630
+ const parentId = (project) => {
5631
+ const parent = project.parent_id;
5632
+ return parent == null ? null : String(parent);
5633
+ };
5634
+ const byId = new Set;
5635
+ for (const project of projects) {
5636
+ const id = projectId(project);
5637
+ if (id !== null)
5638
+ byId.add(id);
5639
+ }
5640
+ const ordered = [];
5641
+ const emitted = new Set;
5642
+ let remaining = [...projects];
5643
+ let progress = true;
5644
+ while (progress && remaining.length > 0) {
5645
+ progress = false;
5646
+ const deferred = [];
5647
+ for (const project of remaining) {
5648
+ const parent = parentId(project);
5649
+ if (parent === null || emitted.has(parent) || !byId.has(parent)) {
5650
+ ordered.push(project);
5651
+ const id = projectId(project);
5652
+ if (id !== null)
5653
+ emitted.add(id);
5654
+ progress = true;
5655
+ } else {
5656
+ deferred.push(project);
5657
+ }
5658
+ }
5659
+ remaining = deferred;
5660
+ }
5661
+ ordered.push(...remaining);
5662
+ return ordered;
5663
+ }
5664
+ function assertNotProjectAncestor(projectId, ancestorId, db) {
5665
+ const d = db || getDatabase();
5666
+ let cursor = ancestorId;
5667
+ const seen = new Set;
5668
+ while (cursor !== null) {
5669
+ if (cursor === projectId) {
5670
+ throw new ResourceConflictError("PROJECT_PARENT_CYCLE", `Project "${projectId}" cannot be placed under its own descendant`);
5671
+ }
5672
+ if (seen.has(cursor))
5673
+ break;
5674
+ seen.add(cursor);
5675
+ const row = d.query("SELECT parent_id FROM projects WHERE id = ?").get(cursor);
5676
+ cursor = row?.parent_id ?? null;
5677
+ }
5678
+ }
5612
5679
  function updateProject2(id, input, db) {
5613
5680
  const d = db || getDatabase();
5614
5681
  const project = getProject(id, d);
@@ -5631,6 +5698,16 @@ function updateProject2(id, input, db) {
5631
5698
  sets.push("path = ?");
5632
5699
  params.push(input.path);
5633
5700
  }
5701
+ if (input.parent_id !== undefined) {
5702
+ if (input.parent_id !== null) {
5703
+ const parent = getProject(input.parent_id, d);
5704
+ if (!parent)
5705
+ throw new ProjectNotFoundError(input.parent_id);
5706
+ assertNotProjectAncestor(id, input.parent_id, d);
5707
+ }
5708
+ sets.push("parent_id = ?");
5709
+ params.push(input.parent_id);
5710
+ }
5634
5711
  params.push(id);
5635
5712
  d.run(`UPDATE projects SET ${sets.join(", ")} WHERE id = ?`, params);
5636
5713
  return getProject(id, d);
@@ -12731,7 +12808,7 @@ import { createHash as createHash7 } from "crypto";
12731
12808
  // package.json
12732
12809
  var package_default = {
12733
12810
  name: "@hasna/todos",
12734
- version: "0.15.34",
12811
+ version: "0.15.35",
12735
12812
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
12736
12813
  type: "module",
12737
12814
  main: "dist/index.js",
@@ -12806,6 +12883,9 @@ var package_default = {
12806
12883
  "verify:release-review": "bun run scripts/verify-npm-release-agent-review.ts",
12807
12884
  "verify:attested-container-candidate": "bun run scripts/attested-container-candidate.ts verify",
12808
12885
  "test:attested-container-candidate": "bun test scripts/attested-container-candidate.test.ts",
12886
+ "emit:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts emit",
12887
+ "verify:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts verify",
12888
+ "test:attested-container-compatibility-vector": "bun test scripts/attested-container-compatibility-vector.test.ts",
12809
12889
  "issue:release-review": "bun run scripts/issue-npm-release-agent-review.ts",
12810
12890
  prepublishOnly: "bun run scripts/verify-public-release.ts --mode=publish",
12811
12891
  postinstall: "mkdir -p $HOME/.hasna/todos $HOME/.hasna/todos/training 2>/dev/null || true"
@@ -14252,8 +14332,12 @@ class PostgresJsonRecordStore {
14252
14332
  OR ($11::text <> $2
14253
14333
  AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE object_id = $2)
14254
14334
  AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE cycle))) AS parent_acyclic,
14255
- (SELECT count(*) FROM locked_plans) = jsonb_array_length($7::jsonb) AS all_plans_found,
14256
- ($8::text IS NULL OR EXISTS (SELECT 1 FROM locked_plans WHERE object_id = $8)) AS target_plan_found,
14335
+ (COALESCE((SELECT payload->>'plan_id' FROM locked_task), '')
14336
+ IS DISTINCT FROM COALESCE($3::jsonb->>'plan_id', '')) AS membership_changed,
14337
+ (NOT (COALESCE((SELECT payload->>'plan_id' FROM locked_task), '')
14338
+ IS DISTINCT FROM COALESCE($3::jsonb->>'plan_id', ''))
14339
+ OR $8::text IS NULL
14340
+ OR EXISTS (SELECT 1 FROM locked_plans WHERE object_id = $8)) AS target_plan_found,
14257
14341
  (SELECT payload->>'project_id' FROM locked_plans WHERE object_id = $8) AS target_project_id
14258
14342
  ), guarded AS (
14259
14343
  SELECT
@@ -14276,7 +14360,6 @@ class PostgresJsonRecordStore {
14276
14360
  AND guarded.version_matches
14277
14361
  AND guarded.parent_found
14278
14362
  AND guarded.parent_acyclic
14279
- AND guarded.all_plans_found
14280
14363
  AND guarded.target_plan_found
14281
14364
  AND NOT guarded.project_conflict
14282
14365
  ON CONFLICT (service, object_type, object_id) DO UPDATE SET
@@ -14292,7 +14375,7 @@ class PostgresJsonRecordStore {
14292
14375
  RETURNING payload
14293
14376
  )
14294
14377
  SELECT guarded.task_found, guarded.version_matches, guarded.parent_found, guarded.parent_acyclic,
14295
- guarded.all_plans_found, guarded.target_plan_found, guarded.project_conflict,
14378
+ guarded.membership_changed, guarded.target_plan_found, guarded.project_conflict,
14296
14379
  (SELECT payload FROM stored) AS payload,
14297
14380
  (SELECT payload FROM locked_task) AS current_payload
14298
14381
  FROM guarded`, [
@@ -14324,7 +14407,7 @@ class PostgresJsonRecordStore {
14324
14407
  if (parentGuard && !row?.parent_acyclic && parentGuard.parentId) {
14325
14408
  throw new ResourceConflictError("TASK_PARENT_CYCLE", `TASK_PARENT_CYCLE: assigning parent ${parentGuard.parentId} to task ${value.id} would create or retain a parent cycle`);
14326
14409
  }
14327
- if (!row?.all_plans_found || !row.target_plan_found) {
14410
+ if (!row?.target_plan_found) {
14328
14411
  throw new PlanProjectLinkError("PLAN_PROJECT_LINK_PLAN_NOT_FOUND", `Plan membership changed through a missing plan: ${targetPlanId ?? planIds.join(", ")}`, { plan_ids: planIds, target_plan_id: targetPlanId });
14329
14412
  }
14330
14413
  if (row.project_conflict) {
@@ -15536,17 +15619,26 @@ async function getChangedSince(since, filters, store) {
15536
15619
  async function createProject(input, store, context) {
15537
15620
  const timestamp = new Date().toISOString();
15538
15621
  const derivedSlug = slugifyRaw(input.name);
15539
- const taskListId = input.task_list_id === undefined ? `todos-${derivedSlug}` : slugifyRaw(input.task_list_id);
15622
+ const taskListId = input.task_list_id === undefined ? derivedSlug : slugifyRaw(input.task_list_id);
15540
15623
  if (!derivedSlug || !taskListId)
15541
15624
  throw new Error("Project name and task-list slug must be non-empty");
15625
+ const parentId = input.parent_id ?? null;
15626
+ const id = randomUUID();
15627
+ if (parentId !== null) {
15628
+ const parent = await store.get("projects", parentId);
15629
+ if (!parent)
15630
+ throw new ProjectNotFoundError(parentId);
15631
+ await assertNotProjectAncestorPostgres(id, parentId, store);
15632
+ }
15542
15633
  const project = {
15543
- id: randomUUID(),
15634
+ id,
15544
15635
  name: input.name,
15545
15636
  path: input.path,
15546
15637
  description: input.description ?? null,
15547
15638
  task_list_id: taskListId,
15548
15639
  task_prefix: input.task_prefix ?? await generateProjectPrefix(input.name, store),
15549
15640
  task_counter: 0,
15641
+ parent_id: parentId,
15550
15642
  created_at: timestamp,
15551
15643
  updated_at: timestamp,
15552
15644
  machine_id: store.machineId(context),
@@ -15554,12 +15646,38 @@ async function createProject(input, store, context) {
15554
15646
  };
15555
15647
  return store.upsert("projects", project, context);
15556
15648
  }
15649
+ async function assertNotProjectAncestorPostgres(projectId, candidateParentId, store) {
15650
+ const all = await store.list("projects");
15651
+ const byId = new Map(all.map((project) => [project.id, project.parent_id ?? null]));
15652
+ let cursor = candidateParentId;
15653
+ const seen = new Set;
15654
+ while (cursor !== null) {
15655
+ if (cursor === projectId) {
15656
+ throw new ResourceConflictError("PROJECT_PARENT_CYCLE", `Project "${projectId}" cannot be placed under its own descendant`);
15657
+ }
15658
+ if (seen.has(cursor))
15659
+ break;
15660
+ seen.add(cursor);
15661
+ cursor = byId.get(cursor) ?? null;
15662
+ }
15663
+ }
15557
15664
  async function updateProject(id, input, store) {
15558
15665
  if ("task_list_id" in input) {
15559
15666
  throw new Error("task_list_id cannot be changed by updateProject; use renameProject for an atomic canonical rename");
15560
15667
  }
15561
15668
  const project = await requireRecord("projects", id, store);
15562
- const updated = { ...project, ...definedPatch(input), updated_at: new Date().toISOString() };
15669
+ if (input.parent_id !== undefined && input.parent_id !== null) {
15670
+ const parent = await store.get("projects", input.parent_id);
15671
+ if (!parent)
15672
+ throw new ProjectNotFoundError(input.parent_id);
15673
+ await assertNotProjectAncestorPostgres(id, input.parent_id, store);
15674
+ }
15675
+ const updated = {
15676
+ ...project,
15677
+ ...definedPatch(input),
15678
+ ...input.parent_id !== undefined ? { parent_id: input.parent_id } : {},
15679
+ updated_at: new Date().toISOString()
15680
+ };
15563
15681
  return store.upsert("projects", updated);
15564
15682
  }
15565
15683
  async function createPlan(input, store, context) {
@@ -17570,6 +17688,7 @@ var PROJECT_COLUMNS = [
17570
17688
  "task_list_id",
17571
17689
  "task_prefix",
17572
17690
  "task_counter",
17691
+ "parent_id",
17573
17692
  "created_at",
17574
17693
  "updated_at",
17575
17694
  "machine_id",
@@ -17789,7 +17908,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
17789
17908
  }
17790
17909
  }
17791
17910
  };
17792
- applyRows("projects", "projects", PROJECT_COLUMNS, snapshot.projects, "updated_at");
17911
+ applyRows("projects", "projects", PROJECT_COLUMNS, orderProjectsParentFirst(snapshot.projects), "updated_at");
17793
17912
  applyRows("project_machine_paths", "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS, snapshot.projectMachinePaths ?? [], "updated_at");
17794
17913
  applyRows("agents", "agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
17795
17914
  applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
@@ -18619,7 +18738,7 @@ class StagedSqliteTodosProjectRegistrationTransaction {
18619
18738
  }
18620
18739
  async createProject(input) {
18621
18740
  const derivedSlug = normalizeSlug(input.name);
18622
- const taskListId = input.task_list_id === undefined ? `todos-${derivedSlug}` : normalizeSlug(input.task_list_id);
18741
+ const taskListId = input.task_list_id === undefined ? derivedSlug : normalizeSlug(input.task_list_id);
18623
18742
  if (!derivedSlug || !taskListId) {
18624
18743
  throw new Error("Project name and task-list slug must be non-empty");
18625
18744
  }
@@ -18631,6 +18750,7 @@ class StagedSqliteTodosProjectRegistrationTransaction {
18631
18750
  task_list_id: taskListId,
18632
18751
  task_prefix: input.task_prefix ?? this.availableProjectPrefix(input.name),
18633
18752
  task_counter: 0,
18753
+ parent_id: input.parent_id ?? null,
18634
18754
  created_at: now(),
18635
18755
  updated_at: now(),
18636
18756
  machine_id: currentStorageMachineId(this.db)
@@ -18641,14 +18761,15 @@ class StagedSqliteTodosProjectRegistrationTransaction {
18641
18761
  try {
18642
18762
  const result = this.db.run(`INSERT INTO projects (
18643
18763
  id, name, path, description, task_list_id, task_prefix,
18644
- task_counter, created_at, updated_at, machine_id
18645
- ) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [
18764
+ task_counter, parent_id, created_at, updated_at, machine_id
18765
+ ) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?)`, [
18646
18766
  project.id,
18647
18767
  project.name,
18648
18768
  project.path,
18649
18769
  project.description,
18650
18770
  project.task_list_id,
18651
18771
  project.task_prefix,
18772
+ project.parent_id,
18652
18773
  project.created_at,
18653
18774
  project.updated_at,
18654
18775
  project.machine_id ?? null
@@ -19044,7 +19165,10 @@ function taskListSlug(projectSlug) {
19044
19165
  if (!slug || slug !== projectSlug) {
19045
19166
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "project_slug must be canonical kebab-case");
19046
19167
  }
19047
- return `todos-${slug}`;
19168
+ return slug;
19169
+ }
19170
+ function legacyTaskListSlug(projectSlug) {
19171
+ return `todos-${normalizeSlug(projectSlug)}`;
19048
19172
  }
19049
19173
  function deterministicTaskPrefix(projectSlug) {
19050
19174
  const letters = projectSlug.replace(/[^a-z0-9]/gi, "").toUpperCase();
@@ -19563,6 +19687,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
19563
19687
  created_by_operation: false
19564
19688
  };
19565
19689
  }
19690
+ if (request.bind_existing === true && conflict2.path === path && conflict2.task_list_id === legacyTaskListSlug(request.project_slug)) {
19691
+ return {
19692
+ record: boundExistingProjectRecord(conflict2),
19693
+ created_by_operation: false
19694
+ };
19695
+ }
19566
19696
  return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict2.id });
19567
19697
  }
19568
19698
  await this.fault("before_object_write", request);
@@ -19599,6 +19729,15 @@ class PackageOwnedTodosProjectRegistrationAuthority {
19599
19729
  }
19600
19730
  return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict.id });
19601
19731
  }
19732
+ if (request.bind_existing === true) {
19733
+ const legacy = await transaction.findTaskListConflict(todosProjectId, legacyTaskListSlug(request.project_slug));
19734
+ if (legacy && legacy.project_id === todosProjectId) {
19735
+ return {
19736
+ record: boundExistingTaskListRecord(legacy),
19737
+ created_by_operation: false
19738
+ };
19739
+ }
19740
+ }
19602
19741
  await this.fault("before_object_write", request);
19603
19742
  const taskList = await transaction.createTaskList({
19604
19743
  name: request.project_name,
package/dist/registry.js CHANGED
@@ -1884,6 +1884,10 @@ var init_migrations = __esm(() => {
1884
1884
  );
1885
1885
  INSERT OR IGNORE INTO _migrations (id) VALUES (70);
1886
1886
  COMMIT;
1887
+ `,
1888
+ `BEGIN;
1889
+ INSERT OR IGNORE INTO _migrations (id) VALUES (71);
1890
+ COMMIT;
1887
1891
  `
1888
1892
  ];
1889
1893
  });
@@ -2557,6 +2561,8 @@ function ensureSchema(db) {
2557
2561
  ensureColumn("projects", "task_list_id", "TEXT");
2558
2562
  ensureColumn("projects", "task_prefix", "TEXT");
2559
2563
  ensureColumn("projects", "task_counter", "INTEGER NOT NULL DEFAULT 0");
2564
+ ensureColumn("projects", "parent_id", "TEXT REFERENCES projects(id) ON DELETE SET NULL");
2565
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_projects_parent_id ON projects(parent_id)");
2560
2566
  ensureColumn("tasks", "plan_id", "TEXT REFERENCES plans(id) ON DELETE SET NULL");
2561
2567
  ensureColumn("tasks", "task_list_id", "TEXT REFERENCES task_lists(id) ON DELETE SET NULL");
2562
2568
  ensureColumn("tasks", "short_id", "TEXT");
@@ -5045,17 +5051,24 @@ function createProject(input, db) {
5045
5051
  const id = uuid();
5046
5052
  const timestamp2 = now();
5047
5053
  const derivedSlug = slugify(input.name);
5048
- const taskListId = input.task_list_id === undefined ? `todos-${derivedSlug}` : slugify(input.task_list_id);
5054
+ const taskListId = input.task_list_id === undefined ? derivedSlug : slugify(input.task_list_id);
5049
5055
  if (!derivedSlug || !taskListId)
5050
5056
  throw new Error("Project name and task-list slug must be non-empty");
5051
5057
  const slugConflict = d.query("SELECT id FROM projects WHERE task_list_id = ? LIMIT 1").get(taskListId);
5052
5058
  if (slugConflict || !claimCanonicalSlug("project", "global", taskListId, id, d)) {
5053
5059
  throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${taskListId}" already exists`);
5054
5060
  }
5061
+ const parentId = input.parent_id ?? null;
5062
+ if (parentId !== null) {
5063
+ const parent = getProject(parentId, d);
5064
+ if (!parent)
5065
+ throw new ProjectNotFoundError(parentId);
5066
+ assertNotProjectAncestor(id, parentId, d);
5067
+ }
5055
5068
  const taskPrefix = input.task_prefix || generatePrefix(input.name, d);
5056
5069
  const machineId = currentStorageMachineId(d);
5057
- d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at, machine_id)
5058
- VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp2, timestamp2, machineId]);
5070
+ d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, parent_id, created_at, updated_at, machine_id)
5071
+ VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, parentId, timestamp2, timestamp2, machineId]);
5059
5072
  return getProject(id, d);
5060
5073
  })();
5061
5074
  }
@@ -5080,6 +5093,60 @@ function listProjects(db) {
5080
5093
  const d = db || getDatabase();
5081
5094
  return d.query("SELECT * FROM projects ORDER BY name").all();
5082
5095
  }
5096
+ function orderProjectsParentFirst(projects) {
5097
+ const projectId = (project) => {
5098
+ const id = project.id;
5099
+ return typeof id === "string" && id.length > 0 ? id : null;
5100
+ };
5101
+ const parentId = (project) => {
5102
+ const parent = project.parent_id;
5103
+ return parent == null ? null : String(parent);
5104
+ };
5105
+ const byId = new Set;
5106
+ for (const project of projects) {
5107
+ const id = projectId(project);
5108
+ if (id !== null)
5109
+ byId.add(id);
5110
+ }
5111
+ const ordered = [];
5112
+ const emitted = new Set;
5113
+ let remaining = [...projects];
5114
+ let progress = true;
5115
+ while (progress && remaining.length > 0) {
5116
+ progress = false;
5117
+ const deferred = [];
5118
+ for (const project of remaining) {
5119
+ const parent = parentId(project);
5120
+ if (parent === null || emitted.has(parent) || !byId.has(parent)) {
5121
+ ordered.push(project);
5122
+ const id = projectId(project);
5123
+ if (id !== null)
5124
+ emitted.add(id);
5125
+ progress = true;
5126
+ } else {
5127
+ deferred.push(project);
5128
+ }
5129
+ }
5130
+ remaining = deferred;
5131
+ }
5132
+ ordered.push(...remaining);
5133
+ return ordered;
5134
+ }
5135
+ function assertNotProjectAncestor(projectId, ancestorId, db) {
5136
+ const d = db || getDatabase();
5137
+ let cursor = ancestorId;
5138
+ const seen = new Set;
5139
+ while (cursor !== null) {
5140
+ if (cursor === projectId) {
5141
+ throw new ResourceConflictError("PROJECT_PARENT_CYCLE", `Project "${projectId}" cannot be placed under its own descendant`);
5142
+ }
5143
+ if (seen.has(cursor))
5144
+ break;
5145
+ seen.add(cursor);
5146
+ const row = d.query("SELECT parent_id FROM projects WHERE id = ?").get(cursor);
5147
+ cursor = row?.parent_id ?? null;
5148
+ }
5149
+ }
5083
5150
  function updateProject(id, input, db) {
5084
5151
  const d = db || getDatabase();
5085
5152
  const project = getProject(id, d);
@@ -5102,6 +5169,16 @@ function updateProject(id, input, db) {
5102
5169
  sets.push("path = ?");
5103
5170
  params.push(input.path);
5104
5171
  }
5172
+ if (input.parent_id !== undefined) {
5173
+ if (input.parent_id !== null) {
5174
+ const parent = getProject(input.parent_id, d);
5175
+ if (!parent)
5176
+ throw new ProjectNotFoundError(input.parent_id);
5177
+ assertNotProjectAncestor(id, input.parent_id, d);
5178
+ }
5179
+ sets.push("parent_id = ?");
5180
+ params.push(input.parent_id);
5181
+ }
5105
5182
  params.push(id);
5106
5183
  d.run(`UPDATE projects SET ${sets.join(", ")} WHERE id = ?`, params);
5107
5184
  return getProject(id, d);
@@ -12728,7 +12805,7 @@ var init_tasks = __esm(() => {
12728
12805
  // package.json
12729
12806
  var package_default = {
12730
12807
  name: "@hasna/todos",
12731
- version: "0.15.34",
12808
+ version: "0.15.35",
12732
12809
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
12733
12810
  type: "module",
12734
12811
  main: "dist/index.js",
@@ -12803,6 +12880,9 @@ var package_default = {
12803
12880
  "verify:release-review": "bun run scripts/verify-npm-release-agent-review.ts",
12804
12881
  "verify:attested-container-candidate": "bun run scripts/attested-container-candidate.ts verify",
12805
12882
  "test:attested-container-candidate": "bun test scripts/attested-container-candidate.test.ts",
12883
+ "emit:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts emit",
12884
+ "verify:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts verify",
12885
+ "test:attested-container-compatibility-vector": "bun test scripts/attested-container-compatibility-vector.test.ts",
12806
12886
  "issue:release-review": "bun run scripts/issue-npm-release-agent-review.ts",
12807
12887
  prepublishOnly: "bun run scripts/verify-public-release.ts --mode=publish",
12808
12888
  postinstall: "mkdir -p $HOME/.hasna/todos $HOME/.hasna/todos/training 2>/dev/null || true"
@@ -13491,6 +13571,7 @@ var TODOS_JSON_CONTRACTS = [
13491
13571
  task_list_id: nullableIdField,
13492
13572
  task_prefix: field(["string", "null"], "Optional task prefix.", true),
13493
13573
  task_counter: field("integer", "Monotonic project task counter."),
13574
+ parent_id: field(["string", "null"], "Optional parent project id; null means top-level.", true),
13494
13575
  created_at: isoDateField,
13495
13576
  updated_at: isoDateField
13496
13577
  },
@@ -16776,6 +16857,7 @@ function createAgentProjectDemoBundle() {
16776
16857
  task_list_id: ids.list,
16777
16858
  task_prefix: "DEMO",
16778
16859
  task_counter: 4,
16860
+ parent_id: null,
16779
16861
  created_at: createdAt,
16780
16862
  updated_at: completedAt
16781
16863
  });
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "packageName": "@hasna/todos",
3
- "packageVersion": "0.15.34",
3
+ "packageVersion": "0.15.35",
4
4
  "repository": "https://github.com/hasna/todos.git",
5
- "gitCommit": "1e6a7cf5727c4e29cc48d4fabad68602a1bdb183",
6
- "gitTree": "2a0facd155af45e5d46ced7ae193643b223ff4e5",
7
- "sourceTreeSha256": "e735d06856448b5f3f5903fa141b5201c768077e6531fb7761ab65b5f54a1b8a",
8
- "generatedAt": "2026-08-15T18:18:40.000Z"
5
+ "gitCommit": "0d94d7e2b58f09974c00f68b96c09acfd25128a8",
6
+ "gitTree": "44f6dc53a613820140b8f2ec1b5deb6e2d367edd",
7
+ "sourceTreeSha256": "35acd37b72272fe8e51d841bc0a58de973d20adb78ce8cf0963a9d3c895fbea1",
8
+ "generatedAt": "2026-08-17T17:25:19.000Z"
9
9
  }
@@ -25,6 +25,7 @@ export interface Project {
25
25
  "task_list_id"?: string | null;
26
26
  "task_prefix"?: string | null;
27
27
  "task_counter"?: number;
28
+ "parent_id"?: string | null;
28
29
  "created_at"?: string;
29
30
  "updated_at"?: string;
30
31
  }
@@ -518,11 +519,13 @@ export interface CreateProjectInput {
518
519
  "description"?: string;
519
520
  "task_list_id"?: string;
520
521
  "task_prefix"?: string;
522
+ "parent_id"?: string;
521
523
  }
522
524
  export interface UpdateProjectInput {
523
525
  "name"?: string;
524
526
  "path"?: string;
525
527
  "description"?: string | null;
528
+ "parent_id"?: string | null;
526
529
  }
527
530
  export interface RenameProjectInput {
528
531
  "new_slug": string;