@hasna/todos 0.11.70 → 0.11.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/plan-template-commands.d.ts.map +1 -1
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/index.js +722 -102
- package/dist/contracts.js +159 -10
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/plans.d.ts +8 -0
- package/dist/db/plans.d.ts.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +985 -439
- package/dist/lib/json-schemas.d.ts.map +1 -1
- package/dist/lib/local-bridge.d.ts.map +1 -1
- package/dist/lib/onboarding-fixtures.d.ts.map +1 -1
- package/dist/lib/plan-artifacts.d.ts +3 -0
- package/dist/lib/plan-artifacts.d.ts.map +1 -1
- package/dist/lib/shared-events.d.ts.map +1 -1
- package/dist/lib/task-route-contract.d.ts +2 -1
- package/dist/lib/task-route-contract.d.ts.map +1 -1
- package/dist/lib/task-route-sources.d.ts +68 -0
- package/dist/lib/task-route-sources.d.ts.map +1 -0
- package/dist/lib/task-routing.d.ts.map +1 -1
- package/dist/mcp/index.js +162 -10
- package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
- package/dist/registry.js +159 -10
- package/dist/release-provenance.json +3 -3
- package/dist/server/index.js +164 -12
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/storage.js +177 -12
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/registry.js
CHANGED
|
@@ -1198,6 +1198,11 @@ var init_migrations = __esm(() => {
|
|
|
1198
1198
|
CREATE INDEX IF NOT EXISTS idx_storage_tombstones_object ON storage_tombstones(object_type, object_id);
|
|
1199
1199
|
CREATE INDEX IF NOT EXISTS idx_storage_tombstones_updated ON storage_tombstones(updated_at);
|
|
1200
1200
|
INSERT OR IGNORE INTO _migrations (id) VALUES (63);
|
|
1201
|
+
`,
|
|
1202
|
+
`
|
|
1203
|
+
ALTER TABLE plans ADD COLUMN slug TEXT;
|
|
1204
|
+
CREATE INDEX IF NOT EXISTS idx_plans_slug ON plans(slug);
|
|
1205
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (64);
|
|
1201
1206
|
`
|
|
1202
1207
|
];
|
|
1203
1208
|
});
|
|
@@ -1221,6 +1226,29 @@ function runMigrations(db) {
|
|
|
1221
1226
|
}
|
|
1222
1227
|
ensureSchema(db);
|
|
1223
1228
|
}
|
|
1229
|
+
function planSlugBase(value) {
|
|
1230
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "plan";
|
|
1231
|
+
}
|
|
1232
|
+
function backfillPlanSlugs(db) {
|
|
1233
|
+
try {
|
|
1234
|
+
const rows = db.query("SELECT id, project_id, name, slug FROM plans ORDER BY created_at ASC, id ASC").all();
|
|
1235
|
+
const used = new Set;
|
|
1236
|
+
for (const row of rows) {
|
|
1237
|
+
const scope = row.project_id ?? "__global__";
|
|
1238
|
+
const base = planSlugBase(row.slug || row.name);
|
|
1239
|
+
let candidate = base;
|
|
1240
|
+
let suffix = 2;
|
|
1241
|
+
while (used.has(`${scope}:${candidate}`)) {
|
|
1242
|
+
candidate = `${base}-${suffix}`;
|
|
1243
|
+
suffix += 1;
|
|
1244
|
+
}
|
|
1245
|
+
used.add(`${scope}:${candidate}`);
|
|
1246
|
+
if (row.slug !== candidate) {
|
|
1247
|
+
db.run("UPDATE plans SET slug = ? WHERE id = ?", [candidate, row.id]);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
} catch {}
|
|
1251
|
+
}
|
|
1224
1252
|
function ensureSchema(db) {
|
|
1225
1253
|
const ensureColumn = (table, column, type) => {
|
|
1226
1254
|
try {
|
|
@@ -1270,7 +1298,8 @@ function ensureSchema(db) {
|
|
|
1270
1298
|
)`);
|
|
1271
1299
|
ensureTable("plans", `
|
|
1272
1300
|
CREATE TABLE plans (
|
|
1273
|
-
id TEXT PRIMARY KEY,
|
|
1301
|
+
id TEXT PRIMARY KEY, slug TEXT,
|
|
1302
|
+
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
1274
1303
|
task_list_id TEXT, agent_id TEXT,
|
|
1275
1304
|
name TEXT NOT NULL, description TEXT,
|
|
1276
1305
|
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'archived')),
|
|
@@ -1785,8 +1814,10 @@ function ensureSchema(db) {
|
|
|
1785
1814
|
ensureColumn("agents", "org_id", "TEXT");
|
|
1786
1815
|
ensureColumn("agents", "capabilities", "TEXT DEFAULT '[]'");
|
|
1787
1816
|
ensureColumn("projects", "org_id", "TEXT");
|
|
1817
|
+
ensureColumn("plans", "slug", "TEXT");
|
|
1788
1818
|
ensureColumn("plans", "task_list_id", "TEXT");
|
|
1789
1819
|
ensureColumn("plans", "agent_id", "TEXT");
|
|
1820
|
+
backfillPlanSlugs(db);
|
|
1790
1821
|
ensureColumn("task_templates", "variables", "TEXT DEFAULT '[]'");
|
|
1791
1822
|
ensureColumn("task_templates", "version", "INTEGER NOT NULL DEFAULT 1");
|
|
1792
1823
|
ensureColumn("template_tasks", "condition", "TEXT");
|
|
@@ -1901,6 +1932,8 @@ function ensureSchema(db) {
|
|
|
1901
1932
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name)");
|
|
1902
1933
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_id)");
|
|
1903
1934
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status)");
|
|
1935
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_slug ON plans(slug)");
|
|
1936
|
+
ensureIndex("CREATE UNIQUE INDEX IF NOT EXISTS idx_plans_scope_slug ON plans(COALESCE(project_id, ''), slug) WHERE slug IS NOT NULL");
|
|
1904
1937
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_task_list ON plans(task_list_id)");
|
|
1905
1938
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_agent ON plans(agent_id)");
|
|
1906
1939
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_task ON task_history(task_id)");
|
|
@@ -2734,6 +2767,9 @@ function clearExpiredLocks(db) {
|
|
|
2734
2767
|
const cutoff = lockExpiryCutoff();
|
|
2735
2768
|
db.run("UPDATE tasks SET locked_by = NULL, locked_at = NULL WHERE locked_at IS NOT NULL AND locked_at < ?", [cutoff]);
|
|
2736
2769
|
}
|
|
2770
|
+
function slugifyRef(value) {
|
|
2771
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
2772
|
+
}
|
|
2737
2773
|
function resolvePartialId(db, table, partialId) {
|
|
2738
2774
|
if (!ALLOWED_TABLES.has(table)) {
|
|
2739
2775
|
throw new Error(`Invalid table name: ${table}`);
|
|
@@ -2760,6 +2796,16 @@ function resolvePartialId(db, table, partialId) {
|
|
|
2760
2796
|
if (slugRow)
|
|
2761
2797
|
return slugRow.id;
|
|
2762
2798
|
}
|
|
2799
|
+
if (table === "plans") {
|
|
2800
|
+
const slug = slugifyRef(partialId);
|
|
2801
|
+
if (slug) {
|
|
2802
|
+
const slugRows = db.query("SELECT id FROM plans WHERE slug = ?").all(slug);
|
|
2803
|
+
if (slugRows.length === 1)
|
|
2804
|
+
return slugRows[0].id;
|
|
2805
|
+
if (slugRows.length > 1)
|
|
2806
|
+
return null;
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2763
2809
|
if (table === "projects") {
|
|
2764
2810
|
const nameRow = db.query("SELECT id FROM projects WHERE lower(name) = ?").get(partialId.toLowerCase());
|
|
2765
2811
|
if (nameRow)
|
|
@@ -7880,8 +7926,6 @@ function routeEnabledForTask(task, taskList) {
|
|
|
7880
7926
|
const explicit = booleanField(task.metadata.route_enabled);
|
|
7881
7927
|
if (explicit !== undefined)
|
|
7882
7928
|
return explicit;
|
|
7883
|
-
if (task.tags.includes("auto:route") || task.tags.includes("route:enabled"))
|
|
7884
|
-
return true;
|
|
7885
7929
|
const taskListDefault = taskList ? booleanField(taskList.metadata.route_enabled) : undefined;
|
|
7886
7930
|
if (taskListDefault !== undefined)
|
|
7887
7931
|
return taskListDefault;
|
|
@@ -7903,8 +7947,26 @@ function workflowPointersFromMetadata(metadata) {
|
|
|
7903
7947
|
function compactWorkflowPointers(pointers) {
|
|
7904
7948
|
return Object.fromEntries(Object.entries(pointers).filter(([, value]) => typeof value === "string" && value.length > 0));
|
|
7905
7949
|
}
|
|
7906
|
-
function
|
|
7907
|
-
|
|
7950
|
+
function metadataStringField(record, keys) {
|
|
7951
|
+
if (!record)
|
|
7952
|
+
return;
|
|
7953
|
+
for (const key of keys) {
|
|
7954
|
+
const value = record[key];
|
|
7955
|
+
if (typeof value === "string" && value.trim())
|
|
7956
|
+
return value.trim();
|
|
7957
|
+
}
|
|
7958
|
+
return;
|
|
7959
|
+
}
|
|
7960
|
+
function projectKindFromMetadata(...records) {
|
|
7961
|
+
for (const record of records) {
|
|
7962
|
+
const value = metadataStringField(record ?? undefined, ["project_kind", "projectKind", "source_kind", "sourceKind"]);
|
|
7963
|
+
if (value)
|
|
7964
|
+
return value;
|
|
7965
|
+
}
|
|
7966
|
+
return null;
|
|
7967
|
+
}
|
|
7968
|
+
function classifyProjectKind(_path, metadata) {
|
|
7969
|
+
return projectKindFromMetadata(metadata);
|
|
7908
7970
|
}
|
|
7909
7971
|
function isWorktreePath(path) {
|
|
7910
7972
|
return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
|
|
@@ -7977,7 +8039,6 @@ function taskEventMetadata(task) {
|
|
|
7977
8039
|
metadata.project_canonical_path = projectPath;
|
|
7978
8040
|
}
|
|
7979
8041
|
if (projectPath) {
|
|
7980
|
-
metadata.project_kind = classifyProjectKind(projectPath);
|
|
7981
8042
|
metadata.project_is_worktree = isWorktreePath(projectPath);
|
|
7982
8043
|
metadata.working_dir = task.working_dir ?? projectPath;
|
|
7983
8044
|
}
|
|
@@ -7989,6 +8050,10 @@ function taskEventMetadata(task) {
|
|
|
7989
8050
|
metadata.task_list_project_id = taskList.project_id;
|
|
7990
8051
|
metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
|
|
7991
8052
|
}
|
|
8053
|
+
const projectKind = projectKindFromMetadata(task.metadata, taskList?.metadata);
|
|
8054
|
+
if (projectKind) {
|
|
8055
|
+
metadata.project_kind = classifyProjectKind(projectPath ?? "", { project_kind: projectKind });
|
|
8056
|
+
}
|
|
7992
8057
|
const routeEnabled = routeEnabledForTask(task, taskList);
|
|
7993
8058
|
if (routeEnabled !== undefined) {
|
|
7994
8059
|
metadata.route_enabled = routeEnabled;
|
|
@@ -10631,15 +10696,59 @@ init_database();
|
|
|
10631
10696
|
// src/db/plans.ts
|
|
10632
10697
|
init_types();
|
|
10633
10698
|
init_database();
|
|
10699
|
+
function planSlugBase2(value) {
|
|
10700
|
+
return slugify(value) || "plan";
|
|
10701
|
+
}
|
|
10702
|
+
function normalizePlanSlug(value) {
|
|
10703
|
+
const slug = slugify(value);
|
|
10704
|
+
if (!slug)
|
|
10705
|
+
throw new Error("Invalid plan slug");
|
|
10706
|
+
return slug;
|
|
10707
|
+
}
|
|
10708
|
+
function plansBySlug(slug, db, projectId) {
|
|
10709
|
+
if (projectId !== undefined) {
|
|
10710
|
+
if (projectId === null) {
|
|
10711
|
+
return db.query("SELECT * FROM plans WHERE slug = ? AND project_id IS NULL ORDER BY created_at ASC, id ASC").all(slug);
|
|
10712
|
+
}
|
|
10713
|
+
return db.query("SELECT * FROM plans WHERE slug = ? AND project_id = ? ORDER BY created_at ASC, id ASC").all(slug, projectId);
|
|
10714
|
+
}
|
|
10715
|
+
return db.query("SELECT * FROM plans WHERE slug = ? ORDER BY created_at ASC, id ASC").all(slug);
|
|
10716
|
+
}
|
|
10717
|
+
function planSlugExists(slug, projectId, db, excludeId) {
|
|
10718
|
+
const rows = plansBySlug(slug, db, projectId);
|
|
10719
|
+
return rows.some((plan) => plan.id !== excludeId);
|
|
10720
|
+
}
|
|
10721
|
+
function nextPlanSlug(base, projectId, db, excludeId) {
|
|
10722
|
+
let candidate = base;
|
|
10723
|
+
let suffix = 2;
|
|
10724
|
+
while (planSlugExists(candidate, projectId, db, excludeId)) {
|
|
10725
|
+
candidate = `${base}-${suffix}`;
|
|
10726
|
+
suffix += 1;
|
|
10727
|
+
}
|
|
10728
|
+
return candidate;
|
|
10729
|
+
}
|
|
10730
|
+
function resolveCreateSlug(input, projectId, db) {
|
|
10731
|
+
if (input.slug !== undefined) {
|
|
10732
|
+
const slug = normalizePlanSlug(input.slug);
|
|
10733
|
+
if (planSlugExists(slug, projectId, db)) {
|
|
10734
|
+
throw new Error(`Plan slug already exists in this scope: ${slug}`);
|
|
10735
|
+
}
|
|
10736
|
+
return slug;
|
|
10737
|
+
}
|
|
10738
|
+
return nextPlanSlug(planSlugBase2(input.name), projectId, db);
|
|
10739
|
+
}
|
|
10634
10740
|
function createPlan(input, db) {
|
|
10635
10741
|
const d = db || getDatabase();
|
|
10636
10742
|
const id = uuid();
|
|
10637
10743
|
const timestamp = now();
|
|
10744
|
+
const projectId = input.project_id || null;
|
|
10745
|
+
const slug = resolveCreateSlug(input, projectId, d);
|
|
10638
10746
|
const machineId = currentStorageMachineId(d);
|
|
10639
|
-
d.run(`INSERT INTO plans (id, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at, machine_id)
|
|
10640
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
10747
|
+
d.run(`INSERT INTO plans (id, slug, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at, machine_id)
|
|
10748
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
10641
10749
|
id,
|
|
10642
|
-
|
|
10750
|
+
slug,
|
|
10751
|
+
projectId,
|
|
10643
10752
|
input.task_list_id || null,
|
|
10644
10753
|
input.agent_id || null,
|
|
10645
10754
|
input.name,
|
|
@@ -10674,6 +10783,14 @@ function updatePlan(id, input, db) {
|
|
|
10674
10783
|
sets.push("name = ?");
|
|
10675
10784
|
params.push(input.name);
|
|
10676
10785
|
}
|
|
10786
|
+
if (input.slug !== undefined) {
|
|
10787
|
+
const slug = normalizePlanSlug(input.slug);
|
|
10788
|
+
if (planSlugExists(slug, plan.project_id, d, id)) {
|
|
10789
|
+
throw new Error(`Plan slug already exists in this scope: ${slug}`);
|
|
10790
|
+
}
|
|
10791
|
+
sets.push("slug = ?");
|
|
10792
|
+
params.push(slug);
|
|
10793
|
+
}
|
|
10677
10794
|
if (input.description !== undefined) {
|
|
10678
10795
|
sets.push("description = ?");
|
|
10679
10796
|
params.push(input.description);
|
|
@@ -12527,7 +12644,7 @@ var dataKeys = [
|
|
|
12527
12644
|
var insertColumns = {
|
|
12528
12645
|
projects: ["id", "name", "path", "description", "task_list_id", "task_prefix", "task_counter", "created_at", "updated_at", "machine_id", "synced_at"],
|
|
12529
12646
|
task_lists: ["id", "project_id", "slug", "name", "description", "metadata", "created_at", "updated_at", "machine_id", "synced_at"],
|
|
12530
|
-
plans: ["id", "project_id", "task_list_id", "agent_id", "name", "description", "status", "created_at", "updated_at", "machine_id", "synced_at"],
|
|
12647
|
+
plans: ["id", "slug", "project_id", "task_list_id", "agent_id", "name", "description", "status", "created_at", "updated_at", "machine_id", "synced_at"],
|
|
12531
12648
|
tasks: [
|
|
12532
12649
|
"id",
|
|
12533
12650
|
"short_id",
|
|
@@ -12819,6 +12936,36 @@ function prepareValue(column, value) {
|
|
|
12819
12936
|
return JSON.stringify(value ?? (column === "tags" || column === "files_changed" ? [] : {}));
|
|
12820
12937
|
return value === undefined ? null : value;
|
|
12821
12938
|
}
|
|
12939
|
+
function slugifyPlanValue(value) {
|
|
12940
|
+
return typeof value === "string" ? value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") : "";
|
|
12941
|
+
}
|
|
12942
|
+
function planSlugBase3(plan) {
|
|
12943
|
+
return slugifyPlanValue(plan.slug) || slugifyPlanValue(plan.name) || "plan";
|
|
12944
|
+
}
|
|
12945
|
+
function planSlugScope(projectId) {
|
|
12946
|
+
return typeof projectId === "string" && projectId ? projectId : "__global__";
|
|
12947
|
+
}
|
|
12948
|
+
function planSlugKey(projectId, slug) {
|
|
12949
|
+
return `${planSlugScope(projectId)}:${slug}`;
|
|
12950
|
+
}
|
|
12951
|
+
function normalizeBridgePlanSlugs(plans, db) {
|
|
12952
|
+
const existingRows = db.query("SELECT id, project_id, slug FROM plans WHERE slug IS NOT NULL").all();
|
|
12953
|
+
const existingIds = new Set(existingRows.map((row) => row.id));
|
|
12954
|
+
const used = new Set(existingRows.filter((row) => row.slug).map((row) => planSlugKey(row.project_id, row.slug)));
|
|
12955
|
+
return plans.map((plan) => {
|
|
12956
|
+
if (existingIds.has(plan.id))
|
|
12957
|
+
return plan;
|
|
12958
|
+
const base = planSlugBase3(plan);
|
|
12959
|
+
let candidate = base;
|
|
12960
|
+
let suffix = 2;
|
|
12961
|
+
while (used.has(planSlugKey(plan.project_id, candidate))) {
|
|
12962
|
+
candidate = `${base}-${suffix}`;
|
|
12963
|
+
suffix += 1;
|
|
12964
|
+
}
|
|
12965
|
+
used.add(planSlugKey(plan.project_id, candidate));
|
|
12966
|
+
return { ...plan, slug: candidate };
|
|
12967
|
+
});
|
|
12968
|
+
}
|
|
12822
12969
|
function insertRecord(db, tableKey, row) {
|
|
12823
12970
|
const table = tableByKey[tableKey];
|
|
12824
12971
|
const columns = insertColumns[tableKey];
|
|
@@ -12955,6 +13102,7 @@ function importLocalBridgeBundle(bundle, options = {}, db) {
|
|
|
12955
13102
|
const conflictStrategy = options.conflictStrategy ?? "skip";
|
|
12956
13103
|
const data = {
|
|
12957
13104
|
...bundle.data,
|
|
13105
|
+
plans: normalizeBridgePlanSlugs(bundle.data.plans, d),
|
|
12958
13106
|
tasks: sortedTasks(bundle.data.tasks),
|
|
12959
13107
|
saved_views: bundle.data.saved_views ?? [],
|
|
12960
13108
|
task_boards: bundle.data.task_boards ?? [],
|
|
@@ -13148,6 +13296,7 @@ function createAgentProjectDemoBundle() {
|
|
|
13148
13296
|
});
|
|
13149
13297
|
data.plans.push({
|
|
13150
13298
|
id: ids.plan,
|
|
13299
|
+
slug: "ship-local-demo-workflow",
|
|
13151
13300
|
project_id: ids.project,
|
|
13152
13301
|
task_list_id: ids.list,
|
|
13153
13302
|
agent_id: "demo-agent",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"packageName": "@hasna/todos",
|
|
3
|
-
"packageVersion": "0.11.
|
|
3
|
+
"packageVersion": "0.11.72",
|
|
4
4
|
"repository": "https://github.com/hasna/todos.git",
|
|
5
|
-
"gitCommit": "
|
|
6
|
-
"generatedAt": "2026-
|
|
5
|
+
"gitCommit": "6faf721538ddfdcd384e253bee48821966186d85",
|
|
6
|
+
"generatedAt": "2026-07-03T13:05:19.129Z"
|
|
7
7
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -1248,6 +1248,11 @@ var init_migrations = __esm(() => {
|
|
|
1248
1248
|
CREATE INDEX IF NOT EXISTS idx_storage_tombstones_object ON storage_tombstones(object_type, object_id);
|
|
1249
1249
|
CREATE INDEX IF NOT EXISTS idx_storage_tombstones_updated ON storage_tombstones(updated_at);
|
|
1250
1250
|
INSERT OR IGNORE INTO _migrations (id) VALUES (63);
|
|
1251
|
+
`,
|
|
1252
|
+
`
|
|
1253
|
+
ALTER TABLE plans ADD COLUMN slug TEXT;
|
|
1254
|
+
CREATE INDEX IF NOT EXISTS idx_plans_slug ON plans(slug);
|
|
1255
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (64);
|
|
1251
1256
|
`
|
|
1252
1257
|
];
|
|
1253
1258
|
});
|
|
@@ -1271,6 +1276,29 @@ function runMigrations(db) {
|
|
|
1271
1276
|
}
|
|
1272
1277
|
ensureSchema(db);
|
|
1273
1278
|
}
|
|
1279
|
+
function planSlugBase(value) {
|
|
1280
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "plan";
|
|
1281
|
+
}
|
|
1282
|
+
function backfillPlanSlugs(db) {
|
|
1283
|
+
try {
|
|
1284
|
+
const rows = db.query("SELECT id, project_id, name, slug FROM plans ORDER BY created_at ASC, id ASC").all();
|
|
1285
|
+
const used = new Set;
|
|
1286
|
+
for (const row of rows) {
|
|
1287
|
+
const scope = row.project_id ?? "__global__";
|
|
1288
|
+
const base = planSlugBase(row.slug || row.name);
|
|
1289
|
+
let candidate = base;
|
|
1290
|
+
let suffix = 2;
|
|
1291
|
+
while (used.has(`${scope}:${candidate}`)) {
|
|
1292
|
+
candidate = `${base}-${suffix}`;
|
|
1293
|
+
suffix += 1;
|
|
1294
|
+
}
|
|
1295
|
+
used.add(`${scope}:${candidate}`);
|
|
1296
|
+
if (row.slug !== candidate) {
|
|
1297
|
+
db.run("UPDATE plans SET slug = ? WHERE id = ?", [candidate, row.id]);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
} catch {}
|
|
1301
|
+
}
|
|
1274
1302
|
function ensureSchema(db) {
|
|
1275
1303
|
const ensureColumn = (table, column, type) => {
|
|
1276
1304
|
try {
|
|
@@ -1320,7 +1348,8 @@ function ensureSchema(db) {
|
|
|
1320
1348
|
)`);
|
|
1321
1349
|
ensureTable("plans", `
|
|
1322
1350
|
CREATE TABLE plans (
|
|
1323
|
-
id TEXT PRIMARY KEY,
|
|
1351
|
+
id TEXT PRIMARY KEY, slug TEXT,
|
|
1352
|
+
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
1324
1353
|
task_list_id TEXT, agent_id TEXT,
|
|
1325
1354
|
name TEXT NOT NULL, description TEXT,
|
|
1326
1355
|
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'archived')),
|
|
@@ -1835,8 +1864,10 @@ function ensureSchema(db) {
|
|
|
1835
1864
|
ensureColumn("agents", "org_id", "TEXT");
|
|
1836
1865
|
ensureColumn("agents", "capabilities", "TEXT DEFAULT '[]'");
|
|
1837
1866
|
ensureColumn("projects", "org_id", "TEXT");
|
|
1867
|
+
ensureColumn("plans", "slug", "TEXT");
|
|
1838
1868
|
ensureColumn("plans", "task_list_id", "TEXT");
|
|
1839
1869
|
ensureColumn("plans", "agent_id", "TEXT");
|
|
1870
|
+
backfillPlanSlugs(db);
|
|
1840
1871
|
ensureColumn("task_templates", "variables", "TEXT DEFAULT '[]'");
|
|
1841
1872
|
ensureColumn("task_templates", "version", "INTEGER NOT NULL DEFAULT 1");
|
|
1842
1873
|
ensureColumn("template_tasks", "condition", "TEXT");
|
|
@@ -1951,6 +1982,8 @@ function ensureSchema(db) {
|
|
|
1951
1982
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name)");
|
|
1952
1983
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_id)");
|
|
1953
1984
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status)");
|
|
1985
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_slug ON plans(slug)");
|
|
1986
|
+
ensureIndex("CREATE UNIQUE INDEX IF NOT EXISTS idx_plans_scope_slug ON plans(COALESCE(project_id, ''), slug) WHERE slug IS NOT NULL");
|
|
1954
1987
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_task_list ON plans(task_list_id)");
|
|
1955
1988
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_agent ON plans(agent_id)");
|
|
1956
1989
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_task ON task_history(task_id)");
|
|
@@ -2799,6 +2832,9 @@ function clearExpiredLocks(db) {
|
|
|
2799
2832
|
const cutoff = lockExpiryCutoff();
|
|
2800
2833
|
db.run("UPDATE tasks SET locked_by = NULL, locked_at = NULL WHERE locked_at IS NOT NULL AND locked_at < ?", [cutoff]);
|
|
2801
2834
|
}
|
|
2835
|
+
function slugifyRef(value) {
|
|
2836
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
2837
|
+
}
|
|
2802
2838
|
function resolvePartialId(db, table, partialId) {
|
|
2803
2839
|
if (!ALLOWED_TABLES.has(table)) {
|
|
2804
2840
|
throw new Error(`Invalid table name: ${table}`);
|
|
@@ -2825,6 +2861,16 @@ function resolvePartialId(db, table, partialId) {
|
|
|
2825
2861
|
if (slugRow)
|
|
2826
2862
|
return slugRow.id;
|
|
2827
2863
|
}
|
|
2864
|
+
if (table === "plans") {
|
|
2865
|
+
const slug = slugifyRef(partialId);
|
|
2866
|
+
if (slug) {
|
|
2867
|
+
const slugRows = db.query("SELECT id FROM plans WHERE slug = ?").all(slug);
|
|
2868
|
+
if (slugRows.length === 1)
|
|
2869
|
+
return slugRows[0].id;
|
|
2870
|
+
if (slugRows.length > 1)
|
|
2871
|
+
return null;
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2828
2874
|
if (table === "projects") {
|
|
2829
2875
|
const nameRow = db.query("SELECT id FROM projects WHERE lower(name) = ?").get(partialId.toLowerCase());
|
|
2830
2876
|
if (nameRow)
|
|
@@ -5014,8 +5060,6 @@ function routeEnabledForTask(task, taskList) {
|
|
|
5014
5060
|
const explicit = booleanField(task.metadata.route_enabled);
|
|
5015
5061
|
if (explicit !== undefined)
|
|
5016
5062
|
return explicit;
|
|
5017
|
-
if (task.tags.includes("auto:route") || task.tags.includes("route:enabled"))
|
|
5018
|
-
return true;
|
|
5019
5063
|
const taskListDefault = taskList ? booleanField(taskList.metadata.route_enabled) : undefined;
|
|
5020
5064
|
if (taskListDefault !== undefined)
|
|
5021
5065
|
return taskListDefault;
|
|
@@ -5034,8 +5078,26 @@ function workflowPointersFromMetadata(metadata) {
|
|
|
5034
5078
|
workflow_state: stringField(metadata.workflow_state) ?? stringField(nested.workflow_state) ?? stringField(nested.state)
|
|
5035
5079
|
};
|
|
5036
5080
|
}
|
|
5037
|
-
function
|
|
5038
|
-
|
|
5081
|
+
function metadataStringField(record, keys) {
|
|
5082
|
+
if (!record)
|
|
5083
|
+
return;
|
|
5084
|
+
for (const key of keys) {
|
|
5085
|
+
const value = record[key];
|
|
5086
|
+
if (typeof value === "string" && value.trim())
|
|
5087
|
+
return value.trim();
|
|
5088
|
+
}
|
|
5089
|
+
return;
|
|
5090
|
+
}
|
|
5091
|
+
function projectKindFromMetadata(...records) {
|
|
5092
|
+
for (const record of records) {
|
|
5093
|
+
const value = metadataStringField(record ?? undefined, ["project_kind", "projectKind", "source_kind", "sourceKind"]);
|
|
5094
|
+
if (value)
|
|
5095
|
+
return value;
|
|
5096
|
+
}
|
|
5097
|
+
return null;
|
|
5098
|
+
}
|
|
5099
|
+
function classifyProjectKind(_path, metadata) {
|
|
5100
|
+
return projectKindFromMetadata(metadata);
|
|
5039
5101
|
}
|
|
5040
5102
|
function isWorktreePath(path) {
|
|
5041
5103
|
return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
|
|
@@ -5108,7 +5170,6 @@ function taskEventMetadata(task) {
|
|
|
5108
5170
|
metadata.project_canonical_path = projectPath;
|
|
5109
5171
|
}
|
|
5110
5172
|
if (projectPath) {
|
|
5111
|
-
metadata.project_kind = classifyProjectKind(projectPath);
|
|
5112
5173
|
metadata.project_is_worktree = isWorktreePath(projectPath);
|
|
5113
5174
|
metadata.working_dir = task.working_dir ?? projectPath;
|
|
5114
5175
|
}
|
|
@@ -5120,6 +5181,10 @@ function taskEventMetadata(task) {
|
|
|
5120
5181
|
metadata.task_list_project_id = taskList.project_id;
|
|
5121
5182
|
metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
|
|
5122
5183
|
}
|
|
5184
|
+
const projectKind = projectKindFromMetadata(task.metadata, taskList?.metadata);
|
|
5185
|
+
if (projectKind) {
|
|
5186
|
+
metadata.project_kind = classifyProjectKind(projectPath ?? "", { project_kind: projectKind });
|
|
5187
|
+
}
|
|
5123
5188
|
const routeEnabled = routeEnabledForTask(task, taskList);
|
|
5124
5189
|
if (routeEnabled !== undefined) {
|
|
5125
5190
|
metadata.route_enabled = routeEnabled;
|
|
@@ -8142,15 +8207,59 @@ var init_task_relations = __esm(() => {
|
|
|
8142
8207
|
});
|
|
8143
8208
|
|
|
8144
8209
|
// src/db/plans.ts
|
|
8210
|
+
function planSlugBase2(value) {
|
|
8211
|
+
return slugify(value) || "plan";
|
|
8212
|
+
}
|
|
8213
|
+
function normalizePlanSlug(value) {
|
|
8214
|
+
const slug = slugify(value);
|
|
8215
|
+
if (!slug)
|
|
8216
|
+
throw new Error("Invalid plan slug");
|
|
8217
|
+
return slug;
|
|
8218
|
+
}
|
|
8219
|
+
function plansBySlug(slug, db, projectId) {
|
|
8220
|
+
if (projectId !== undefined) {
|
|
8221
|
+
if (projectId === null) {
|
|
8222
|
+
return db.query("SELECT * FROM plans WHERE slug = ? AND project_id IS NULL ORDER BY created_at ASC, id ASC").all(slug);
|
|
8223
|
+
}
|
|
8224
|
+
return db.query("SELECT * FROM plans WHERE slug = ? AND project_id = ? ORDER BY created_at ASC, id ASC").all(slug, projectId);
|
|
8225
|
+
}
|
|
8226
|
+
return db.query("SELECT * FROM plans WHERE slug = ? ORDER BY created_at ASC, id ASC").all(slug);
|
|
8227
|
+
}
|
|
8228
|
+
function planSlugExists(slug, projectId, db, excludeId) {
|
|
8229
|
+
const rows = plansBySlug(slug, db, projectId);
|
|
8230
|
+
return rows.some((plan) => plan.id !== excludeId);
|
|
8231
|
+
}
|
|
8232
|
+
function nextPlanSlug(base, projectId, db, excludeId) {
|
|
8233
|
+
let candidate = base;
|
|
8234
|
+
let suffix = 2;
|
|
8235
|
+
while (planSlugExists(candidate, projectId, db, excludeId)) {
|
|
8236
|
+
candidate = `${base}-${suffix}`;
|
|
8237
|
+
suffix += 1;
|
|
8238
|
+
}
|
|
8239
|
+
return candidate;
|
|
8240
|
+
}
|
|
8241
|
+
function resolveCreateSlug(input, projectId, db) {
|
|
8242
|
+
if (input.slug !== undefined) {
|
|
8243
|
+
const slug = normalizePlanSlug(input.slug);
|
|
8244
|
+
if (planSlugExists(slug, projectId, db)) {
|
|
8245
|
+
throw new Error(`Plan slug already exists in this scope: ${slug}`);
|
|
8246
|
+
}
|
|
8247
|
+
return slug;
|
|
8248
|
+
}
|
|
8249
|
+
return nextPlanSlug(planSlugBase2(input.name), projectId, db);
|
|
8250
|
+
}
|
|
8145
8251
|
function createPlan(input, db) {
|
|
8146
8252
|
const d = db || getDatabase();
|
|
8147
8253
|
const id = uuid();
|
|
8148
8254
|
const timestamp = now();
|
|
8255
|
+
const projectId = input.project_id || null;
|
|
8256
|
+
const slug = resolveCreateSlug(input, projectId, d);
|
|
8149
8257
|
const machineId = currentStorageMachineId(d);
|
|
8150
|
-
d.run(`INSERT INTO plans (id, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at, machine_id)
|
|
8151
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
8258
|
+
d.run(`INSERT INTO plans (id, slug, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at, machine_id)
|
|
8259
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
8152
8260
|
id,
|
|
8153
|
-
|
|
8261
|
+
slug,
|
|
8262
|
+
projectId,
|
|
8154
8263
|
input.task_list_id || null,
|
|
8155
8264
|
input.agent_id || null,
|
|
8156
8265
|
input.name,
|
|
@@ -8185,6 +8294,14 @@ function updatePlan(id, input, db) {
|
|
|
8185
8294
|
sets.push("name = ?");
|
|
8186
8295
|
params.push(input.name);
|
|
8187
8296
|
}
|
|
8297
|
+
if (input.slug !== undefined) {
|
|
8298
|
+
const slug = normalizePlanSlug(input.slug);
|
|
8299
|
+
if (planSlugExists(slug, plan.project_id, d, id)) {
|
|
8300
|
+
throw new Error(`Plan slug already exists in this scope: ${slug}`);
|
|
8301
|
+
}
|
|
8302
|
+
sets.push("slug = ?");
|
|
8303
|
+
params.push(slug);
|
|
8304
|
+
}
|
|
8188
8305
|
if (input.description !== undefined) {
|
|
8189
8306
|
sets.push("description = ?");
|
|
8190
8307
|
params.push(input.description);
|
|
@@ -8229,6 +8346,7 @@ var init_plans = __esm(() => {
|
|
|
8229
8346
|
init_event_emission_safety();
|
|
8230
8347
|
init_event_hooks();
|
|
8231
8348
|
init_database();
|
|
8349
|
+
init_projects();
|
|
8232
8350
|
init_storage_tombstones();
|
|
8233
8351
|
});
|
|
8234
8352
|
|
|
@@ -12111,6 +12229,7 @@ async function handleCreatePlan(req, _ctx, json2) {
|
|
|
12111
12229
|
return json2({ error: "Missing 'name'" }, 400);
|
|
12112
12230
|
const plan = createPlan({
|
|
12113
12231
|
name: body.name,
|
|
12232
|
+
slug: body.slug,
|
|
12114
12233
|
description: body.description,
|
|
12115
12234
|
project_id: body.project_id,
|
|
12116
12235
|
task_list_id: body.task_list_id,
|
|
@@ -15565,7 +15684,7 @@ class JSONSchemaGenerator {
|
|
|
15565
15684
|
if (val === undefined) {
|
|
15566
15685
|
if (this.unrepresentable === "throw") {
|
|
15567
15686
|
throw new Error("Literal `undefined` cannot be represented in JSON Schema");
|
|
15568
|
-
}
|
|
15687
|
+
} else {}
|
|
15569
15688
|
} else if (typeof val === "bigint") {
|
|
15570
15689
|
if (this.unrepresentable === "throw") {
|
|
15571
15690
|
throw new Error("BigInt literals cannot be represented in JSON Schema");
|
|
@@ -40130,6 +40249,7 @@ Tasks:` : null,
|
|
|
40130
40249
|
if (shouldRegisterTool("create_plan")) {
|
|
40131
40250
|
server.tool("create_plan", "Create a new plan (sprint/milestone).", {
|
|
40132
40251
|
name: exports_external.string().describe("Plan name"),
|
|
40252
|
+
slug: exports_external.string().optional().describe("Readable plan slug"),
|
|
40133
40253
|
project_id: exports_external.string().optional().describe("Project ID"),
|
|
40134
40254
|
description: exports_external.string().optional(),
|
|
40135
40255
|
start_date: exports_external.string().optional().describe("ISO date"),
|
|
@@ -58652,7 +58772,7 @@ var require_to_json_schema = __commonJS((exports) => {
|
|
|
58652
58772
|
if (val === undefined) {
|
|
58653
58773
|
if (this.unrepresentable === "throw") {
|
|
58654
58774
|
throw new Error("Literal `undefined` cannot be represented in JSON Schema");
|
|
58655
|
-
}
|
|
58775
|
+
} else {}
|
|
58656
58776
|
} else if (typeof val === "bigint") {
|
|
58657
58777
|
if (this.unrepresentable === "throw") {
|
|
58658
58778
|
throw new Error("BigInt literals cannot be represented in JSON Schema");
|
|
@@ -70547,6 +70667,36 @@ function prepareValue(column, value) {
|
|
|
70547
70667
|
return JSON.stringify(value ?? (column === "tags" || column === "files_changed" ? [] : {}));
|
|
70548
70668
|
return value === undefined ? null : value;
|
|
70549
70669
|
}
|
|
70670
|
+
function slugifyPlanValue(value) {
|
|
70671
|
+
return typeof value === "string" ? value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") : "";
|
|
70672
|
+
}
|
|
70673
|
+
function planSlugBase3(plan) {
|
|
70674
|
+
return slugifyPlanValue(plan.slug) || slugifyPlanValue(plan.name) || "plan";
|
|
70675
|
+
}
|
|
70676
|
+
function planSlugScope(projectId) {
|
|
70677
|
+
return typeof projectId === "string" && projectId ? projectId : "__global__";
|
|
70678
|
+
}
|
|
70679
|
+
function planSlugKey(projectId, slug) {
|
|
70680
|
+
return `${planSlugScope(projectId)}:${slug}`;
|
|
70681
|
+
}
|
|
70682
|
+
function normalizeBridgePlanSlugs(plans, db) {
|
|
70683
|
+
const existingRows = db.query("SELECT id, project_id, slug FROM plans WHERE slug IS NOT NULL").all();
|
|
70684
|
+
const existingIds = new Set(existingRows.map((row) => row.id));
|
|
70685
|
+
const used = new Set(existingRows.filter((row) => row.slug).map((row) => planSlugKey(row.project_id, row.slug)));
|
|
70686
|
+
return plans.map((plan) => {
|
|
70687
|
+
if (existingIds.has(plan.id))
|
|
70688
|
+
return plan;
|
|
70689
|
+
const base = planSlugBase3(plan);
|
|
70690
|
+
let candidate = base;
|
|
70691
|
+
let suffix = 2;
|
|
70692
|
+
while (used.has(planSlugKey(plan.project_id, candidate))) {
|
|
70693
|
+
candidate = `${base}-${suffix}`;
|
|
70694
|
+
suffix += 1;
|
|
70695
|
+
}
|
|
70696
|
+
used.add(planSlugKey(plan.project_id, candidate));
|
|
70697
|
+
return { ...plan, slug: candidate };
|
|
70698
|
+
});
|
|
70699
|
+
}
|
|
70550
70700
|
function insertRecord(db, tableKey, row) {
|
|
70551
70701
|
const table = tableByKey[tableKey];
|
|
70552
70702
|
const columns = insertColumns[tableKey];
|
|
@@ -70683,6 +70833,7 @@ function importLocalBridgeBundle(bundle, options = {}, db) {
|
|
|
70683
70833
|
const conflictStrategy = options.conflictStrategy ?? "skip";
|
|
70684
70834
|
const data = {
|
|
70685
70835
|
...bundle.data,
|
|
70836
|
+
plans: normalizeBridgePlanSlugs(bundle.data.plans, d),
|
|
70686
70837
|
tasks: sortedTasks(bundle.data.tasks),
|
|
70687
70838
|
saved_views: bundle.data.saved_views ?? [],
|
|
70688
70839
|
task_boards: bundle.data.task_boards ?? [],
|
|
@@ -70767,7 +70918,7 @@ var init_local_bridge = __esm(() => {
|
|
|
70767
70918
|
insertColumns = {
|
|
70768
70919
|
projects: ["id", "name", "path", "description", "task_list_id", "task_prefix", "task_counter", "created_at", "updated_at", "machine_id", "synced_at"],
|
|
70769
70920
|
task_lists: ["id", "project_id", "slug", "name", "description", "metadata", "created_at", "updated_at", "machine_id", "synced_at"],
|
|
70770
|
-
plans: ["id", "project_id", "task_list_id", "agent_id", "name", "description", "status", "created_at", "updated_at", "machine_id", "synced_at"],
|
|
70921
|
+
plans: ["id", "slug", "project_id", "task_list_id", "agent_id", "name", "description", "status", "created_at", "updated_at", "machine_id", "synced_at"],
|
|
70771
70922
|
tasks: [
|
|
70772
70923
|
"id",
|
|
70773
70924
|
"short_id",
|
|
@@ -71269,6 +71420,7 @@ function createAgentProjectDemoBundle() {
|
|
|
71269
71420
|
});
|
|
71270
71421
|
data.plans.push({
|
|
71271
71422
|
id: ids.plan,
|
|
71423
|
+
slug: "ship-local-demo-workflow",
|
|
71272
71424
|
project_id: ids.project,
|
|
71273
71425
|
task_list_id: ids.list,
|
|
71274
71426
|
agent_id: "demo-agent",
|