@hasna/todos 0.11.70 → 0.11.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/plan-template-commands.d.ts.map +1 -1
- package/dist/cli/index.js +278 -49
- package/dist/contracts.js +135 -5
- 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.js +223 -20
- 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/mcp/index.js +138 -5
- package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
- package/dist/registry.js +135 -5
- package/dist/release-provenance.json +3 -3
- package/dist/server/index.js +138 -5
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/storage.js +153 -7
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plan-template-commands.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/plan-template-commands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"plan-template-commands.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/plan-template-commands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA8BzC,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,OAAO,QAqe5D"}
|
package/dist/cli/index.js
CHANGED
|
@@ -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)
|
|
@@ -10333,15 +10379,76 @@ var init_task_relations = __esm(() => {
|
|
|
10333
10379
|
});
|
|
10334
10380
|
|
|
10335
10381
|
// src/db/plans.ts
|
|
10382
|
+
function planSlugBase2(value) {
|
|
10383
|
+
return slugify(value) || "plan";
|
|
10384
|
+
}
|
|
10385
|
+
function normalizePlanSlug(value) {
|
|
10386
|
+
const slug = slugify(value);
|
|
10387
|
+
if (!slug)
|
|
10388
|
+
throw new Error("Invalid plan slug");
|
|
10389
|
+
return slug;
|
|
10390
|
+
}
|
|
10391
|
+
function plansBySlug(slug, db, projectId) {
|
|
10392
|
+
if (projectId !== undefined) {
|
|
10393
|
+
if (projectId === null) {
|
|
10394
|
+
return db.query("SELECT * FROM plans WHERE slug = ? AND project_id IS NULL ORDER BY created_at ASC, id ASC").all(slug);
|
|
10395
|
+
}
|
|
10396
|
+
return db.query("SELECT * FROM plans WHERE slug = ? AND project_id = ? ORDER BY created_at ASC, id ASC").all(slug, projectId);
|
|
10397
|
+
}
|
|
10398
|
+
return db.query("SELECT * FROM plans WHERE slug = ? ORDER BY created_at ASC, id ASC").all(slug);
|
|
10399
|
+
}
|
|
10400
|
+
function planSlugExists(slug, projectId, db, excludeId) {
|
|
10401
|
+
const rows = plansBySlug(slug, db, projectId);
|
|
10402
|
+
return rows.some((plan) => plan.id !== excludeId);
|
|
10403
|
+
}
|
|
10404
|
+
function nextPlanSlug(base, projectId, db, excludeId) {
|
|
10405
|
+
let candidate = base;
|
|
10406
|
+
let suffix = 2;
|
|
10407
|
+
while (planSlugExists(candidate, projectId, db, excludeId)) {
|
|
10408
|
+
candidate = `${base}-${suffix}`;
|
|
10409
|
+
suffix += 1;
|
|
10410
|
+
}
|
|
10411
|
+
return candidate;
|
|
10412
|
+
}
|
|
10413
|
+
function resolveCreateSlug(input, projectId, db) {
|
|
10414
|
+
if (input.slug !== undefined) {
|
|
10415
|
+
const slug = normalizePlanSlug(input.slug);
|
|
10416
|
+
if (planSlugExists(slug, projectId, db)) {
|
|
10417
|
+
throw new Error(`Plan slug already exists in this scope: ${slug}`);
|
|
10418
|
+
}
|
|
10419
|
+
return slug;
|
|
10420
|
+
}
|
|
10421
|
+
return nextPlanSlug(planSlugBase2(input.name), projectId, db);
|
|
10422
|
+
}
|
|
10423
|
+
function resolvePlanRefDetailed(ref, db, projectId) {
|
|
10424
|
+
const d = db || getDatabase();
|
|
10425
|
+
const byId = d.query("SELECT * FROM plans WHERE id = ? OR id LIKE ? ORDER BY id").all(ref, `${ref}%`);
|
|
10426
|
+
if (byId.length === 1)
|
|
10427
|
+
return { id: byId[0].id, reason: "id", matches: byId };
|
|
10428
|
+
if (byId.length > 1)
|
|
10429
|
+
return { id: null, reason: "ambiguous", matches: byId };
|
|
10430
|
+
const slug = slugify(ref);
|
|
10431
|
+
if (!slug)
|
|
10432
|
+
return { id: null, reason: "not_found", matches: [] };
|
|
10433
|
+
const bySlug = plansBySlug(slug, d, projectId);
|
|
10434
|
+
if (bySlug.length === 1)
|
|
10435
|
+
return { id: bySlug[0].id, reason: "slug", matches: bySlug };
|
|
10436
|
+
if (bySlug.length > 1)
|
|
10437
|
+
return { id: null, reason: "ambiguous", matches: bySlug };
|
|
10438
|
+
return { id: null, reason: "not_found", matches: [] };
|
|
10439
|
+
}
|
|
10336
10440
|
function createPlan(input, db) {
|
|
10337
10441
|
const d = db || getDatabase();
|
|
10338
10442
|
const id = uuid();
|
|
10339
10443
|
const timestamp = now();
|
|
10444
|
+
const projectId = input.project_id || null;
|
|
10445
|
+
const slug = resolveCreateSlug(input, projectId, d);
|
|
10340
10446
|
const machineId = currentStorageMachineId(d);
|
|
10341
|
-
d.run(`INSERT INTO plans (id, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at, machine_id)
|
|
10342
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
10447
|
+
d.run(`INSERT INTO plans (id, slug, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at, machine_id)
|
|
10448
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
10343
10449
|
id,
|
|
10344
|
-
|
|
10450
|
+
slug,
|
|
10451
|
+
projectId,
|
|
10345
10452
|
input.task_list_id || null,
|
|
10346
10453
|
input.agent_id || null,
|
|
10347
10454
|
input.name,
|
|
@@ -10376,6 +10483,14 @@ function updatePlan(id, input, db) {
|
|
|
10376
10483
|
sets.push("name = ?");
|
|
10377
10484
|
params.push(input.name);
|
|
10378
10485
|
}
|
|
10486
|
+
if (input.slug !== undefined) {
|
|
10487
|
+
const slug = normalizePlanSlug(input.slug);
|
|
10488
|
+
if (planSlugExists(slug, plan.project_id, d, id)) {
|
|
10489
|
+
throw new Error(`Plan slug already exists in this scope: ${slug}`);
|
|
10490
|
+
}
|
|
10491
|
+
sets.push("slug = ?");
|
|
10492
|
+
params.push(slug);
|
|
10493
|
+
}
|
|
10379
10494
|
if (input.description !== undefined) {
|
|
10380
10495
|
sets.push("description = ?");
|
|
10381
10496
|
params.push(input.description);
|
|
@@ -10420,6 +10535,7 @@ var init_plans = __esm(() => {
|
|
|
10420
10535
|
init_event_emission_safety();
|
|
10421
10536
|
init_event_hooks();
|
|
10422
10537
|
init_database();
|
|
10538
|
+
init_projects();
|
|
10423
10539
|
init_storage_tombstones();
|
|
10424
10540
|
});
|
|
10425
10541
|
|
|
@@ -13718,6 +13834,9 @@ function projectSlugMatches(project, ref) {
|
|
|
13718
13834
|
const normalized = slugify(ref);
|
|
13719
13835
|
return Boolean(normalized) && (project.task_list_id === normalized || slugify(project.name) === normalized);
|
|
13720
13836
|
}
|
|
13837
|
+
function planArtifactSlug(plan) {
|
|
13838
|
+
return slugify(plan.slug || plan.name) || "plan";
|
|
13839
|
+
}
|
|
13721
13840
|
function resolvePlanArtifactProject(input) {
|
|
13722
13841
|
const db = input.db || getDatabase();
|
|
13723
13842
|
const ref = input.project_id || input.project_ref;
|
|
@@ -13743,11 +13862,24 @@ function resolvePlanArtifactPaths(input) {
|
|
|
13743
13862
|
const projectRoot = resolve10(project.path);
|
|
13744
13863
|
const directory = join7(projectRoot, ".hasna", "todos", "plans", projectId);
|
|
13745
13864
|
const planId = input.plan_id ? assertSafePathSegment(input.plan_id, "plan id") : null;
|
|
13865
|
+
const planSlug = input.plan_slug ? assertSafePathSegment(slugify(input.plan_slug), "plan slug") : null;
|
|
13866
|
+
const fileName = planId ? planSlug ? `${planSlug}--${planId.slice(0, 8)}.md` : `${planId}.md` : null;
|
|
13746
13867
|
return {
|
|
13747
13868
|
project_id: project.id,
|
|
13748
13869
|
project_root: projectRoot,
|
|
13749
13870
|
directory,
|
|
13750
|
-
file_path:
|
|
13871
|
+
file_path: fileName ? join7(directory, fileName) : directory
|
|
13872
|
+
};
|
|
13873
|
+
}
|
|
13874
|
+
function resolvePlanArtifactCandidatePaths(plan, db) {
|
|
13875
|
+
return {
|
|
13876
|
+
primary: resolvePlanArtifactPaths({
|
|
13877
|
+
project_id: plan.project_id,
|
|
13878
|
+
plan_id: plan.id,
|
|
13879
|
+
plan_slug: planArtifactSlug(plan),
|
|
13880
|
+
db
|
|
13881
|
+
}),
|
|
13882
|
+
legacy: resolvePlanArtifactPaths({ project_id: plan.project_id, plan_id: plan.id, db })
|
|
13751
13883
|
};
|
|
13752
13884
|
}
|
|
13753
13885
|
function buildPlanArtifactSnapshot(plan, tasks = [], artifactUpdatedAt = new Date().toISOString()) {
|
|
@@ -13764,6 +13896,7 @@ function buildPlanArtifactSnapshot(plan, tasks = [], artifactUpdatedAt = new Dat
|
|
|
13764
13896
|
metadata: {
|
|
13765
13897
|
schema: PLAN_MARKDOWN_SCHEMA,
|
|
13766
13898
|
plan_id: plan.id,
|
|
13899
|
+
plan_slug: plan.slug ?? null,
|
|
13767
13900
|
project_id: plan.project_id,
|
|
13768
13901
|
task_list_id: plan.task_list_id ?? null,
|
|
13769
13902
|
agent_id: plan.agent_id ?? null,
|
|
@@ -13803,6 +13936,7 @@ function renderPlanArtifactMarkdown(snapshot) {
|
|
|
13803
13936
|
"---",
|
|
13804
13937
|
`schema: ${frontmatterScalar(metadata.schema)}`,
|
|
13805
13938
|
`plan_id: ${frontmatterScalar(metadata.plan_id)}`,
|
|
13939
|
+
`plan_slug: ${frontmatterScalar(metadata.plan_slug)}`,
|
|
13806
13940
|
`project_id: ${frontmatterScalar(metadata.project_id)}`,
|
|
13807
13941
|
`task_list_id: ${frontmatterScalar(metadata.task_list_id)}`,
|
|
13808
13942
|
`agent_id: ${frontmatterScalar(metadata.agent_id)}`,
|
|
@@ -13848,6 +13982,7 @@ function parsePlanArtifactMarkdown(markdown) {
|
|
|
13848
13982
|
metadata: {
|
|
13849
13983
|
schema: PLAN_MARKDOWN_SCHEMA,
|
|
13850
13984
|
plan_id: rawMetadata.plan_id,
|
|
13985
|
+
plan_slug: rawMetadata.plan_slug ?? null,
|
|
13851
13986
|
project_id: rawMetadata.project_id,
|
|
13852
13987
|
task_list_id: rawMetadata.task_list_id ?? null,
|
|
13853
13988
|
agent_id: rawMetadata.agent_id ?? null,
|
|
@@ -13888,7 +14023,7 @@ function writePlanArtifact(plan, db) {
|
|
|
13888
14023
|
return null;
|
|
13889
14024
|
const d = db || getDatabase();
|
|
13890
14025
|
const tasks = listTasks({ plan_id: plan.id, include_archived: true }, d);
|
|
13891
|
-
const paths =
|
|
14026
|
+
const paths = resolvePlanArtifactCandidatePaths(plan, d).primary;
|
|
13892
14027
|
const snapshot = buildPlanArtifactSnapshot(plan, tasks);
|
|
13893
14028
|
mkdirSync5(paths.directory, { recursive: true });
|
|
13894
14029
|
writeFileSync3(paths.file_path, renderPlanArtifactMarkdown(snapshot), "utf8");
|
|
@@ -13898,12 +14033,13 @@ function readPlanArtifact(plan, db) {
|
|
|
13898
14033
|
if (!plan.project_id)
|
|
13899
14034
|
return null;
|
|
13900
14035
|
const d = db || getDatabase();
|
|
13901
|
-
const paths =
|
|
13902
|
-
|
|
14036
|
+
const paths = resolvePlanArtifactCandidatePaths(plan, d);
|
|
14037
|
+
const path = existsSync8(paths.primary.file_path) ? paths.primary.file_path : existsSync8(paths.legacy.file_path) ? paths.legacy.file_path : null;
|
|
14038
|
+
if (!path)
|
|
13903
14039
|
return null;
|
|
13904
|
-
const markdown = readFileSync4(
|
|
14040
|
+
const markdown = readFileSync4(path, "utf8");
|
|
13905
14041
|
return {
|
|
13906
|
-
path
|
|
14042
|
+
path,
|
|
13907
14043
|
markdown,
|
|
13908
14044
|
...parsePlanArtifactMarkdown(markdown)
|
|
13909
14045
|
};
|
|
@@ -13912,10 +14048,11 @@ function inspectPlanArtifact(plan, db) {
|
|
|
13912
14048
|
if (!plan.project_id)
|
|
13913
14049
|
return null;
|
|
13914
14050
|
const d = db || getDatabase();
|
|
13915
|
-
const paths =
|
|
13916
|
-
|
|
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) {
|
|
13917
14054
|
return {
|
|
13918
|
-
path: paths.file_path,
|
|
14055
|
+
path: paths.primary.file_path,
|
|
13919
14056
|
exists: false,
|
|
13920
14057
|
parse_error: null,
|
|
13921
14058
|
metadata: null,
|
|
@@ -13924,9 +14061,9 @@ function inspectPlanArtifact(plan, db) {
|
|
|
13924
14061
|
};
|
|
13925
14062
|
}
|
|
13926
14063
|
try {
|
|
13927
|
-
const artifact = parsePlanArtifactMarkdown(readFileSync4(
|
|
14064
|
+
const artifact = parsePlanArtifactMarkdown(readFileSync4(path, "utf8"));
|
|
13928
14065
|
return {
|
|
13929
|
-
path
|
|
14066
|
+
path,
|
|
13930
14067
|
exists: true,
|
|
13931
14068
|
parse_error: null,
|
|
13932
14069
|
metadata: artifact.metadata,
|
|
@@ -13935,7 +14072,7 @@ function inspectPlanArtifact(plan, db) {
|
|
|
13935
14072
|
};
|
|
13936
14073
|
} catch (error) {
|
|
13937
14074
|
return {
|
|
13938
|
-
path
|
|
14075
|
+
path,
|
|
13939
14076
|
exists: true,
|
|
13940
14077
|
parse_error: error instanceof Error ? error.message : String(error),
|
|
13941
14078
|
metadata: null,
|
|
@@ -13947,6 +14084,9 @@ function inspectPlanArtifact(plan, db) {
|
|
|
13947
14084
|
function comparePlanArtifact(plan, artifact, tasks) {
|
|
13948
14085
|
const conflicts = [];
|
|
13949
14086
|
compare("plan_id", plan.id, artifact.metadata.plan_id, conflicts);
|
|
14087
|
+
if (artifact.metadata.plan_slug !== null) {
|
|
14088
|
+
compare("plan_slug", plan.slug ?? null, artifact.metadata.plan_slug, conflicts);
|
|
14089
|
+
}
|
|
13950
14090
|
compare("project_id", plan.project_id ?? null, artifact.metadata.project_id, conflicts);
|
|
13951
14091
|
compare("name", plan.name, artifact.metadata.name, conflicts);
|
|
13952
14092
|
compare("status", plan.status, artifact.metadata.status, conflicts);
|
|
@@ -14316,22 +14456,44 @@ __export(exports_plan_template_commands, {
|
|
|
14316
14456
|
registerPlanTemplateCommands: () => registerPlanTemplateCommands
|
|
14317
14457
|
});
|
|
14318
14458
|
import chalk3 from "chalk";
|
|
14459
|
+
function resolvePlanCliRef(ref, projectId) {
|
|
14460
|
+
const db = getDatabase();
|
|
14461
|
+
const resolved = resolvePlanRefDetailed(ref, db, projectId);
|
|
14462
|
+
if (resolved.id)
|
|
14463
|
+
return resolved.id;
|
|
14464
|
+
if (resolved.reason === "ambiguous") {
|
|
14465
|
+
console.error(chalk3.red(`Ambiguous plan reference: ${ref}`));
|
|
14466
|
+
if (resolved.matches.length > 0) {
|
|
14467
|
+
console.error(chalk3.dim(`Matches: ${resolved.matches.map((plan) => `${plan.slug ?? plan.name} (${plan.id.slice(0, 8)})`).join(", ")}`));
|
|
14468
|
+
}
|
|
14469
|
+
} else {
|
|
14470
|
+
console.error(chalk3.red(`Could not resolve plan ID or slug: ${ref}`));
|
|
14471
|
+
}
|
|
14472
|
+
process.exit(1);
|
|
14473
|
+
}
|
|
14319
14474
|
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) => {
|
|
14475
|
+
program2.command("plans").description("List and manage plans").option("--add <name>", "Create a plan").option("--slug <slug>", "Readable plan slug (with --add)").option("-d, --description <text>", "Plan description (with --add)").option("--show <id-or-slug>", "Show plan details with its tasks").option("--artifact <id-or-slug>", "Show local Markdown artifact diagnostics for a plan").option("--write-artifacts", "Write local Markdown artifacts for all project-scoped plans in scope").option("--delete <id>", "Delete a plan").option("--complete <id>", "Mark a plan as completed").action((opts) => {
|
|
14321
14476
|
const globalOpts = program2.opts();
|
|
14322
14477
|
const projectId = autoProject(globalOpts);
|
|
14323
14478
|
if (opts.add) {
|
|
14324
|
-
|
|
14325
|
-
|
|
14326
|
-
|
|
14327
|
-
|
|
14328
|
-
|
|
14479
|
+
let plan;
|
|
14480
|
+
try {
|
|
14481
|
+
plan = createPlan({
|
|
14482
|
+
name: opts.add,
|
|
14483
|
+
slug: opts.slug,
|
|
14484
|
+
description: opts.description,
|
|
14485
|
+
project_id: projectId
|
|
14486
|
+
});
|
|
14487
|
+
} catch (error) {
|
|
14488
|
+
handleError(error);
|
|
14489
|
+
}
|
|
14329
14490
|
const artifact = writePlanArtifact(plan);
|
|
14330
14491
|
if (globalOpts.json) {
|
|
14331
14492
|
output(plan, true);
|
|
14332
14493
|
} else {
|
|
14333
14494
|
console.log(chalk3.green("Plan created:"));
|
|
14334
14495
|
console.log(`${chalk3.dim(plan.id.slice(0, 8))} ${chalk3.bold(plan.name)} ${chalk3.cyan(`[${plan.status}]`)}`);
|
|
14496
|
+
console.log(`${chalk3.dim("Slug:")} ${plan.slug}`);
|
|
14335
14497
|
if (artifact)
|
|
14336
14498
|
console.log(`${chalk3.dim("Artifact:")} ${artifact.path}`);
|
|
14337
14499
|
}
|
|
@@ -14339,11 +14501,7 @@ function registerPlanTemplateCommands(program2) {
|
|
|
14339
14501
|
}
|
|
14340
14502
|
if (opts.artifact) {
|
|
14341
14503
|
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
|
-
}
|
|
14504
|
+
const resolvedId = resolvePlanCliRef(opts.artifact, projectId);
|
|
14347
14505
|
const plan = getPlan(resolvedId);
|
|
14348
14506
|
if (!plan) {
|
|
14349
14507
|
console.error(chalk3.red(`Plan not found: ${opts.artifact}`));
|
|
@@ -14396,11 +14554,7 @@ function registerPlanTemplateCommands(program2) {
|
|
|
14396
14554
|
}
|
|
14397
14555
|
if (opts.show) {
|
|
14398
14556
|
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
|
-
}
|
|
14557
|
+
const resolvedId = resolvePlanCliRef(opts.show, projectId);
|
|
14404
14558
|
const plan = getPlan(resolvedId);
|
|
14405
14559
|
if (!plan) {
|
|
14406
14560
|
console.error(chalk3.red(`Plan not found: ${opts.show}`));
|
|
@@ -14425,6 +14579,8 @@ function registerPlanTemplateCommands(program2) {
|
|
|
14425
14579
|
console.log(chalk3.bold(`Plan Details:
|
|
14426
14580
|
`));
|
|
14427
14581
|
console.log(` ${chalk3.dim("ID:")} ${plan.id}`);
|
|
14582
|
+
if (plan.slug)
|
|
14583
|
+
console.log(` ${chalk3.dim("Slug:")} ${plan.slug}`);
|
|
14428
14584
|
console.log(` ${chalk3.dim("Name:")} ${plan.name}`);
|
|
14429
14585
|
console.log(` ${chalk3.dim("Status:")} ${chalk3.cyan(plan.status)}`);
|
|
14430
14586
|
if (plan.description)
|
|
@@ -14447,12 +14603,7 @@ function registerPlanTemplateCommands(program2) {
|
|
|
14447
14603
|
return;
|
|
14448
14604
|
}
|
|
14449
14605
|
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
|
-
}
|
|
14606
|
+
const resolvedId = resolvePlanCliRef(opts.delete, projectId);
|
|
14456
14607
|
const deleted = deletePlan(resolvedId);
|
|
14457
14608
|
if (globalOpts.json) {
|
|
14458
14609
|
output({ deleted }, true);
|
|
@@ -14465,12 +14616,7 @@ function registerPlanTemplateCommands(program2) {
|
|
|
14465
14616
|
return;
|
|
14466
14617
|
}
|
|
14467
14618
|
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
|
-
}
|
|
14619
|
+
const resolvedId = resolvePlanCliRef(opts.complete, projectId);
|
|
14474
14620
|
try {
|
|
14475
14621
|
const plan = updatePlan(resolvedId, { status: "completed" });
|
|
14476
14622
|
const artifact = writePlanArtifact(plan);
|
|
@@ -14500,7 +14646,8 @@ function registerPlanTemplateCommands(program2) {
|
|
|
14500
14646
|
`));
|
|
14501
14647
|
for (const p of plans) {
|
|
14502
14648
|
const desc = p.description ? chalk3.dim(` - ${p.description}`) : "";
|
|
14503
|
-
|
|
14649
|
+
const slug = p.slug ? chalk3.dim(` ${p.slug}`) : "";
|
|
14650
|
+
console.log(`${chalk3.dim(p.id.slice(0, 8))}${slug} ${chalk3.bold(p.name)} ${chalk3.cyan(`[${p.status}]`)}${desc}`);
|
|
14504
14651
|
}
|
|
14505
14652
|
});
|
|
14506
14653
|
program2.command("templates").description("List and manage task templates").option("--add <name>", "Create a template").option("--title <pattern>", "Title pattern (with --add)").option("-d, --description <text>", "Default description").option("-p, --priority <level>", "Default priority").option("-t, --tags <tags>", "Default tags (comma-separated)").option("--delete <id>", "Delete a template").option("--update <id>", "Update a template").option("--use <id>", "Create a task from a template").option("--var <vars...>", "Variable substitutions: key=value (e.g. --var feature=login)").action(async (opts) => {
|
|
@@ -22074,6 +22221,36 @@ function prepareValue(column, value) {
|
|
|
22074
22221
|
return JSON.stringify(value ?? (column === "tags" || column === "files_changed" ? [] : {}));
|
|
22075
22222
|
return value === undefined ? null : value;
|
|
22076
22223
|
}
|
|
22224
|
+
function slugifyPlanValue(value) {
|
|
22225
|
+
return typeof value === "string" ? value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") : "";
|
|
22226
|
+
}
|
|
22227
|
+
function planSlugBase3(plan) {
|
|
22228
|
+
return slugifyPlanValue(plan.slug) || slugifyPlanValue(plan.name) || "plan";
|
|
22229
|
+
}
|
|
22230
|
+
function planSlugScope(projectId) {
|
|
22231
|
+
return typeof projectId === "string" && projectId ? projectId : "__global__";
|
|
22232
|
+
}
|
|
22233
|
+
function planSlugKey(projectId, slug) {
|
|
22234
|
+
return `${planSlugScope(projectId)}:${slug}`;
|
|
22235
|
+
}
|
|
22236
|
+
function normalizeBridgePlanSlugs(plans, db) {
|
|
22237
|
+
const existingRows = db.query("SELECT id, project_id, slug FROM plans WHERE slug IS NOT NULL").all();
|
|
22238
|
+
const existingIds = new Set(existingRows.map((row) => row.id));
|
|
22239
|
+
const used = new Set(existingRows.filter((row) => row.slug).map((row) => planSlugKey(row.project_id, row.slug)));
|
|
22240
|
+
return plans.map((plan) => {
|
|
22241
|
+
if (existingIds.has(plan.id))
|
|
22242
|
+
return plan;
|
|
22243
|
+
const base = planSlugBase3(plan);
|
|
22244
|
+
let candidate = base;
|
|
22245
|
+
let suffix = 2;
|
|
22246
|
+
while (used.has(planSlugKey(plan.project_id, candidate))) {
|
|
22247
|
+
candidate = `${base}-${suffix}`;
|
|
22248
|
+
suffix += 1;
|
|
22249
|
+
}
|
|
22250
|
+
used.add(planSlugKey(plan.project_id, candidate));
|
|
22251
|
+
return { ...plan, slug: candidate };
|
|
22252
|
+
});
|
|
22253
|
+
}
|
|
22077
22254
|
function insertRecord(db, tableKey, row) {
|
|
22078
22255
|
const table = tableByKey[tableKey];
|
|
22079
22256
|
const columns = insertColumns[tableKey];
|
|
@@ -22210,6 +22387,7 @@ function importLocalBridgeBundle(bundle, options = {}, db) {
|
|
|
22210
22387
|
const conflictStrategy = options.conflictStrategy ?? "skip";
|
|
22211
22388
|
const data = {
|
|
22212
22389
|
...bundle.data,
|
|
22390
|
+
plans: normalizeBridgePlanSlugs(bundle.data.plans, d),
|
|
22213
22391
|
tasks: sortedTasks(bundle.data.tasks),
|
|
22214
22392
|
saved_views: bundle.data.saved_views ?? [],
|
|
22215
22393
|
task_boards: bundle.data.task_boards ?? [],
|
|
@@ -22294,7 +22472,7 @@ var init_local_bridge = __esm(() => {
|
|
|
22294
22472
|
insertColumns = {
|
|
22295
22473
|
projects: ["id", "name", "path", "description", "task_list_id", "task_prefix", "task_counter", "created_at", "updated_at", "machine_id", "synced_at"],
|
|
22296
22474
|
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"],
|
|
22475
|
+
plans: ["id", "slug", "project_id", "task_list_id", "agent_id", "name", "description", "status", "created_at", "updated_at", "machine_id", "synced_at"],
|
|
22298
22476
|
tasks: [
|
|
22299
22477
|
"id",
|
|
22300
22478
|
"short_id",
|
|
@@ -28389,6 +28567,7 @@ async function handleCreatePlan(req, _ctx, json2) {
|
|
|
28389
28567
|
return json2({ error: "Missing 'name'" }, 400);
|
|
28390
28568
|
const plan = createPlan({
|
|
28391
28569
|
name: body.name,
|
|
28570
|
+
slug: body.slug,
|
|
28392
28571
|
description: body.description,
|
|
28393
28572
|
project_id: body.project_id,
|
|
28394
28573
|
task_list_id: body.task_list_id,
|
|
@@ -37738,6 +37917,7 @@ Tasks:` : null,
|
|
|
37738
37917
|
if (shouldRegisterTool("create_plan")) {
|
|
37739
37918
|
server.tool("create_plan", "Create a new plan (sprint/milestone).", {
|
|
37740
37919
|
name: exports_external2.string().describe("Plan name"),
|
|
37920
|
+
slug: exports_external2.string().optional().describe("Readable plan slug"),
|
|
37741
37921
|
project_id: exports_external2.string().optional().describe("Project ID"),
|
|
37742
37922
|
description: exports_external2.string().optional(),
|
|
37743
37923
|
start_date: exports_external2.string().optional().describe("ISO date"),
|
|
@@ -44898,6 +45078,7 @@ function createAgentProjectDemoBundle() {
|
|
|
44898
45078
|
});
|
|
44899
45079
|
data.plans.push({
|
|
44900
45080
|
id: ids.plan,
|
|
45081
|
+
slug: "ship-local-demo-workflow",
|
|
44901
45082
|
project_id: ids.project,
|
|
44902
45083
|
task_list_id: ids.list,
|
|
44903
45084
|
agent_id: "demo-agent",
|
|
@@ -66291,9 +66472,17 @@ async function updateProject2(id, input, store) {
|
|
|
66291
66472
|
}
|
|
66292
66473
|
async function createPlan2(input, store, context) {
|
|
66293
66474
|
const timestamp3 = new Date().toISOString();
|
|
66475
|
+
const projectId = input.project_id ?? context?.projectId ?? null;
|
|
66476
|
+
const slug = await resolvePostgresPlanSlug({
|
|
66477
|
+
name: input.name,
|
|
66478
|
+
slug: input.slug,
|
|
66479
|
+
projectId,
|
|
66480
|
+
store
|
|
66481
|
+
});
|
|
66294
66482
|
return store.upsert("plans", {
|
|
66295
66483
|
id: randomUUID3(),
|
|
66296
|
-
|
|
66484
|
+
slug,
|
|
66485
|
+
project_id: projectId,
|
|
66297
66486
|
task_list_id: input.task_list_id ?? context?.taskListId ?? null,
|
|
66298
66487
|
agent_id: input.agent_id ?? context?.agentId ?? null,
|
|
66299
66488
|
name: input.name,
|
|
@@ -66307,7 +66496,17 @@ async function createPlan2(input, store, context) {
|
|
|
66307
66496
|
}
|
|
66308
66497
|
async function updatePlan2(id, input, store) {
|
|
66309
66498
|
const plan = await requireRecord("plans", id, store);
|
|
66310
|
-
|
|
66499
|
+
const patch = definedPatch(input);
|
|
66500
|
+
if (input.slug !== undefined) {
|
|
66501
|
+
patch.slug = await resolvePostgresPlanSlug({
|
|
66502
|
+
name: plan.name,
|
|
66503
|
+
slug: input.slug,
|
|
66504
|
+
projectId: plan.project_id,
|
|
66505
|
+
store,
|
|
66506
|
+
excludeId: id
|
|
66507
|
+
});
|
|
66508
|
+
}
|
|
66509
|
+
return store.upsert("plans", { ...plan, ...patch, updated_at: new Date().toISOString() });
|
|
66311
66510
|
}
|
|
66312
66511
|
async function registerAgent2(input, store, context) {
|
|
66313
66512
|
const existing = (await store.list("agents")).find((agent2) => agent2.name === input.name && agent2.status !== "archived");
|
|
@@ -66560,8 +66759,38 @@ function matchesOne(value, expected) {
|
|
|
66560
66759
|
function priorityRank2(priority) {
|
|
66561
66760
|
return { critical: 0, high: 1, medium: 2, low: 3 }[priority];
|
|
66562
66761
|
}
|
|
66762
|
+
function slugifyRaw(value) {
|
|
66763
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
66764
|
+
}
|
|
66563
66765
|
function slugify2(value) {
|
|
66564
|
-
return value
|
|
66766
|
+
return slugifyRaw(value) || "todos";
|
|
66767
|
+
}
|
|
66768
|
+
function normalizePlanSlug2(value) {
|
|
66769
|
+
const slug = slugifyRaw(value);
|
|
66770
|
+
if (!slug)
|
|
66771
|
+
throw new Error("Invalid plan slug");
|
|
66772
|
+
return slug;
|
|
66773
|
+
}
|
|
66774
|
+
function planSlugBase4(value) {
|
|
66775
|
+
return slugifyRaw(value) || "plan";
|
|
66776
|
+
}
|
|
66777
|
+
async function resolvePostgresPlanSlug(options) {
|
|
66778
|
+
const plans = await options.store.list("plans");
|
|
66779
|
+
const used = new Set(plans.filter((plan) => plan.project_id === options.projectId && plan.id !== options.excludeId && plan.slug).map((plan) => plan.slug));
|
|
66780
|
+
if (options.slug !== undefined) {
|
|
66781
|
+
const slug = normalizePlanSlug2(options.slug);
|
|
66782
|
+
if (used.has(slug))
|
|
66783
|
+
throw new Error(`Plan slug already exists in this scope: ${slug}`);
|
|
66784
|
+
return slug;
|
|
66785
|
+
}
|
|
66786
|
+
const base = planSlugBase4(options.name);
|
|
66787
|
+
let candidate = base;
|
|
66788
|
+
let suffix = 2;
|
|
66789
|
+
while (used.has(candidate)) {
|
|
66790
|
+
candidate = `${base}-${suffix}`;
|
|
66791
|
+
suffix += 1;
|
|
66792
|
+
}
|
|
66793
|
+
return candidate;
|
|
66565
66794
|
}
|
|
66566
66795
|
function definedPatch(value) {
|
|
66567
66796
|
return Object.fromEntries(Object.entries(value).filter(([, entry2]) => entry2 !== undefined));
|