@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.
- package/dist/cli/cloud-router.d.ts +1 -1
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/project-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +255 -43
- package/dist/contracts.js +86 -4
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/projects.d.ts +23 -0
- package/dist/db/projects.d.ts.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/index.js +209 -39
- package/dist/json-contracts.d.ts.map +1 -1
- package/dist/lib/import-export-bridge.d.ts.map +1 -1
- package/dist/lib/onboarding-fixtures.d.ts.map +1 -1
- package/dist/mcp/index.js +221 -39
- package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
- package/dist/mcp.js +4 -1
- package/dist/project-registration/authority.d.ts.map +1 -1
- package/dist/project-registration/sqlite.d.ts.map +1 -1
- package/dist/project-registration.js +156 -17
- package/dist/registry.js +86 -4
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/v1.generated.d.ts +3 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +221 -39
- package/dist/server/openapi.d.ts +13 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage.js +128 -12
- package/dist/types/index.d.ts +5 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +4 -1
|
@@ -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 ?
|
|
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.
|
|
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
|
|
14256
|
-
|
|
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.
|
|
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?.
|
|
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 ?
|
|
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
|
|
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
|
-
|
|
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 ?
|
|
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
|
|
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 ?
|
|
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.
|
|
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.
|
|
3
|
+
"packageVersion": "0.15.35",
|
|
4
4
|
"repository": "https://github.com/hasna/todos.git",
|
|
5
|
-
"gitCommit": "
|
|
6
|
-
"gitTree": "
|
|
7
|
-
"sourceTreeSha256": "
|
|
8
|
-
"generatedAt": "2026-08-
|
|
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;
|