@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/cli/index.js
CHANGED
|
@@ -1011,7 +1011,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1011
1011
|
this._exitCallback = (err) => {
|
|
1012
1012
|
if (err.code !== "commander.executeSubCommandAsync") {
|
|
1013
1013
|
throw err;
|
|
1014
|
-
}
|
|
1014
|
+
} else {}
|
|
1015
1015
|
};
|
|
1016
1016
|
}
|
|
1017
1017
|
return this;
|
|
@@ -3282,6 +3282,11 @@ var init_migrations = __esm(() => {
|
|
|
3282
3282
|
CREATE INDEX IF NOT EXISTS idx_storage_tombstones_object ON storage_tombstones(object_type, object_id);
|
|
3283
3283
|
CREATE INDEX IF NOT EXISTS idx_storage_tombstones_updated ON storage_tombstones(updated_at);
|
|
3284
3284
|
INSERT OR IGNORE INTO _migrations (id) VALUES (63);
|
|
3285
|
+
`,
|
|
3286
|
+
`
|
|
3287
|
+
ALTER TABLE plans ADD COLUMN slug TEXT;
|
|
3288
|
+
CREATE INDEX IF NOT EXISTS idx_plans_slug ON plans(slug);
|
|
3289
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (64);
|
|
3285
3290
|
`
|
|
3286
3291
|
];
|
|
3287
3292
|
});
|
|
@@ -3305,6 +3310,29 @@ function runMigrations(db) {
|
|
|
3305
3310
|
}
|
|
3306
3311
|
ensureSchema(db);
|
|
3307
3312
|
}
|
|
3313
|
+
function planSlugBase(value) {
|
|
3314
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "plan";
|
|
3315
|
+
}
|
|
3316
|
+
function backfillPlanSlugs(db) {
|
|
3317
|
+
try {
|
|
3318
|
+
const rows = db.query("SELECT id, project_id, name, slug FROM plans ORDER BY created_at ASC, id ASC").all();
|
|
3319
|
+
const used = new Set;
|
|
3320
|
+
for (const row of rows) {
|
|
3321
|
+
const scope = row.project_id ?? "__global__";
|
|
3322
|
+
const base = planSlugBase(row.slug || row.name);
|
|
3323
|
+
let candidate = base;
|
|
3324
|
+
let suffix = 2;
|
|
3325
|
+
while (used.has(`${scope}:${candidate}`)) {
|
|
3326
|
+
candidate = `${base}-${suffix}`;
|
|
3327
|
+
suffix += 1;
|
|
3328
|
+
}
|
|
3329
|
+
used.add(`${scope}:${candidate}`);
|
|
3330
|
+
if (row.slug !== candidate) {
|
|
3331
|
+
db.run("UPDATE plans SET slug = ? WHERE id = ?", [candidate, row.id]);
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
} catch {}
|
|
3335
|
+
}
|
|
3308
3336
|
function ensureSchema(db) {
|
|
3309
3337
|
const ensureColumn = (table, column, type) => {
|
|
3310
3338
|
try {
|
|
@@ -3354,7 +3382,8 @@ function ensureSchema(db) {
|
|
|
3354
3382
|
)`);
|
|
3355
3383
|
ensureTable("plans", `
|
|
3356
3384
|
CREATE TABLE plans (
|
|
3357
|
-
id TEXT PRIMARY KEY,
|
|
3385
|
+
id TEXT PRIMARY KEY, slug TEXT,
|
|
3386
|
+
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
3358
3387
|
task_list_id TEXT, agent_id TEXT,
|
|
3359
3388
|
name TEXT NOT NULL, description TEXT,
|
|
3360
3389
|
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'archived')),
|
|
@@ -3869,8 +3898,10 @@ function ensureSchema(db) {
|
|
|
3869
3898
|
ensureColumn("agents", "org_id", "TEXT");
|
|
3870
3899
|
ensureColumn("agents", "capabilities", "TEXT DEFAULT '[]'");
|
|
3871
3900
|
ensureColumn("projects", "org_id", "TEXT");
|
|
3901
|
+
ensureColumn("plans", "slug", "TEXT");
|
|
3872
3902
|
ensureColumn("plans", "task_list_id", "TEXT");
|
|
3873
3903
|
ensureColumn("plans", "agent_id", "TEXT");
|
|
3904
|
+
backfillPlanSlugs(db);
|
|
3874
3905
|
ensureColumn("task_templates", "variables", "TEXT DEFAULT '[]'");
|
|
3875
3906
|
ensureColumn("task_templates", "version", "INTEGER NOT NULL DEFAULT 1");
|
|
3876
3907
|
ensureColumn("template_tasks", "condition", "TEXT");
|
|
@@ -3985,6 +4016,8 @@ function ensureSchema(db) {
|
|
|
3985
4016
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name)");
|
|
3986
4017
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_id)");
|
|
3987
4018
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status)");
|
|
4019
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_slug ON plans(slug)");
|
|
4020
|
+
ensureIndex("CREATE UNIQUE INDEX IF NOT EXISTS idx_plans_scope_slug ON plans(COALESCE(project_id, ''), slug) WHERE slug IS NOT NULL");
|
|
3988
4021
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_task_list ON plans(task_list_id)");
|
|
3989
4022
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_agent ON plans(agent_id)");
|
|
3990
4023
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_task ON task_history(task_id)");
|
|
@@ -4838,6 +4871,9 @@ function clearExpiredLocks(db) {
|
|
|
4838
4871
|
const cutoff = lockExpiryCutoff();
|
|
4839
4872
|
db.run("UPDATE tasks SET locked_by = NULL, locked_at = NULL WHERE locked_at IS NOT NULL AND locked_at < ?", [cutoff]);
|
|
4840
4873
|
}
|
|
4874
|
+
function slugifyRef(value) {
|
|
4875
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
4876
|
+
}
|
|
4841
4877
|
function resolvePartialId(db, table, partialId) {
|
|
4842
4878
|
if (!ALLOWED_TABLES.has(table)) {
|
|
4843
4879
|
throw new Error(`Invalid table name: ${table}`);
|
|
@@ -4864,6 +4900,16 @@ function resolvePartialId(db, table, partialId) {
|
|
|
4864
4900
|
if (slugRow)
|
|
4865
4901
|
return slugRow.id;
|
|
4866
4902
|
}
|
|
4903
|
+
if (table === "plans") {
|
|
4904
|
+
const slug = slugifyRef(partialId);
|
|
4905
|
+
if (slug) {
|
|
4906
|
+
const slugRows = db.query("SELECT id FROM plans WHERE slug = ?").all(slug);
|
|
4907
|
+
if (slugRows.length === 1)
|
|
4908
|
+
return slugRows[0].id;
|
|
4909
|
+
if (slugRows.length > 1)
|
|
4910
|
+
return null;
|
|
4911
|
+
}
|
|
4912
|
+
}
|
|
4867
4913
|
if (table === "projects") {
|
|
4868
4914
|
const nameRow = db.query("SELECT id FROM projects WHERE lower(name) = ?").get(partialId.toLowerCase());
|
|
4869
4915
|
if (nameRow)
|
|
@@ -7169,8 +7215,6 @@ function routeEnabledForTask(task, taskList) {
|
|
|
7169
7215
|
const explicit = booleanField(task.metadata.route_enabled);
|
|
7170
7216
|
if (explicit !== undefined)
|
|
7171
7217
|
return explicit;
|
|
7172
|
-
if (task.tags.includes("auto:route") || task.tags.includes("route:enabled"))
|
|
7173
|
-
return true;
|
|
7174
7218
|
const taskListDefault = taskList ? booleanField(taskList.metadata.route_enabled) : undefined;
|
|
7175
7219
|
if (taskListDefault !== undefined)
|
|
7176
7220
|
return taskListDefault;
|
|
@@ -7192,8 +7236,26 @@ function workflowPointersFromMetadata(metadata) {
|
|
|
7192
7236
|
function compactWorkflowPointers(pointers) {
|
|
7193
7237
|
return Object.fromEntries(Object.entries(pointers).filter(([, value]) => typeof value === "string" && value.length > 0));
|
|
7194
7238
|
}
|
|
7195
|
-
function
|
|
7196
|
-
|
|
7239
|
+
function metadataStringField(record, keys) {
|
|
7240
|
+
if (!record)
|
|
7241
|
+
return;
|
|
7242
|
+
for (const key of keys) {
|
|
7243
|
+
const value = record[key];
|
|
7244
|
+
if (typeof value === "string" && value.trim())
|
|
7245
|
+
return value.trim();
|
|
7246
|
+
}
|
|
7247
|
+
return;
|
|
7248
|
+
}
|
|
7249
|
+
function projectKindFromMetadata(...records) {
|
|
7250
|
+
for (const record of records) {
|
|
7251
|
+
const value = metadataStringField(record ?? undefined, ["project_kind", "projectKind", "source_kind", "sourceKind"]);
|
|
7252
|
+
if (value)
|
|
7253
|
+
return value;
|
|
7254
|
+
}
|
|
7255
|
+
return null;
|
|
7256
|
+
}
|
|
7257
|
+
function classifyProjectKind(_path, metadata) {
|
|
7258
|
+
return projectKindFromMetadata(metadata);
|
|
7197
7259
|
}
|
|
7198
7260
|
function isWorktreePath(path) {
|
|
7199
7261
|
return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
|
|
@@ -7266,7 +7328,6 @@ function taskEventMetadata(task) {
|
|
|
7266
7328
|
metadata.project_canonical_path = projectPath;
|
|
7267
7329
|
}
|
|
7268
7330
|
if (projectPath) {
|
|
7269
|
-
metadata.project_kind = classifyProjectKind(projectPath);
|
|
7270
7331
|
metadata.project_is_worktree = isWorktreePath(projectPath);
|
|
7271
7332
|
metadata.working_dir = task.working_dir ?? projectPath;
|
|
7272
7333
|
}
|
|
@@ -7278,6 +7339,10 @@ function taskEventMetadata(task) {
|
|
|
7278
7339
|
metadata.task_list_project_id = taskList.project_id;
|
|
7279
7340
|
metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
|
|
7280
7341
|
}
|
|
7342
|
+
const projectKind = projectKindFromMetadata(task.metadata, taskList?.metadata);
|
|
7343
|
+
if (projectKind) {
|
|
7344
|
+
metadata.project_kind = classifyProjectKind(projectPath ?? "", { project_kind: projectKind });
|
|
7345
|
+
}
|
|
7281
7346
|
const routeEnabled = routeEnabledForTask(task, taskList);
|
|
7282
7347
|
if (routeEnabled !== undefined) {
|
|
7283
7348
|
metadata.route_enabled = routeEnabled;
|
|
@@ -10333,15 +10398,76 @@ var init_task_relations = __esm(() => {
|
|
|
10333
10398
|
});
|
|
10334
10399
|
|
|
10335
10400
|
// src/db/plans.ts
|
|
10401
|
+
function planSlugBase2(value) {
|
|
10402
|
+
return slugify(value) || "plan";
|
|
10403
|
+
}
|
|
10404
|
+
function normalizePlanSlug(value) {
|
|
10405
|
+
const slug = slugify(value);
|
|
10406
|
+
if (!slug)
|
|
10407
|
+
throw new Error("Invalid plan slug");
|
|
10408
|
+
return slug;
|
|
10409
|
+
}
|
|
10410
|
+
function plansBySlug(slug, db, projectId) {
|
|
10411
|
+
if (projectId !== undefined) {
|
|
10412
|
+
if (projectId === null) {
|
|
10413
|
+
return db.query("SELECT * FROM plans WHERE slug = ? AND project_id IS NULL ORDER BY created_at ASC, id ASC").all(slug);
|
|
10414
|
+
}
|
|
10415
|
+
return db.query("SELECT * FROM plans WHERE slug = ? AND project_id = ? ORDER BY created_at ASC, id ASC").all(slug, projectId);
|
|
10416
|
+
}
|
|
10417
|
+
return db.query("SELECT * FROM plans WHERE slug = ? ORDER BY created_at ASC, id ASC").all(slug);
|
|
10418
|
+
}
|
|
10419
|
+
function planSlugExists(slug, projectId, db, excludeId) {
|
|
10420
|
+
const rows = plansBySlug(slug, db, projectId);
|
|
10421
|
+
return rows.some((plan) => plan.id !== excludeId);
|
|
10422
|
+
}
|
|
10423
|
+
function nextPlanSlug(base, projectId, db, excludeId) {
|
|
10424
|
+
let candidate = base;
|
|
10425
|
+
let suffix = 2;
|
|
10426
|
+
while (planSlugExists(candidate, projectId, db, excludeId)) {
|
|
10427
|
+
candidate = `${base}-${suffix}`;
|
|
10428
|
+
suffix += 1;
|
|
10429
|
+
}
|
|
10430
|
+
return candidate;
|
|
10431
|
+
}
|
|
10432
|
+
function resolveCreateSlug(input, projectId, db) {
|
|
10433
|
+
if (input.slug !== undefined) {
|
|
10434
|
+
const slug = normalizePlanSlug(input.slug);
|
|
10435
|
+
if (planSlugExists(slug, projectId, db)) {
|
|
10436
|
+
throw new Error(`Plan slug already exists in this scope: ${slug}`);
|
|
10437
|
+
}
|
|
10438
|
+
return slug;
|
|
10439
|
+
}
|
|
10440
|
+
return nextPlanSlug(planSlugBase2(input.name), projectId, db);
|
|
10441
|
+
}
|
|
10442
|
+
function resolvePlanRefDetailed(ref, db, projectId) {
|
|
10443
|
+
const d = db || getDatabase();
|
|
10444
|
+
const byId = d.query("SELECT * FROM plans WHERE id = ? OR id LIKE ? ORDER BY id").all(ref, `${ref}%`);
|
|
10445
|
+
if (byId.length === 1)
|
|
10446
|
+
return { id: byId[0].id, reason: "id", matches: byId };
|
|
10447
|
+
if (byId.length > 1)
|
|
10448
|
+
return { id: null, reason: "ambiguous", matches: byId };
|
|
10449
|
+
const slug = slugify(ref);
|
|
10450
|
+
if (!slug)
|
|
10451
|
+
return { id: null, reason: "not_found", matches: [] };
|
|
10452
|
+
const bySlug = plansBySlug(slug, d, projectId);
|
|
10453
|
+
if (bySlug.length === 1)
|
|
10454
|
+
return { id: bySlug[0].id, reason: "slug", matches: bySlug };
|
|
10455
|
+
if (bySlug.length > 1)
|
|
10456
|
+
return { id: null, reason: "ambiguous", matches: bySlug };
|
|
10457
|
+
return { id: null, reason: "not_found", matches: [] };
|
|
10458
|
+
}
|
|
10336
10459
|
function createPlan(input, db) {
|
|
10337
10460
|
const d = db || getDatabase();
|
|
10338
10461
|
const id = uuid();
|
|
10339
10462
|
const timestamp = now();
|
|
10463
|
+
const projectId = input.project_id || null;
|
|
10464
|
+
const slug = resolveCreateSlug(input, projectId, d);
|
|
10340
10465
|
const machineId = currentStorageMachineId(d);
|
|
10341
|
-
d.run(`INSERT INTO plans (id, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at, machine_id)
|
|
10342
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
10466
|
+
d.run(`INSERT INTO plans (id, slug, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at, machine_id)
|
|
10467
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
10343
10468
|
id,
|
|
10344
|
-
|
|
10469
|
+
slug,
|
|
10470
|
+
projectId,
|
|
10345
10471
|
input.task_list_id || null,
|
|
10346
10472
|
input.agent_id || null,
|
|
10347
10473
|
input.name,
|
|
@@ -10376,6 +10502,14 @@ function updatePlan(id, input, db) {
|
|
|
10376
10502
|
sets.push("name = ?");
|
|
10377
10503
|
params.push(input.name);
|
|
10378
10504
|
}
|
|
10505
|
+
if (input.slug !== undefined) {
|
|
10506
|
+
const slug = normalizePlanSlug(input.slug);
|
|
10507
|
+
if (planSlugExists(slug, plan.project_id, d, id)) {
|
|
10508
|
+
throw new Error(`Plan slug already exists in this scope: ${slug}`);
|
|
10509
|
+
}
|
|
10510
|
+
sets.push("slug = ?");
|
|
10511
|
+
params.push(slug);
|
|
10512
|
+
}
|
|
10379
10513
|
if (input.description !== undefined) {
|
|
10380
10514
|
sets.push("description = ?");
|
|
10381
10515
|
params.push(input.description);
|
|
@@ -10420,6 +10554,7 @@ var init_plans = __esm(() => {
|
|
|
10420
10554
|
init_event_emission_safety();
|
|
10421
10555
|
init_event_hooks();
|
|
10422
10556
|
init_database();
|
|
10557
|
+
init_projects();
|
|
10423
10558
|
init_storage_tombstones();
|
|
10424
10559
|
});
|
|
10425
10560
|
|
|
@@ -12626,11 +12761,6 @@ __export(exports_task_routing, {
|
|
|
12626
12761
|
setTaskWorkflowPointers: () => setTaskWorkflowPointers,
|
|
12627
12762
|
getTaskRouteState: () => getTaskRouteState
|
|
12628
12763
|
});
|
|
12629
|
-
function classifyProjectKind2(path) {
|
|
12630
|
-
if (!path)
|
|
12631
|
-
return null;
|
|
12632
|
-
return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
|
|
12633
|
-
}
|
|
12634
12764
|
function machineLocalPath(project, db) {
|
|
12635
12765
|
const machineId = process.env["TODOS_MACHINE_ID"];
|
|
12636
12766
|
if (!machineId)
|
|
@@ -12678,6 +12808,7 @@ function getTaskRouteState(taskOrId, db) {
|
|
|
12678
12808
|
const automation = routingAutomationMetadata(task, taskList) ?? {};
|
|
12679
12809
|
const routeEnabled = routeEnabledForTask(task, taskList) === true;
|
|
12680
12810
|
const tagOptIn = task.tags.includes("auto:route") || task.tags.includes("route:enabled");
|
|
12811
|
+
const projectKind = projectKindFromMetadata(task.metadata, taskList?.metadata);
|
|
12681
12812
|
const locked = Boolean(task.locked_by && !isLockExpired(task.locked_at));
|
|
12682
12813
|
const blockers = getBlockingDeps(task.id, d);
|
|
12683
12814
|
const blocked = blockers.length > 0;
|
|
@@ -12740,7 +12871,7 @@ function getTaskRouteState(taskOrId, db) {
|
|
|
12740
12871
|
project_id: project?.id ?? task.project_id,
|
|
12741
12872
|
project_path: projectPath,
|
|
12742
12873
|
working_dir: task.working_dir ?? projectPath,
|
|
12743
|
-
project_kind:
|
|
12874
|
+
project_kind: projectKind,
|
|
12744
12875
|
task_list_id: taskList?.id ?? task.task_list_id,
|
|
12745
12876
|
task_list_slug: taskList?.slug ?? null,
|
|
12746
12877
|
task_list_name: taskList?.name ?? null,
|
|
@@ -13718,6 +13849,9 @@ function projectSlugMatches(project, ref) {
|
|
|
13718
13849
|
const normalized = slugify(ref);
|
|
13719
13850
|
return Boolean(normalized) && (project.task_list_id === normalized || slugify(project.name) === normalized);
|
|
13720
13851
|
}
|
|
13852
|
+
function planArtifactSlug(plan) {
|
|
13853
|
+
return slugify(plan.slug || plan.name) || "plan";
|
|
13854
|
+
}
|
|
13721
13855
|
function resolvePlanArtifactProject(input) {
|
|
13722
13856
|
const db = input.db || getDatabase();
|
|
13723
13857
|
const ref = input.project_id || input.project_ref;
|
|
@@ -13743,11 +13877,24 @@ function resolvePlanArtifactPaths(input) {
|
|
|
13743
13877
|
const projectRoot = resolve10(project.path);
|
|
13744
13878
|
const directory = join7(projectRoot, ".hasna", "todos", "plans", projectId);
|
|
13745
13879
|
const planId = input.plan_id ? assertSafePathSegment(input.plan_id, "plan id") : null;
|
|
13880
|
+
const planSlug = input.plan_slug ? assertSafePathSegment(slugify(input.plan_slug), "plan slug") : null;
|
|
13881
|
+
const fileName = planId ? planSlug ? `${planSlug}--${planId.slice(0, 8)}.md` : `${planId}.md` : null;
|
|
13746
13882
|
return {
|
|
13747
13883
|
project_id: project.id,
|
|
13748
13884
|
project_root: projectRoot,
|
|
13749
13885
|
directory,
|
|
13750
|
-
file_path:
|
|
13886
|
+
file_path: fileName ? join7(directory, fileName) : directory
|
|
13887
|
+
};
|
|
13888
|
+
}
|
|
13889
|
+
function resolvePlanArtifactCandidatePaths(plan, db) {
|
|
13890
|
+
return {
|
|
13891
|
+
primary: resolvePlanArtifactPaths({
|
|
13892
|
+
project_id: plan.project_id,
|
|
13893
|
+
plan_id: plan.id,
|
|
13894
|
+
plan_slug: planArtifactSlug(plan),
|
|
13895
|
+
db
|
|
13896
|
+
}),
|
|
13897
|
+
legacy: resolvePlanArtifactPaths({ project_id: plan.project_id, plan_id: plan.id, db })
|
|
13751
13898
|
};
|
|
13752
13899
|
}
|
|
13753
13900
|
function buildPlanArtifactSnapshot(plan, tasks = [], artifactUpdatedAt = new Date().toISOString()) {
|
|
@@ -13764,6 +13911,7 @@ function buildPlanArtifactSnapshot(plan, tasks = [], artifactUpdatedAt = new Dat
|
|
|
13764
13911
|
metadata: {
|
|
13765
13912
|
schema: PLAN_MARKDOWN_SCHEMA,
|
|
13766
13913
|
plan_id: plan.id,
|
|
13914
|
+
plan_slug: plan.slug ?? null,
|
|
13767
13915
|
project_id: plan.project_id,
|
|
13768
13916
|
task_list_id: plan.task_list_id ?? null,
|
|
13769
13917
|
agent_id: plan.agent_id ?? null,
|
|
@@ -13803,6 +13951,7 @@ function renderPlanArtifactMarkdown(snapshot) {
|
|
|
13803
13951
|
"---",
|
|
13804
13952
|
`schema: ${frontmatterScalar(metadata.schema)}`,
|
|
13805
13953
|
`plan_id: ${frontmatterScalar(metadata.plan_id)}`,
|
|
13954
|
+
`plan_slug: ${frontmatterScalar(metadata.plan_slug)}`,
|
|
13806
13955
|
`project_id: ${frontmatterScalar(metadata.project_id)}`,
|
|
13807
13956
|
`task_list_id: ${frontmatterScalar(metadata.task_list_id)}`,
|
|
13808
13957
|
`agent_id: ${frontmatterScalar(metadata.agent_id)}`,
|
|
@@ -13848,6 +13997,7 @@ function parsePlanArtifactMarkdown(markdown) {
|
|
|
13848
13997
|
metadata: {
|
|
13849
13998
|
schema: PLAN_MARKDOWN_SCHEMA,
|
|
13850
13999
|
plan_id: rawMetadata.plan_id,
|
|
14000
|
+
plan_slug: rawMetadata.plan_slug ?? null,
|
|
13851
14001
|
project_id: rawMetadata.project_id,
|
|
13852
14002
|
task_list_id: rawMetadata.task_list_id ?? null,
|
|
13853
14003
|
agent_id: rawMetadata.agent_id ?? null,
|
|
@@ -13888,7 +14038,7 @@ function writePlanArtifact(plan, db) {
|
|
|
13888
14038
|
return null;
|
|
13889
14039
|
const d = db || getDatabase();
|
|
13890
14040
|
const tasks = listTasks({ plan_id: plan.id, include_archived: true }, d);
|
|
13891
|
-
const paths =
|
|
14041
|
+
const paths = resolvePlanArtifactCandidatePaths(plan, d).primary;
|
|
13892
14042
|
const snapshot = buildPlanArtifactSnapshot(plan, tasks);
|
|
13893
14043
|
mkdirSync5(paths.directory, { recursive: true });
|
|
13894
14044
|
writeFileSync3(paths.file_path, renderPlanArtifactMarkdown(snapshot), "utf8");
|
|
@@ -13898,12 +14048,13 @@ function readPlanArtifact(plan, db) {
|
|
|
13898
14048
|
if (!plan.project_id)
|
|
13899
14049
|
return null;
|
|
13900
14050
|
const d = db || getDatabase();
|
|
13901
|
-
const paths =
|
|
13902
|
-
|
|
14051
|
+
const paths = resolvePlanArtifactCandidatePaths(plan, d);
|
|
14052
|
+
const path = existsSync8(paths.primary.file_path) ? paths.primary.file_path : existsSync8(paths.legacy.file_path) ? paths.legacy.file_path : null;
|
|
14053
|
+
if (!path)
|
|
13903
14054
|
return null;
|
|
13904
|
-
const markdown = readFileSync4(
|
|
14055
|
+
const markdown = readFileSync4(path, "utf8");
|
|
13905
14056
|
return {
|
|
13906
|
-
path
|
|
14057
|
+
path,
|
|
13907
14058
|
markdown,
|
|
13908
14059
|
...parsePlanArtifactMarkdown(markdown)
|
|
13909
14060
|
};
|
|
@@ -13912,10 +14063,11 @@ function inspectPlanArtifact(plan, db) {
|
|
|
13912
14063
|
if (!plan.project_id)
|
|
13913
14064
|
return null;
|
|
13914
14065
|
const d = db || getDatabase();
|
|
13915
|
-
const paths =
|
|
13916
|
-
|
|
14066
|
+
const paths = resolvePlanArtifactCandidatePaths(plan, d);
|
|
14067
|
+
const path = existsSync8(paths.primary.file_path) ? paths.primary.file_path : existsSync8(paths.legacy.file_path) ? paths.legacy.file_path : null;
|
|
14068
|
+
if (!path) {
|
|
13917
14069
|
return {
|
|
13918
|
-
path: paths.file_path,
|
|
14070
|
+
path: paths.primary.file_path,
|
|
13919
14071
|
exists: false,
|
|
13920
14072
|
parse_error: null,
|
|
13921
14073
|
metadata: null,
|
|
@@ -13924,9 +14076,9 @@ function inspectPlanArtifact(plan, db) {
|
|
|
13924
14076
|
};
|
|
13925
14077
|
}
|
|
13926
14078
|
try {
|
|
13927
|
-
const artifact = parsePlanArtifactMarkdown(readFileSync4(
|
|
14079
|
+
const artifact = parsePlanArtifactMarkdown(readFileSync4(path, "utf8"));
|
|
13928
14080
|
return {
|
|
13929
|
-
path
|
|
14081
|
+
path,
|
|
13930
14082
|
exists: true,
|
|
13931
14083
|
parse_error: null,
|
|
13932
14084
|
metadata: artifact.metadata,
|
|
@@ -13935,7 +14087,7 @@ function inspectPlanArtifact(plan, db) {
|
|
|
13935
14087
|
};
|
|
13936
14088
|
} catch (error) {
|
|
13937
14089
|
return {
|
|
13938
|
-
path
|
|
14090
|
+
path,
|
|
13939
14091
|
exists: true,
|
|
13940
14092
|
parse_error: error instanceof Error ? error.message : String(error),
|
|
13941
14093
|
metadata: null,
|
|
@@ -13947,6 +14099,9 @@ function inspectPlanArtifact(plan, db) {
|
|
|
13947
14099
|
function comparePlanArtifact(plan, artifact, tasks) {
|
|
13948
14100
|
const conflicts = [];
|
|
13949
14101
|
compare("plan_id", plan.id, artifact.metadata.plan_id, conflicts);
|
|
14102
|
+
if (artifact.metadata.plan_slug !== null) {
|
|
14103
|
+
compare("plan_slug", plan.slug ?? null, artifact.metadata.plan_slug, conflicts);
|
|
14104
|
+
}
|
|
13950
14105
|
compare("project_id", plan.project_id ?? null, artifact.metadata.project_id, conflicts);
|
|
13951
14106
|
compare("name", plan.name, artifact.metadata.name, conflicts);
|
|
13952
14107
|
compare("status", plan.status, artifact.metadata.status, conflicts);
|
|
@@ -14316,22 +14471,44 @@ __export(exports_plan_template_commands, {
|
|
|
14316
14471
|
registerPlanTemplateCommands: () => registerPlanTemplateCommands
|
|
14317
14472
|
});
|
|
14318
14473
|
import chalk3 from "chalk";
|
|
14474
|
+
function resolvePlanCliRef(ref, projectId) {
|
|
14475
|
+
const db = getDatabase();
|
|
14476
|
+
const resolved = resolvePlanRefDetailed(ref, db, projectId);
|
|
14477
|
+
if (resolved.id)
|
|
14478
|
+
return resolved.id;
|
|
14479
|
+
if (resolved.reason === "ambiguous") {
|
|
14480
|
+
console.error(chalk3.red(`Ambiguous plan reference: ${ref}`));
|
|
14481
|
+
if (resolved.matches.length > 0) {
|
|
14482
|
+
console.error(chalk3.dim(`Matches: ${resolved.matches.map((plan) => `${plan.slug ?? plan.name} (${plan.id.slice(0, 8)})`).join(", ")}`));
|
|
14483
|
+
}
|
|
14484
|
+
} else {
|
|
14485
|
+
console.error(chalk3.red(`Could not resolve plan ID or slug: ${ref}`));
|
|
14486
|
+
}
|
|
14487
|
+
process.exit(1);
|
|
14488
|
+
}
|
|
14319
14489
|
function registerPlanTemplateCommands(program2) {
|
|
14320
|
-
program2.command("plans").description("List and manage plans").option("--add <name>", "Create a plan").option("-d, --description <text>", "Plan description (with --add)").option("--show <id>", "Show plan details with its tasks").option("--artifact <id>", "Show local Markdown artifact diagnostics for a plan").option("--write-artifacts", "Write local Markdown artifacts for all project-scoped plans in scope").option("--delete <id>", "Delete a plan").option("--complete <id>", "Mark a plan as completed").action((opts) => {
|
|
14490
|
+
program2.command("plans").description("List and manage plans").option("--add <name>", "Create a plan").option("--slug <slug>", "Readable plan slug (with --add)").option("-d, --description <text>", "Plan description (with --add)").option("--show <id-or-slug>", "Show plan details with its tasks").option("--artifact <id-or-slug>", "Show local Markdown artifact diagnostics for a plan").option("--write-artifacts", "Write local Markdown artifacts for all project-scoped plans in scope").option("--delete <id>", "Delete a plan").option("--complete <id>", "Mark a plan as completed").action((opts) => {
|
|
14321
14491
|
const globalOpts = program2.opts();
|
|
14322
14492
|
const projectId = autoProject(globalOpts);
|
|
14323
14493
|
if (opts.add) {
|
|
14324
|
-
|
|
14325
|
-
|
|
14326
|
-
|
|
14327
|
-
|
|
14328
|
-
|
|
14494
|
+
let plan;
|
|
14495
|
+
try {
|
|
14496
|
+
plan = createPlan({
|
|
14497
|
+
name: opts.add,
|
|
14498
|
+
slug: opts.slug,
|
|
14499
|
+
description: opts.description,
|
|
14500
|
+
project_id: projectId
|
|
14501
|
+
});
|
|
14502
|
+
} catch (error) {
|
|
14503
|
+
handleError(error);
|
|
14504
|
+
}
|
|
14329
14505
|
const artifact = writePlanArtifact(plan);
|
|
14330
14506
|
if (globalOpts.json) {
|
|
14331
14507
|
output(plan, true);
|
|
14332
14508
|
} else {
|
|
14333
14509
|
console.log(chalk3.green("Plan created:"));
|
|
14334
14510
|
console.log(`${chalk3.dim(plan.id.slice(0, 8))} ${chalk3.bold(plan.name)} ${chalk3.cyan(`[${plan.status}]`)}`);
|
|
14511
|
+
console.log(`${chalk3.dim("Slug:")} ${plan.slug}`);
|
|
14335
14512
|
if (artifact)
|
|
14336
14513
|
console.log(`${chalk3.dim("Artifact:")} ${artifact.path}`);
|
|
14337
14514
|
}
|
|
@@ -14339,11 +14516,7 @@ function registerPlanTemplateCommands(program2) {
|
|
|
14339
14516
|
}
|
|
14340
14517
|
if (opts.artifact) {
|
|
14341
14518
|
const db = getDatabase();
|
|
14342
|
-
const resolvedId =
|
|
14343
|
-
if (!resolvedId) {
|
|
14344
|
-
console.error(chalk3.red(`Could not resolve plan ID: ${opts.artifact}`));
|
|
14345
|
-
process.exit(1);
|
|
14346
|
-
}
|
|
14519
|
+
const resolvedId = resolvePlanCliRef(opts.artifact, projectId);
|
|
14347
14520
|
const plan = getPlan(resolvedId);
|
|
14348
14521
|
if (!plan) {
|
|
14349
14522
|
console.error(chalk3.red(`Plan not found: ${opts.artifact}`));
|
|
@@ -14396,11 +14569,7 @@ function registerPlanTemplateCommands(program2) {
|
|
|
14396
14569
|
}
|
|
14397
14570
|
if (opts.show) {
|
|
14398
14571
|
const db = getDatabase();
|
|
14399
|
-
const resolvedId =
|
|
14400
|
-
if (!resolvedId) {
|
|
14401
|
-
console.error(chalk3.red(`Could not resolve plan ID: ${opts.show}`));
|
|
14402
|
-
process.exit(1);
|
|
14403
|
-
}
|
|
14572
|
+
const resolvedId = resolvePlanCliRef(opts.show, projectId);
|
|
14404
14573
|
const plan = getPlan(resolvedId);
|
|
14405
14574
|
if (!plan) {
|
|
14406
14575
|
console.error(chalk3.red(`Plan not found: ${opts.show}`));
|
|
@@ -14425,6 +14594,8 @@ function registerPlanTemplateCommands(program2) {
|
|
|
14425
14594
|
console.log(chalk3.bold(`Plan Details:
|
|
14426
14595
|
`));
|
|
14427
14596
|
console.log(` ${chalk3.dim("ID:")} ${plan.id}`);
|
|
14597
|
+
if (plan.slug)
|
|
14598
|
+
console.log(` ${chalk3.dim("Slug:")} ${plan.slug}`);
|
|
14428
14599
|
console.log(` ${chalk3.dim("Name:")} ${plan.name}`);
|
|
14429
14600
|
console.log(` ${chalk3.dim("Status:")} ${chalk3.cyan(plan.status)}`);
|
|
14430
14601
|
if (plan.description)
|
|
@@ -14447,12 +14618,7 @@ function registerPlanTemplateCommands(program2) {
|
|
|
14447
14618
|
return;
|
|
14448
14619
|
}
|
|
14449
14620
|
if (opts.delete) {
|
|
14450
|
-
const
|
|
14451
|
-
const resolvedId = resolvePartialId(db, "plans", opts.delete);
|
|
14452
|
-
if (!resolvedId) {
|
|
14453
|
-
console.error(chalk3.red(`Could not resolve plan ID: ${opts.delete}`));
|
|
14454
|
-
process.exit(1);
|
|
14455
|
-
}
|
|
14621
|
+
const resolvedId = resolvePlanCliRef(opts.delete, projectId);
|
|
14456
14622
|
const deleted = deletePlan(resolvedId);
|
|
14457
14623
|
if (globalOpts.json) {
|
|
14458
14624
|
output({ deleted }, true);
|
|
@@ -14465,12 +14631,7 @@ function registerPlanTemplateCommands(program2) {
|
|
|
14465
14631
|
return;
|
|
14466
14632
|
}
|
|
14467
14633
|
if (opts.complete) {
|
|
14468
|
-
const
|
|
14469
|
-
const resolvedId = resolvePartialId(db, "plans", opts.complete);
|
|
14470
|
-
if (!resolvedId) {
|
|
14471
|
-
console.error(chalk3.red(`Could not resolve plan ID: ${opts.complete}`));
|
|
14472
|
-
process.exit(1);
|
|
14473
|
-
}
|
|
14634
|
+
const resolvedId = resolvePlanCliRef(opts.complete, projectId);
|
|
14474
14635
|
try {
|
|
14475
14636
|
const plan = updatePlan(resolvedId, { status: "completed" });
|
|
14476
14637
|
const artifact = writePlanArtifact(plan);
|
|
@@ -14500,7 +14661,8 @@ function registerPlanTemplateCommands(program2) {
|
|
|
14500
14661
|
`));
|
|
14501
14662
|
for (const p of plans) {
|
|
14502
14663
|
const desc = p.description ? chalk3.dim(` - ${p.description}`) : "";
|
|
14503
|
-
|
|
14664
|
+
const slug = p.slug ? chalk3.dim(` ${p.slug}`) : "";
|
|
14665
|
+
console.log(`${chalk3.dim(p.id.slice(0, 8))}${slug} ${chalk3.bold(p.name)} ${chalk3.cyan(`[${p.status}]`)}${desc}`);
|
|
14504
14666
|
}
|
|
14505
14667
|
});
|
|
14506
14668
|
program2.command("templates").description("List and manage task templates").option("--add <name>", "Create a template").option("--title <pattern>", "Title pattern (with --add)").option("-d, --description <text>", "Default description").option("-p, --priority <level>", "Default priority").option("-t, --tags <tags>", "Default tags (comma-separated)").option("--delete <id>", "Delete a template").option("--update <id>", "Update a template").option("--use <id>", "Create a task from a template").option("--var <vars...>", "Variable substitutions: key=value (e.g. --var feature=login)").action(async (opts) => {
|
|
@@ -22074,6 +22236,36 @@ function prepareValue(column, value) {
|
|
|
22074
22236
|
return JSON.stringify(value ?? (column === "tags" || column === "files_changed" ? [] : {}));
|
|
22075
22237
|
return value === undefined ? null : value;
|
|
22076
22238
|
}
|
|
22239
|
+
function slugifyPlanValue(value) {
|
|
22240
|
+
return typeof value === "string" ? value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") : "";
|
|
22241
|
+
}
|
|
22242
|
+
function planSlugBase3(plan) {
|
|
22243
|
+
return slugifyPlanValue(plan.slug) || slugifyPlanValue(plan.name) || "plan";
|
|
22244
|
+
}
|
|
22245
|
+
function planSlugScope(projectId) {
|
|
22246
|
+
return typeof projectId === "string" && projectId ? projectId : "__global__";
|
|
22247
|
+
}
|
|
22248
|
+
function planSlugKey(projectId, slug) {
|
|
22249
|
+
return `${planSlugScope(projectId)}:${slug}`;
|
|
22250
|
+
}
|
|
22251
|
+
function normalizeBridgePlanSlugs(plans, db) {
|
|
22252
|
+
const existingRows = db.query("SELECT id, project_id, slug FROM plans WHERE slug IS NOT NULL").all();
|
|
22253
|
+
const existingIds = new Set(existingRows.map((row) => row.id));
|
|
22254
|
+
const used = new Set(existingRows.filter((row) => row.slug).map((row) => planSlugKey(row.project_id, row.slug)));
|
|
22255
|
+
return plans.map((plan) => {
|
|
22256
|
+
if (existingIds.has(plan.id))
|
|
22257
|
+
return plan;
|
|
22258
|
+
const base = planSlugBase3(plan);
|
|
22259
|
+
let candidate = base;
|
|
22260
|
+
let suffix = 2;
|
|
22261
|
+
while (used.has(planSlugKey(plan.project_id, candidate))) {
|
|
22262
|
+
candidate = `${base}-${suffix}`;
|
|
22263
|
+
suffix += 1;
|
|
22264
|
+
}
|
|
22265
|
+
used.add(planSlugKey(plan.project_id, candidate));
|
|
22266
|
+
return { ...plan, slug: candidate };
|
|
22267
|
+
});
|
|
22268
|
+
}
|
|
22077
22269
|
function insertRecord(db, tableKey, row) {
|
|
22078
22270
|
const table = tableByKey[tableKey];
|
|
22079
22271
|
const columns = insertColumns[tableKey];
|
|
@@ -22210,6 +22402,7 @@ function importLocalBridgeBundle(bundle, options = {}, db) {
|
|
|
22210
22402
|
const conflictStrategy = options.conflictStrategy ?? "skip";
|
|
22211
22403
|
const data = {
|
|
22212
22404
|
...bundle.data,
|
|
22405
|
+
plans: normalizeBridgePlanSlugs(bundle.data.plans, d),
|
|
22213
22406
|
tasks: sortedTasks(bundle.data.tasks),
|
|
22214
22407
|
saved_views: bundle.data.saved_views ?? [],
|
|
22215
22408
|
task_boards: bundle.data.task_boards ?? [],
|
|
@@ -22294,7 +22487,7 @@ var init_local_bridge = __esm(() => {
|
|
|
22294
22487
|
insertColumns = {
|
|
22295
22488
|
projects: ["id", "name", "path", "description", "task_list_id", "task_prefix", "task_counter", "created_at", "updated_at", "machine_id", "synced_at"],
|
|
22296
22489
|
task_lists: ["id", "project_id", "slug", "name", "description", "metadata", "created_at", "updated_at", "machine_id", "synced_at"],
|
|
22297
|
-
plans: ["id", "project_id", "task_list_id", "agent_id", "name", "description", "status", "created_at", "updated_at", "machine_id", "synced_at"],
|
|
22490
|
+
plans: ["id", "slug", "project_id", "task_list_id", "agent_id", "name", "description", "status", "created_at", "updated_at", "machine_id", "synced_at"],
|
|
22298
22491
|
tasks: [
|
|
22299
22492
|
"id",
|
|
22300
22493
|
"short_id",
|
|
@@ -28389,6 +28582,7 @@ async function handleCreatePlan(req, _ctx, json2) {
|
|
|
28389
28582
|
return json2({ error: "Missing 'name'" }, 400);
|
|
28390
28583
|
const plan = createPlan({
|
|
28391
28584
|
name: body.name,
|
|
28585
|
+
slug: body.slug,
|
|
28392
28586
|
description: body.description,
|
|
28393
28587
|
project_id: body.project_id,
|
|
28394
28588
|
task_list_id: body.task_list_id,
|
|
@@ -37738,6 +37932,7 @@ Tasks:` : null,
|
|
|
37738
37932
|
if (shouldRegisterTool("create_plan")) {
|
|
37739
37933
|
server.tool("create_plan", "Create a new plan (sprint/milestone).", {
|
|
37740
37934
|
name: exports_external2.string().describe("Plan name"),
|
|
37935
|
+
slug: exports_external2.string().optional().describe("Readable plan slug"),
|
|
37741
37936
|
project_id: exports_external2.string().optional().describe("Project ID"),
|
|
37742
37937
|
description: exports_external2.string().optional(),
|
|
37743
37938
|
start_date: exports_external2.string().optional().describe("ISO date"),
|
|
@@ -44898,6 +45093,7 @@ function createAgentProjectDemoBundle() {
|
|
|
44898
45093
|
});
|
|
44899
45094
|
data.plans.push({
|
|
44900
45095
|
id: ids.plan,
|
|
45096
|
+
slug: "ship-local-demo-workflow",
|
|
44901
45097
|
project_id: ids.project,
|
|
44902
45098
|
task_list_id: ids.list,
|
|
44903
45099
|
agent_id: "demo-agent",
|
|
@@ -55038,6 +55234,339 @@ var init_config_serve_commands = __esm(() => {
|
|
|
55038
55234
|
init_helpers();
|
|
55039
55235
|
});
|
|
55040
55236
|
|
|
55237
|
+
// src/lib/task-route-sources.ts
|
|
55238
|
+
var exports_task_route_sources = {};
|
|
55239
|
+
__export(exports_task_route_sources, {
|
|
55240
|
+
discoverTaskRouteSources: () => discoverTaskRouteSources,
|
|
55241
|
+
TASK_ROUTE_SOURCE_DISCOVERY_SCHEMA_VERSION: () => TASK_ROUTE_SOURCE_DISCOVERY_SCHEMA_VERSION
|
|
55242
|
+
});
|
|
55243
|
+
import { Database as Database3 } from "bun:sqlite";
|
|
55244
|
+
import { createHash as createHash13 } from "crypto";
|
|
55245
|
+
import { existsSync as existsSync21, readdirSync as readdirSync5, statSync as statSync9 } from "fs";
|
|
55246
|
+
import { basename as basename9, dirname as dirname12, join as join21, resolve as resolve21 } from "path";
|
|
55247
|
+
function normalizePath5(input) {
|
|
55248
|
+
return resolve21(input);
|
|
55249
|
+
}
|
|
55250
|
+
function sourceStoreId(sourceDbPath) {
|
|
55251
|
+
const digest = createHash13("sha256").update(sourceDbPath).digest("hex").slice(0, 16);
|
|
55252
|
+
return `sqlite:${digest}`;
|
|
55253
|
+
}
|
|
55254
|
+
function inferSourceRepoPath(sourceDbPath) {
|
|
55255
|
+
const normalized = normalizePath5(sourceDbPath);
|
|
55256
|
+
if (normalized.endsWith(TODO_STORE_RELATIVE_PATH)) {
|
|
55257
|
+
return dirname12(dirname12(dirname12(normalized)));
|
|
55258
|
+
}
|
|
55259
|
+
return dirname12(normalized);
|
|
55260
|
+
}
|
|
55261
|
+
function createStoreRef(sourceDbPath) {
|
|
55262
|
+
const normalized = normalizePath5(sourceDbPath);
|
|
55263
|
+
return {
|
|
55264
|
+
source_store_id: sourceStoreId(normalized),
|
|
55265
|
+
source_repo_path: inferSourceRepoPath(normalized),
|
|
55266
|
+
source_db_path: normalized
|
|
55267
|
+
};
|
|
55268
|
+
}
|
|
55269
|
+
function normalizePatterns(patterns) {
|
|
55270
|
+
return (patterns ?? []).map((pattern) => pattern.trim()).filter(Boolean);
|
|
55271
|
+
}
|
|
55272
|
+
function escapeRegExp(value) {
|
|
55273
|
+
return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
55274
|
+
}
|
|
55275
|
+
function globPatternToRegExp(pattern) {
|
|
55276
|
+
let source3 = "";
|
|
55277
|
+
for (const char of pattern) {
|
|
55278
|
+
if (char === "*")
|
|
55279
|
+
source3 += ".*";
|
|
55280
|
+
else if (char === "?")
|
|
55281
|
+
source3 += ".";
|
|
55282
|
+
else
|
|
55283
|
+
source3 += escapeRegExp(char);
|
|
55284
|
+
}
|
|
55285
|
+
return new RegExp(`^${source3}$`);
|
|
55286
|
+
}
|
|
55287
|
+
function matchesPattern4(value, pattern) {
|
|
55288
|
+
const normalizedValue = value.replace(/\\/g, "/");
|
|
55289
|
+
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
55290
|
+
if (normalizedPattern.includes("*") || normalizedPattern.includes("?")) {
|
|
55291
|
+
return globPatternToRegExp(normalizedPattern).test(normalizedValue);
|
|
55292
|
+
}
|
|
55293
|
+
return normalizedValue.includes(normalizedPattern);
|
|
55294
|
+
}
|
|
55295
|
+
function storeMatchesAny(ref, patterns) {
|
|
55296
|
+
if (patterns.length === 0)
|
|
55297
|
+
return false;
|
|
55298
|
+
const paths = [ref.source_db_path, ref.source_repo_path].filter((value) => Boolean(value));
|
|
55299
|
+
const values = paths.flatMap((value) => [value, basename9(value)]);
|
|
55300
|
+
return patterns.some((pattern) => values.some((value) => matchesPattern4(value, pattern)));
|
|
55301
|
+
}
|
|
55302
|
+
function shouldIncludeStore(ref, include, exclude) {
|
|
55303
|
+
const included = include.length === 0 || storeMatchesAny(ref, include);
|
|
55304
|
+
return included && !storeMatchesAny(ref, exclude);
|
|
55305
|
+
}
|
|
55306
|
+
function discoverStoresUnderRoot(sourceRoot) {
|
|
55307
|
+
const rootPath = normalizePath5(sourceRoot);
|
|
55308
|
+
const errors2 = [];
|
|
55309
|
+
const stores = [];
|
|
55310
|
+
if (!existsSync21(rootPath)) {
|
|
55311
|
+
const ref = createStoreRef(join21(rootPath, TODO_STORE_RELATIVE_PATH));
|
|
55312
|
+
errors2.push({
|
|
55313
|
+
...ref,
|
|
55314
|
+
code: "SOURCE_ROOT_MISSING",
|
|
55315
|
+
message: `Source root does not exist: ${rootPath}`
|
|
55316
|
+
});
|
|
55317
|
+
return { stores, errors: errors2 };
|
|
55318
|
+
}
|
|
55319
|
+
let rootStat;
|
|
55320
|
+
try {
|
|
55321
|
+
rootStat = statSync9(rootPath);
|
|
55322
|
+
} catch (error) {
|
|
55323
|
+
const ref = createStoreRef(join21(rootPath, TODO_STORE_RELATIVE_PATH));
|
|
55324
|
+
errors2.push({
|
|
55325
|
+
...ref,
|
|
55326
|
+
code: "SOURCE_ROOT_UNREADABLE",
|
|
55327
|
+
message: error instanceof Error ? error.message : `Unable to read source root: ${rootPath}`
|
|
55328
|
+
});
|
|
55329
|
+
return { stores, errors: errors2 };
|
|
55330
|
+
}
|
|
55331
|
+
if (rootStat.isFile()) {
|
|
55332
|
+
stores.push(createStoreRef(rootPath));
|
|
55333
|
+
return { stores, errors: errors2 };
|
|
55334
|
+
}
|
|
55335
|
+
function scanDirectory(dir, depth) {
|
|
55336
|
+
const candidate = join21(dir, TODO_STORE_RELATIVE_PATH);
|
|
55337
|
+
if (existsSync21(candidate)) {
|
|
55338
|
+
stores.push(createStoreRef(candidate));
|
|
55339
|
+
}
|
|
55340
|
+
if (depth >= ROOT_SCAN_MAX_DEPTH)
|
|
55341
|
+
return;
|
|
55342
|
+
let entries;
|
|
55343
|
+
try {
|
|
55344
|
+
entries = readdirSync5(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
55345
|
+
} catch (error) {
|
|
55346
|
+
const ref = createStoreRef(candidate);
|
|
55347
|
+
errors2.push({
|
|
55348
|
+
...ref,
|
|
55349
|
+
code: "SOURCE_ROOT_UNREADABLE",
|
|
55350
|
+
message: error instanceof Error ? error.message : `Unable to read source root: ${dir}`
|
|
55351
|
+
});
|
|
55352
|
+
return;
|
|
55353
|
+
}
|
|
55354
|
+
for (const entry of entries) {
|
|
55355
|
+
if (!entry.isDirectory() || SKIPPED_SCAN_DIRS.has(entry.name))
|
|
55356
|
+
continue;
|
|
55357
|
+
scanDirectory(join21(dir, entry.name), depth + 1);
|
|
55358
|
+
}
|
|
55359
|
+
}
|
|
55360
|
+
scanDirectory(rootPath, 0);
|
|
55361
|
+
return { stores, errors: errors2 };
|
|
55362
|
+
}
|
|
55363
|
+
function collectStoreRefs(input) {
|
|
55364
|
+
const byPath = new Map;
|
|
55365
|
+
const errors2 = [];
|
|
55366
|
+
for (const storePath of input.sourceStores ?? []) {
|
|
55367
|
+
const ref = createStoreRef(storePath);
|
|
55368
|
+
byPath.set(ref.source_db_path, ref);
|
|
55369
|
+
}
|
|
55370
|
+
for (const sourceRoot of input.sourceRoots ?? []) {
|
|
55371
|
+
const discovered = discoverStoresUnderRoot(sourceRoot);
|
|
55372
|
+
for (const ref of discovered.stores) {
|
|
55373
|
+
byPath.set(ref.source_db_path, ref);
|
|
55374
|
+
}
|
|
55375
|
+
errors2.push(...discovered.errors);
|
|
55376
|
+
}
|
|
55377
|
+
return {
|
|
55378
|
+
stores: [...byPath.values()].sort((a, b) => a.source_db_path.localeCompare(b.source_db_path)),
|
|
55379
|
+
errors: errors2.sort((a, b) => a.source_db_path.localeCompare(b.source_db_path))
|
|
55380
|
+
};
|
|
55381
|
+
}
|
|
55382
|
+
function openReadonlyStore(ref) {
|
|
55383
|
+
if (!existsSync21(ref.source_db_path)) {
|
|
55384
|
+
throw Object.assign(new Error(`Store does not exist: ${ref.source_db_path}`), { code: "STORE_MISSING" });
|
|
55385
|
+
}
|
|
55386
|
+
return new Database3(ref.source_db_path, { readonly: true, create: false });
|
|
55387
|
+
}
|
|
55388
|
+
function hasTable2(db, tableName) {
|
|
55389
|
+
const row = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(tableName);
|
|
55390
|
+
return Boolean(row);
|
|
55391
|
+
}
|
|
55392
|
+
function tableColumns2(db, tableName) {
|
|
55393
|
+
const rows = db.query(`PRAGMA table_info(${tableName})`).all();
|
|
55394
|
+
return new Set(rows.map((row) => row.name));
|
|
55395
|
+
}
|
|
55396
|
+
function listPendingTasksReadonly(db) {
|
|
55397
|
+
if (!hasTable2(db, "tasks")) {
|
|
55398
|
+
throw Object.assign(new Error("Store does not contain a tasks table"), { code: "STORE_INVALID" });
|
|
55399
|
+
}
|
|
55400
|
+
const columns = tableColumns2(db, "tasks");
|
|
55401
|
+
const conditions = ["status = 'pending'"];
|
|
55402
|
+
if (columns.has("archived_at"))
|
|
55403
|
+
conditions.push("archived_at IS NULL");
|
|
55404
|
+
const rows = db.query(`SELECT * FROM tasks WHERE ${conditions.join(" AND ")}
|
|
55405
|
+
ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END, created_at DESC`).all();
|
|
55406
|
+
return rows.map(rowToTask);
|
|
55407
|
+
}
|
|
55408
|
+
function isReadyTask(task2, db) {
|
|
55409
|
+
if (task2.locked_by && !isLockExpired(task2.locked_at))
|
|
55410
|
+
return false;
|
|
55411
|
+
return getBlockingDeps(task2.id, db).length === 0;
|
|
55412
|
+
}
|
|
55413
|
+
function metadataFingerprint(metadata) {
|
|
55414
|
+
const value = metadata.fingerprint;
|
|
55415
|
+
if (typeof value === "string" && value.trim())
|
|
55416
|
+
return value;
|
|
55417
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
55418
|
+
return String(value);
|
|
55419
|
+
return null;
|
|
55420
|
+
}
|
|
55421
|
+
function boundedMetadataValue(value, depth = 0) {
|
|
55422
|
+
if (depth > 6)
|
|
55423
|
+
return "[TRUNCATED]";
|
|
55424
|
+
if (typeof value === "string") {
|
|
55425
|
+
return value.length > 2000 ? `${value.slice(0, 2000)}[TRUNCATED]` : value;
|
|
55426
|
+
}
|
|
55427
|
+
if (Array.isArray(value)) {
|
|
55428
|
+
return value.slice(0, 50).map((item) => boundedMetadataValue(item, depth + 1));
|
|
55429
|
+
}
|
|
55430
|
+
if (value && typeof value === "object") {
|
|
55431
|
+
const result = {};
|
|
55432
|
+
for (const [key, child] of Object.entries(value).slice(0, 80)) {
|
|
55433
|
+
const normalized = key.toLowerCase();
|
|
55434
|
+
if (normalized === "comment" || normalized === "comments" || normalized === "task_comments") {
|
|
55435
|
+
result[key] = "[REDACTED_COMMENT]";
|
|
55436
|
+
continue;
|
|
55437
|
+
}
|
|
55438
|
+
result[key] = boundedMetadataValue(child, depth + 1);
|
|
55439
|
+
}
|
|
55440
|
+
return result;
|
|
55441
|
+
}
|
|
55442
|
+
return value;
|
|
55443
|
+
}
|
|
55444
|
+
function discoveryMetadata(metadata) {
|
|
55445
|
+
return redactValue(boundedMetadataValue(metadata));
|
|
55446
|
+
}
|
|
55447
|
+
function sourceCandidate(ref, task2, db) {
|
|
55448
|
+
const routeState = getTaskRouteState(task2, db);
|
|
55449
|
+
const autoRoute = task2.tags.includes("auto:route") || task2.tags.includes("route:enabled");
|
|
55450
|
+
return {
|
|
55451
|
+
source_store_id: ref.source_store_id,
|
|
55452
|
+
source_repo_path: ref.source_repo_path,
|
|
55453
|
+
source_db_path: ref.source_db_path,
|
|
55454
|
+
source_task_key: `${ref.source_store_id}:${task2.id}`,
|
|
55455
|
+
source_selected_by_input: true,
|
|
55456
|
+
task_id: task2.id,
|
|
55457
|
+
task_short_id: task2.short_id,
|
|
55458
|
+
title: task2.title,
|
|
55459
|
+
status: task2.status,
|
|
55460
|
+
priority: task2.priority,
|
|
55461
|
+
project_path: routeState.route.project_path ?? task2.working_dir ?? ref.source_repo_path,
|
|
55462
|
+
task_version: task2.version,
|
|
55463
|
+
task_updated_at: task2.updated_at,
|
|
55464
|
+
task_fingerprint: metadataFingerprint(task2.metadata),
|
|
55465
|
+
tags: task2.tags,
|
|
55466
|
+
task_intent: {
|
|
55467
|
+
auto_route: autoRoute
|
|
55468
|
+
},
|
|
55469
|
+
metadata: discoveryMetadata(task2.metadata),
|
|
55470
|
+
route_state: routeState
|
|
55471
|
+
};
|
|
55472
|
+
}
|
|
55473
|
+
function discoveryError(ref, code, error) {
|
|
55474
|
+
return {
|
|
55475
|
+
...ref,
|
|
55476
|
+
code,
|
|
55477
|
+
message: error instanceof Error ? error.message : String(error)
|
|
55478
|
+
};
|
|
55479
|
+
}
|
|
55480
|
+
function errorCode(error) {
|
|
55481
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "STORE_MISSING") {
|
|
55482
|
+
return "STORE_MISSING";
|
|
55483
|
+
}
|
|
55484
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "STORE_INVALID") {
|
|
55485
|
+
return "STORE_INVALID";
|
|
55486
|
+
}
|
|
55487
|
+
return "STORE_UNREADABLE";
|
|
55488
|
+
}
|
|
55489
|
+
function discoverTaskRouteSources(input) {
|
|
55490
|
+
const include = normalizePatterns(input.include);
|
|
55491
|
+
const exclude = normalizePatterns(input.exclude);
|
|
55492
|
+
const sourceRoots = (input.sourceRoots ?? []).map(normalizePath5).sort();
|
|
55493
|
+
const sourceStores = (input.sourceStores ?? []).map(normalizePath5).sort();
|
|
55494
|
+
const limit = Number.isFinite(input.limit ?? NaN) && (input.limit ?? 0) >= 0 ? Math.floor(input.limit ?? 0) : null;
|
|
55495
|
+
const collected = collectStoreRefs(input);
|
|
55496
|
+
const stores = [];
|
|
55497
|
+
const errors2 = [...collected.errors];
|
|
55498
|
+
const candidates = [];
|
|
55499
|
+
let totalCandidateCount = 0;
|
|
55500
|
+
for (const ref of collected.stores) {
|
|
55501
|
+
if (!shouldIncludeStore(ref, include, exclude))
|
|
55502
|
+
continue;
|
|
55503
|
+
const storeErrors = [];
|
|
55504
|
+
let db = null;
|
|
55505
|
+
try {
|
|
55506
|
+
db = openReadonlyStore(ref);
|
|
55507
|
+
const readyTasks = listPendingTasksReadonly(db).filter((task2) => isReadyTask(task2, db));
|
|
55508
|
+
totalCandidateCount += readyTasks.length;
|
|
55509
|
+
const remaining = limit === null ? readyTasks.length : Math.max(0, limit - candidates.length);
|
|
55510
|
+
const selectedTasks = limit === null ? readyTasks : readyTasks.slice(0, remaining);
|
|
55511
|
+
candidates.push(...selectedTasks.map((task2) => sourceCandidate(ref, task2, db)));
|
|
55512
|
+
stores.push({
|
|
55513
|
+
...ref,
|
|
55514
|
+
status: "ok",
|
|
55515
|
+
candidate_count: readyTasks.length,
|
|
55516
|
+
returned_candidate_count: selectedTasks.length,
|
|
55517
|
+
errors: []
|
|
55518
|
+
});
|
|
55519
|
+
} catch (error) {
|
|
55520
|
+
const storeError = discoveryError(ref, errorCode(error), error);
|
|
55521
|
+
storeErrors.push(storeError);
|
|
55522
|
+
errors2.push(storeError);
|
|
55523
|
+
stores.push({
|
|
55524
|
+
...ref,
|
|
55525
|
+
status: storeError.code === "STORE_MISSING" ? "missing" : "error",
|
|
55526
|
+
candidate_count: 0,
|
|
55527
|
+
returned_candidate_count: 0,
|
|
55528
|
+
errors: storeErrors
|
|
55529
|
+
});
|
|
55530
|
+
} finally {
|
|
55531
|
+
db?.close();
|
|
55532
|
+
}
|
|
55533
|
+
}
|
|
55534
|
+
return {
|
|
55535
|
+
schema_version: TASK_ROUTE_SOURCE_DISCOVERY_SCHEMA_VERSION,
|
|
55536
|
+
sourceRoots,
|
|
55537
|
+
sourceStores,
|
|
55538
|
+
include,
|
|
55539
|
+
exclude,
|
|
55540
|
+
limit,
|
|
55541
|
+
total_candidate_count: totalCandidateCount,
|
|
55542
|
+
returned_candidate_count: candidates.length,
|
|
55543
|
+
truncated: limit !== null && totalCandidateCount > candidates.length,
|
|
55544
|
+
stores,
|
|
55545
|
+
candidates,
|
|
55546
|
+
errors: errors2
|
|
55547
|
+
};
|
|
55548
|
+
}
|
|
55549
|
+
var TASK_ROUTE_SOURCE_DISCOVERY_SCHEMA_VERSION = "todos.task_route_sources.v1", TODO_STORE_RELATIVE_PATH, ROOT_SCAN_MAX_DEPTH = 5, SKIPPED_SCAN_DIRS;
|
|
55550
|
+
var init_task_route_sources = __esm(() => {
|
|
55551
|
+
init_database();
|
|
55552
|
+
init_task_lifecycle();
|
|
55553
|
+
init_task_crud();
|
|
55554
|
+
init_redaction();
|
|
55555
|
+
init_task_routing();
|
|
55556
|
+
TODO_STORE_RELATIVE_PATH = join21(".hasna", "todos", "todos.db");
|
|
55557
|
+
SKIPPED_SCAN_DIRS = new Set([
|
|
55558
|
+
".git",
|
|
55559
|
+
".hg",
|
|
55560
|
+
".svn",
|
|
55561
|
+
"node_modules",
|
|
55562
|
+
"dist",
|
|
55563
|
+
"build",
|
|
55564
|
+
".next",
|
|
55565
|
+
".turbo",
|
|
55566
|
+
".cache"
|
|
55567
|
+
]);
|
|
55568
|
+
});
|
|
55569
|
+
|
|
55041
55570
|
// src/lib/tester-issue-reports.ts
|
|
55042
55571
|
var exports_tester_issue_reports = {};
|
|
55043
55572
|
__export(exports_tester_issue_reports, {
|
|
@@ -55050,7 +55579,7 @@ __export(exports_tester_issue_reports, {
|
|
|
55050
55579
|
TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION: () => TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
|
|
55051
55580
|
TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION: () => TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION
|
|
55052
55581
|
});
|
|
55053
|
-
import { createHash as
|
|
55582
|
+
import { createHash as createHash14 } from "crypto";
|
|
55054
55583
|
function asObject3(value) {
|
|
55055
55584
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
55056
55585
|
}
|
|
@@ -55222,7 +55751,7 @@ function fingerprintTesterIssueReport(report) {
|
|
|
55222
55751
|
normalizeText4(report.failure?.message || report.summary || report.title).slice(0, 240),
|
|
55223
55752
|
normalizeText4(stackTop).slice(0, 160)
|
|
55224
55753
|
].join("::");
|
|
55225
|
-
return `testers:${
|
|
55754
|
+
return `testers:${createHash14("sha256").update(raw).digest("hex").slice(0, 16)}`;
|
|
55226
55755
|
}
|
|
55227
55756
|
function priorityForSeverity(severity, fallback) {
|
|
55228
55757
|
return PRIORITIES5.includes(severity) ? severity : fallback;
|
|
@@ -55541,6 +56070,15 @@ function parseCsvOption(value) {
|
|
|
55541
56070
|
const values = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
55542
56071
|
return values.length > 0 ? values : undefined;
|
|
55543
56072
|
}
|
|
56073
|
+
function collectOption2(value, previous = []) {
|
|
56074
|
+
return [...previous, value];
|
|
56075
|
+
}
|
|
56076
|
+
function expandRepeatedCsvOption(value) {
|
|
56077
|
+
if (!value || value.length === 0)
|
|
56078
|
+
return;
|
|
56079
|
+
const values = value.flatMap((item) => item.split(",").map((part) => part.trim()).filter(Boolean));
|
|
56080
|
+
return values.length > 0 ? values : undefined;
|
|
56081
|
+
}
|
|
55544
56082
|
function resolveOptionalId(table, value) {
|
|
55545
56083
|
if (!value)
|
|
55546
56084
|
return;
|
|
@@ -56179,13 +56717,13 @@ Repairs`));
|
|
|
56179
56717
|
try {
|
|
56180
56718
|
const db = getDatabase();
|
|
56181
56719
|
const row = db.query("SELECT COUNT(*) as count FROM tasks").get();
|
|
56182
|
-
const { statSync:
|
|
56183
|
-
const { join:
|
|
56720
|
+
const { statSync: statSync10 } = await import("fs");
|
|
56721
|
+
const { join: join22 } = await import("path");
|
|
56184
56722
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
56185
|
-
const dbPath = process.env["HASNA_TODOS_DB_PATH"] || process.env["TODOS_DB_PATH"] ||
|
|
56723
|
+
const dbPath = process.env["HASNA_TODOS_DB_PATH"] || process.env["TODOS_DB_PATH"] || join22(home, ".hasna", "todos", "todos.db");
|
|
56186
56724
|
let size = "unknown";
|
|
56187
56725
|
try {
|
|
56188
|
-
size = `${(
|
|
56726
|
+
size = `${(statSync10(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
|
|
56189
56727
|
} catch {}
|
|
56190
56728
|
checks.push({ name: "Database", ok: true, message: `${row.count} tasks \xB7 ${size} \xB7 ${chalk7.dim(dbPath)}` });
|
|
56191
56729
|
} catch (e) {
|
|
@@ -56675,8 +57213,42 @@ Repairs`));
|
|
|
56675
57213
|
console.log(` ${chalk7.dim(time2)} ${chalk7.cyan(entry.source)} ${chalk7.dim(ref)} ${entry.event_type}${message}${agent}`);
|
|
56676
57214
|
}
|
|
56677
57215
|
});
|
|
56678
|
-
program2.command("ready").description("Show all tasks ready to be claimed (pending, unblocked, unlocked)").option("-j, --json", "Output as JSON").option("--project <id>", "Filter to project").option("--limit <n>", "Max tasks to show", "20").action(async (opts) => {
|
|
57216
|
+
program2.command("ready").description("Show all tasks ready to be claimed (pending, unblocked, unlocked)").option("-j, --json", "Output as JSON").option("--project <id>", "Filter to project").option("--limit <n>", "Max tasks to show", "20").option("--source-root <path>", "Read-only source root to scan for .hasna/todos/todos.db (repeatable)", collectOption2, []).option("--source-store <path>", "Read-only todos SQLite store path to scan (repeatable)", collectOption2, []).option("--include <pattern>", "Include source repo/store paths matching substring or glob (repeatable or comma-separated)", collectOption2, []).option("--exclude <pattern>", "Exclude source repo/store paths matching substring or glob (repeatable or comma-separated)", collectOption2, []).action(async (opts) => {
|
|
56679
57217
|
const globalOpts = program2.opts();
|
|
57218
|
+
const sourceRoots = expandRepeatedCsvOption(opts.sourceRoot);
|
|
57219
|
+
const sourceStores = expandRepeatedCsvOption(opts.sourceStore);
|
|
57220
|
+
const include = expandRepeatedCsvOption(opts.include);
|
|
57221
|
+
const exclude = expandRepeatedCsvOption(opts.exclude);
|
|
57222
|
+
if (sourceRoots || sourceStores || include || exclude) {
|
|
57223
|
+
const { discoverTaskRouteSources: discoverTaskRouteSources2 } = await Promise.resolve().then(() => (init_task_route_sources(), exports_task_route_sources));
|
|
57224
|
+
const result = discoverTaskRouteSources2({
|
|
57225
|
+
sourceRoots,
|
|
57226
|
+
sourceStores,
|
|
57227
|
+
include,
|
|
57228
|
+
exclude,
|
|
57229
|
+
limit: parseInt(opts.limit, 10)
|
|
57230
|
+
});
|
|
57231
|
+
if (opts.json || globalOpts.json) {
|
|
57232
|
+
console.log(JSON.stringify(result));
|
|
57233
|
+
return;
|
|
57234
|
+
}
|
|
57235
|
+
if (result.candidates.length === 0) {
|
|
57236
|
+
console.log(chalk7.dim(" No source tasks ready to claim."));
|
|
57237
|
+
} else {
|
|
57238
|
+
console.log(chalk7.bold(`Ready source tasks (${result.candidates.length}):
|
|
57239
|
+
`));
|
|
57240
|
+
for (const candidate of result.candidates) {
|
|
57241
|
+
const source3 = candidate.source_repo_path ?? candidate.source_db_path;
|
|
57242
|
+
const pri = candidate.priority === "critical" ? chalk7.bgRed.white(" CRIT ") : candidate.priority === "high" ? chalk7.red("[high]") : candidate.priority === "medium" ? chalk7.yellow("[med]") : "";
|
|
57243
|
+
console.log(` ${chalk7.cyan(candidate.task_short_id || candidate.task_id.slice(0, 8))} ${candidate.title} ${pri}${chalk7.dim(` ${source3}`)}`);
|
|
57244
|
+
}
|
|
57245
|
+
}
|
|
57246
|
+
if (result.errors.length > 0) {
|
|
57247
|
+
console.log(chalk7.yellow(`
|
|
57248
|
+
${result.errors.length} source error${result.errors.length === 1 ? "" : "s"} isolated; rerun with --json for details.`));
|
|
57249
|
+
}
|
|
57250
|
+
return;
|
|
57251
|
+
}
|
|
56680
57252
|
const db = getDatabase();
|
|
56681
57253
|
const { getBlockingDeps: getBlockingDeps2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
|
|
56682
57254
|
const { isLockExpired: isLockExpired2 } = await Promise.resolve().then(() => (init_database(), exports_database));
|
|
@@ -57998,21 +58570,21 @@ __export(exports_mcp_hooks_commands, {
|
|
|
57998
58570
|
});
|
|
57999
58571
|
import chalk8 from "chalk";
|
|
58000
58572
|
import { execSync as execSync3 } from "child_process";
|
|
58001
|
-
import { existsSync as
|
|
58002
|
-
import { dirname as
|
|
58573
|
+
import { existsSync as existsSync22, readFileSync as readFileSync17, writeFileSync as writeFileSync10, mkdirSync as mkdirSync11, chmodSync as chmodSync2 } from "fs";
|
|
58574
|
+
import { dirname as dirname13, join as join22 } from "path";
|
|
58003
58575
|
function getMcpBinaryPath() {
|
|
58004
58576
|
try {
|
|
58005
58577
|
const p = execSync3("which todos-mcp", { encoding: "utf-8" }).trim();
|
|
58006
58578
|
if (p)
|
|
58007
58579
|
return p;
|
|
58008
58580
|
} catch {}
|
|
58009
|
-
const bunBin =
|
|
58010
|
-
if (
|
|
58581
|
+
const bunBin = join22(HOME2, ".bun", "bin", "todos-mcp");
|
|
58582
|
+
if (existsSync22(bunBin))
|
|
58011
58583
|
return bunBin;
|
|
58012
58584
|
return "todos-mcp";
|
|
58013
58585
|
}
|
|
58014
58586
|
function readJsonFile2(path) {
|
|
58015
|
-
if (!
|
|
58587
|
+
if (!existsSync22(path))
|
|
58016
58588
|
return {};
|
|
58017
58589
|
try {
|
|
58018
58590
|
return JSON.parse(readFileSync17(path, "utf-8"));
|
|
@@ -58021,20 +58593,20 @@ function readJsonFile2(path) {
|
|
|
58021
58593
|
}
|
|
58022
58594
|
}
|
|
58023
58595
|
function writeJsonFile2(path, data) {
|
|
58024
|
-
const dir =
|
|
58025
|
-
if (!
|
|
58596
|
+
const dir = dirname13(path);
|
|
58597
|
+
if (!existsSync22(dir))
|
|
58026
58598
|
mkdirSync11(dir, { recursive: true });
|
|
58027
58599
|
writeFileSync10(path, JSON.stringify(data, null, 2) + `
|
|
58028
58600
|
`);
|
|
58029
58601
|
}
|
|
58030
58602
|
function readTomlFile(path) {
|
|
58031
|
-
if (!
|
|
58603
|
+
if (!existsSync22(path))
|
|
58032
58604
|
return "";
|
|
58033
58605
|
return readFileSync17(path, "utf-8");
|
|
58034
58606
|
}
|
|
58035
58607
|
function writeTomlFile(path, content) {
|
|
58036
|
-
const dir =
|
|
58037
|
-
if (!
|
|
58608
|
+
const dir = dirname13(path);
|
|
58609
|
+
if (!existsSync22(dir))
|
|
58038
58610
|
mkdirSync11(dir, { recursive: true });
|
|
58039
58611
|
writeFileSync10(path, content);
|
|
58040
58612
|
}
|
|
@@ -58100,7 +58672,7 @@ function unregisterClaude(_global) {
|
|
|
58100
58672
|
}
|
|
58101
58673
|
}
|
|
58102
58674
|
function registerCodex(binPath) {
|
|
58103
|
-
const configPath =
|
|
58675
|
+
const configPath = join22(HOME2, ".codex", "config.toml");
|
|
58104
58676
|
let content = readTomlFile(configPath);
|
|
58105
58677
|
content = removeTomlBlock(content, "mcp_servers.todos");
|
|
58106
58678
|
const block = `
|
|
@@ -58114,7 +58686,7 @@ args = []
|
|
|
58114
58686
|
console.log(chalk8.green(`Codex CLI: registered in ${configPath}`));
|
|
58115
58687
|
}
|
|
58116
58688
|
function unregisterCodex() {
|
|
58117
|
-
const configPath =
|
|
58689
|
+
const configPath = join22(HOME2, ".codex", "config.toml");
|
|
58118
58690
|
let content = readTomlFile(configPath);
|
|
58119
58691
|
if (!content.includes("[mcp_servers.todos]")) {
|
|
58120
58692
|
console.log(chalk8.dim(`Codex CLI: todos not found in ${configPath}`));
|
|
@@ -58126,7 +58698,7 @@ function unregisterCodex() {
|
|
|
58126
58698
|
console.log(chalk8.green(`Codex CLI: unregistered from ${configPath}`));
|
|
58127
58699
|
}
|
|
58128
58700
|
function registerGemini(binPath) {
|
|
58129
|
-
const configPath =
|
|
58701
|
+
const configPath = join22(HOME2, ".gemini", "settings.json");
|
|
58130
58702
|
const config = readJsonFile2(configPath);
|
|
58131
58703
|
if (!config["mcpServers"]) {
|
|
58132
58704
|
config["mcpServers"] = {};
|
|
@@ -58140,7 +58712,7 @@ function registerGemini(binPath) {
|
|
|
58140
58712
|
console.log(chalk8.green(`Gemini CLI: registered in ${configPath}`));
|
|
58141
58713
|
}
|
|
58142
58714
|
function unregisterGemini() {
|
|
58143
|
-
const configPath =
|
|
58715
|
+
const configPath = join22(HOME2, ".gemini", "settings.json");
|
|
58144
58716
|
const config = readJsonFile2(configPath);
|
|
58145
58717
|
const servers = config["mcpServers"];
|
|
58146
58718
|
if (!servers || !("todos" in servers)) {
|
|
@@ -58197,8 +58769,8 @@ function registerMcpHooksCommands(program2) {
|
|
|
58197
58769
|
if (p)
|
|
58198
58770
|
todosBin = p;
|
|
58199
58771
|
} catch {}
|
|
58200
|
-
const hooksDir =
|
|
58201
|
-
if (!
|
|
58772
|
+
const hooksDir = join22(process.cwd(), ".claude", "hooks");
|
|
58773
|
+
if (!existsSync22(hooksDir))
|
|
58202
58774
|
mkdirSync11(hooksDir, { recursive: true });
|
|
58203
58775
|
const hookScript = `#!/usr/bin/env bash
|
|
58204
58776
|
# Auto-generated by: todos hooks install
|
|
@@ -58223,11 +58795,11 @@ esac
|
|
|
58223
58795
|
|
|
58224
58796
|
exit 0
|
|
58225
58797
|
`;
|
|
58226
|
-
const hookPath =
|
|
58798
|
+
const hookPath = join22(hooksDir, "todos-sync.sh");
|
|
58227
58799
|
writeFileSync10(hookPath, hookScript);
|
|
58228
58800
|
execSync3(`chmod +x "${hookPath}"`);
|
|
58229
58801
|
console.log(chalk8.green(`Hook script created: ${hookPath}`));
|
|
58230
|
-
const settingsPath =
|
|
58802
|
+
const settingsPath = join22(process.cwd(), ".claude", "settings.json");
|
|
58231
58803
|
const settings = readJsonFile2(settingsPath);
|
|
58232
58804
|
if (!settings["hooks"]) {
|
|
58233
58805
|
settings["hooks"] = {};
|
|
@@ -59096,7 +59668,7 @@ Artifacts:`));
|
|
|
59096
59668
|
const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
|
|
59097
59669
|
const hookPath = `${gitDir}/hooks/post-commit`;
|
|
59098
59670
|
const marker = "# todos-auto-link";
|
|
59099
|
-
if (
|
|
59671
|
+
if (existsSync22(hookPath)) {
|
|
59100
59672
|
const existing = readFileSync17(hookPath, "utf-8");
|
|
59101
59673
|
if (existing.includes(marker)) {
|
|
59102
59674
|
console.log(chalk8.yellow("Hook already installed."));
|
|
@@ -59124,7 +59696,7 @@ $(dirname "$0")/../../scripts/post-commit-hook.sh
|
|
|
59124
59696
|
const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
|
|
59125
59697
|
const hookPath = `${gitDir}/hooks/post-commit`;
|
|
59126
59698
|
const marker = "# todos-auto-link";
|
|
59127
|
-
if (!
|
|
59699
|
+
if (!existsSync22(hookPath)) {
|
|
59128
59700
|
console.log(chalk8.dim("No post-commit hook found."));
|
|
59129
59701
|
return;
|
|
59130
59702
|
}
|
|
@@ -59310,7 +59882,7 @@ import chalk10 from "chalk";
|
|
|
59310
59882
|
import { execSync as execSync4 } from "child_process";
|
|
59311
59883
|
import { readFileSync as readFileSync18, unlinkSync as unlinkSync2, writeFileSync as writeFileSync11 } from "fs";
|
|
59312
59884
|
import { tmpdir as tmpdir4 } from "os";
|
|
59313
|
-
import { join as
|
|
59885
|
+
import { join as join23 } from "path";
|
|
59314
59886
|
function getOrCreateLocalMachineName() {
|
|
59315
59887
|
return process.env["TODOS_MACHINE_NAME"] || __require("os").hostname() || "unknown";
|
|
59316
59888
|
}
|
|
@@ -59348,7 +59920,7 @@ function remoteTempPath(sshAddress) {
|
|
|
59348
59920
|
}
|
|
59349
59921
|
function readRemoteBridgeBundle(sshAddress) {
|
|
59350
59922
|
const remotePath = remoteTempPath(sshAddress);
|
|
59351
|
-
const localPath =
|
|
59923
|
+
const localPath = join23(tmpdir4(), `todos-bridge-pull-${uuid()}.json`);
|
|
59352
59924
|
try {
|
|
59353
59925
|
runSsh(sshAddress, `todos export --format bridge --allow-plaintext-sensitive --output ${shellQuote(remotePath)}`, 120000);
|
|
59354
59926
|
scpFromRemote(sshAddress, remotePath, localPath);
|
|
@@ -59363,7 +59935,7 @@ function readRemoteBridgeBundle(sshAddress) {
|
|
|
59363
59935
|
}
|
|
59364
59936
|
}
|
|
59365
59937
|
function writeLocalBridgeBundle() {
|
|
59366
|
-
const localPath =
|
|
59938
|
+
const localPath = join23(tmpdir4(), `todos-bridge-push-${uuid()}.json`);
|
|
59367
59939
|
writeFileSync11(localPath, JSON.stringify(createLocalBridgeBundle(), null, 2));
|
|
59368
59940
|
return localPath;
|
|
59369
59941
|
}
|
|
@@ -60427,7 +60999,7 @@ __export(exports_onboarding_commands, {
|
|
|
60427
60999
|
registerOnboardingCommands: () => registerOnboardingCommands
|
|
60428
61000
|
});
|
|
60429
61001
|
import chalk17 from "chalk";
|
|
60430
|
-
import { resolve as
|
|
61002
|
+
import { resolve as resolve22 } from "path";
|
|
60431
61003
|
function registerOnboardingCommands(program2) {
|
|
60432
61004
|
program2.command("onboarding").alias("demo-fixtures").description("List, show, write, or import bundled local onboarding fixtures").option("--show <name>", "Show one fixture bridge bundle as JSON").option("--write <dir>", "Write all bundled fixture bridge bundles to a directory").option("--import <name>", "Dry-run or apply an onboarding fixture import").option("--apply", "Apply an onboarding fixture import. Defaults to dry-run.").option("--resolve-conflicts", "Safely merge existing local tasks while preserving divergent fields").action(async (opts) => {
|
|
60433
61005
|
const globalOpts = program2.opts();
|
|
@@ -60443,7 +61015,7 @@ function registerOnboardingCommands(program2) {
|
|
|
60443
61015
|
return;
|
|
60444
61016
|
}
|
|
60445
61017
|
if (opts.write) {
|
|
60446
|
-
const result = writeOnboardingFixtureFiles2(
|
|
61018
|
+
const result = writeOnboardingFixtureFiles2(resolve22(opts.write));
|
|
60447
61019
|
if (globalOpts.json) {
|
|
60448
61020
|
output(result, true);
|
|
60449
61021
|
return;
|
|
@@ -63892,7 +64464,7 @@ __export(exports_sdk_integration_fixtures, {
|
|
|
63892
64464
|
TODOS_SDK_INTEGRATION_FIXTURE_GENERATED_AT: () => TODOS_SDK_INTEGRATION_FIXTURE_GENERATED_AT
|
|
63893
64465
|
});
|
|
63894
64466
|
import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync12 } from "fs";
|
|
63895
|
-
import { join as
|
|
64467
|
+
import { join as join24 } from "path";
|
|
63896
64468
|
function source5(version) {
|
|
63897
64469
|
return {
|
|
63898
64470
|
packageName: "@hasna/todos",
|
|
@@ -63999,7 +64571,7 @@ function writeSdkIntegrationFixtures(directory, options = {}) {
|
|
|
63999
64571
|
];
|
|
64000
64572
|
const written = [];
|
|
64001
64573
|
for (const [name, payload] of files) {
|
|
64002
|
-
const file =
|
|
64574
|
+
const file = join24(directory, name);
|
|
64003
64575
|
writeFileSync12(file, `${JSON.stringify(payload, null, 2)}
|
|
64004
64576
|
`, "utf-8");
|
|
64005
64577
|
written.push(file);
|
|
@@ -64023,7 +64595,7 @@ __export(exports_sdk_fixture_commands, {
|
|
|
64023
64595
|
registerSdkFixtureCommands: () => registerSdkFixtureCommands
|
|
64024
64596
|
});
|
|
64025
64597
|
import chalk19 from "chalk";
|
|
64026
|
-
import { resolve as
|
|
64598
|
+
import { resolve as resolve23 } from "path";
|
|
64027
64599
|
function registerSdkFixtureCommands(program2) {
|
|
64028
64600
|
program2.command("sdk-fixtures").description("List, show, or write local SDK integration fixtures").option("--show", "Print the full fixture pack JSON").option("--write <dir>", "Write fixture pack, bridge fixture, contract snapshots, and example index to a directory").action(async (opts) => {
|
|
64029
64601
|
const globalOpts = program2.opts();
|
|
@@ -64034,7 +64606,7 @@ function registerSdkFixtureCommands(program2) {
|
|
|
64034
64606
|
writeSdkIntegrationFixtures: writeSdkIntegrationFixtures2
|
|
64035
64607
|
} = await Promise.resolve().then(() => (init_sdk_integration_fixtures(), exports_sdk_integration_fixtures));
|
|
64036
64608
|
if (opts.write) {
|
|
64037
|
-
const result = writeSdkIntegrationFixtures2(
|
|
64609
|
+
const result = writeSdkIntegrationFixtures2(resolve23(opts.write));
|
|
64038
64610
|
if (globalOpts.json) {
|
|
64039
64611
|
console.log(JSON.stringify(result));
|
|
64040
64612
|
return;
|
|
@@ -64856,7 +65428,7 @@ __export(exports_local_backup_commands, {
|
|
|
64856
65428
|
registerLocalBackupCommands: () => registerLocalBackupCommands
|
|
64857
65429
|
});
|
|
64858
65430
|
import chalk26 from "chalk";
|
|
64859
|
-
import { resolve as
|
|
65431
|
+
import { resolve as resolve24 } from "path";
|
|
64860
65432
|
function globalOptions6(program2) {
|
|
64861
65433
|
const command = program2;
|
|
64862
65434
|
return command.optsWithGlobals?.() ?? program2.opts();
|
|
@@ -64878,10 +65450,10 @@ function registerLocalBackupCommands(program2) {
|
|
|
64878
65450
|
const projectId = opts.projectId ?? autoProject(globalOpts);
|
|
64879
65451
|
const backupBundle = createLocalBackup2({
|
|
64880
65452
|
project_id: projectId,
|
|
64881
|
-
output_path: opts.output ?
|
|
65453
|
+
output_path: opts.output ? resolve24(opts.output) : undefined
|
|
64882
65454
|
});
|
|
64883
65455
|
const result = {
|
|
64884
|
-
output_path: opts.output ?
|
|
65456
|
+
output_path: opts.output ? resolve24(opts.output) : null,
|
|
64885
65457
|
backup: backupBundle
|
|
64886
65458
|
};
|
|
64887
65459
|
if (opts.json || globalOpts.json) {
|
|
@@ -66291,9 +66863,17 @@ async function updateProject2(id, input, store) {
|
|
|
66291
66863
|
}
|
|
66292
66864
|
async function createPlan2(input, store, context) {
|
|
66293
66865
|
const timestamp3 = new Date().toISOString();
|
|
66866
|
+
const projectId = input.project_id ?? context?.projectId ?? null;
|
|
66867
|
+
const slug = await resolvePostgresPlanSlug({
|
|
66868
|
+
name: input.name,
|
|
66869
|
+
slug: input.slug,
|
|
66870
|
+
projectId,
|
|
66871
|
+
store
|
|
66872
|
+
});
|
|
66294
66873
|
return store.upsert("plans", {
|
|
66295
66874
|
id: randomUUID3(),
|
|
66296
|
-
|
|
66875
|
+
slug,
|
|
66876
|
+
project_id: projectId,
|
|
66297
66877
|
task_list_id: input.task_list_id ?? context?.taskListId ?? null,
|
|
66298
66878
|
agent_id: input.agent_id ?? context?.agentId ?? null,
|
|
66299
66879
|
name: input.name,
|
|
@@ -66307,7 +66887,17 @@ async function createPlan2(input, store, context) {
|
|
|
66307
66887
|
}
|
|
66308
66888
|
async function updatePlan2(id, input, store) {
|
|
66309
66889
|
const plan = await requireRecord("plans", id, store);
|
|
66310
|
-
|
|
66890
|
+
const patch = definedPatch(input);
|
|
66891
|
+
if (input.slug !== undefined) {
|
|
66892
|
+
patch.slug = await resolvePostgresPlanSlug({
|
|
66893
|
+
name: plan.name,
|
|
66894
|
+
slug: input.slug,
|
|
66895
|
+
projectId: plan.project_id,
|
|
66896
|
+
store,
|
|
66897
|
+
excludeId: id
|
|
66898
|
+
});
|
|
66899
|
+
}
|
|
66900
|
+
return store.upsert("plans", { ...plan, ...patch, updated_at: new Date().toISOString() });
|
|
66311
66901
|
}
|
|
66312
66902
|
async function registerAgent2(input, store, context) {
|
|
66313
66903
|
const existing = (await store.list("agents")).find((agent2) => agent2.name === input.name && agent2.status !== "archived");
|
|
@@ -66560,8 +67150,38 @@ function matchesOne(value, expected) {
|
|
|
66560
67150
|
function priorityRank2(priority) {
|
|
66561
67151
|
return { critical: 0, high: 1, medium: 2, low: 3 }[priority];
|
|
66562
67152
|
}
|
|
67153
|
+
function slugifyRaw(value) {
|
|
67154
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
67155
|
+
}
|
|
66563
67156
|
function slugify2(value) {
|
|
66564
|
-
return value
|
|
67157
|
+
return slugifyRaw(value) || "todos";
|
|
67158
|
+
}
|
|
67159
|
+
function normalizePlanSlug2(value) {
|
|
67160
|
+
const slug = slugifyRaw(value);
|
|
67161
|
+
if (!slug)
|
|
67162
|
+
throw new Error("Invalid plan slug");
|
|
67163
|
+
return slug;
|
|
67164
|
+
}
|
|
67165
|
+
function planSlugBase4(value) {
|
|
67166
|
+
return slugifyRaw(value) || "plan";
|
|
67167
|
+
}
|
|
67168
|
+
async function resolvePostgresPlanSlug(options) {
|
|
67169
|
+
const plans = await options.store.list("plans");
|
|
67170
|
+
const used = new Set(plans.filter((plan) => plan.project_id === options.projectId && plan.id !== options.excludeId && plan.slug).map((plan) => plan.slug));
|
|
67171
|
+
if (options.slug !== undefined) {
|
|
67172
|
+
const slug = normalizePlanSlug2(options.slug);
|
|
67173
|
+
if (used.has(slug))
|
|
67174
|
+
throw new Error(`Plan slug already exists in this scope: ${slug}`);
|
|
67175
|
+
return slug;
|
|
67176
|
+
}
|
|
67177
|
+
const base = planSlugBase4(options.name);
|
|
67178
|
+
let candidate = base;
|
|
67179
|
+
let suffix = 2;
|
|
67180
|
+
while (used.has(candidate)) {
|
|
67181
|
+
candidate = `${base}-${suffix}`;
|
|
67182
|
+
suffix += 1;
|
|
67183
|
+
}
|
|
67184
|
+
return candidate;
|
|
66565
67185
|
}
|
|
66566
67186
|
function definedPatch(value) {
|
|
66567
67187
|
return Object.fromEntries(Object.entries(value).filter(([, entry2]) => entry2 !== undefined));
|
|
@@ -66637,7 +67257,7 @@ var init_factory = __esm(() => {
|
|
|
66637
67257
|
});
|
|
66638
67258
|
|
|
66639
67259
|
// src/storage/s3-artifacts.ts
|
|
66640
|
-
import { createHash as
|
|
67260
|
+
import { createHash as createHash15, createHmac as createHmac2 } from "crypto";
|
|
66641
67261
|
function createTodosS3ArtifactStore(options) {
|
|
66642
67262
|
const requestFetch = options.fetch ?? fetch;
|
|
66643
67263
|
const now4 = options.now ?? (() => new Date);
|
|
@@ -66809,7 +67429,7 @@ function toAmzDate(date) {
|
|
|
66809
67429
|
return date.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
66810
67430
|
}
|
|
66811
67431
|
function sha256Hex(value) {
|
|
66812
|
-
return
|
|
67432
|
+
return createHash15("sha256").update(value).digest("hex");
|
|
66813
67433
|
}
|
|
66814
67434
|
function hmac(key, value) {
|
|
66815
67435
|
return createHmac2("sha256", key).update(value).digest();
|