@hasna/todos 0.15.34 → 0.15.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cloud-router.d.ts +1 -1
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/project-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +255 -43
- package/dist/contracts.js +86 -4
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/projects.d.ts +23 -0
- package/dist/db/projects.d.ts.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/index.js +209 -39
- package/dist/json-contracts.d.ts.map +1 -1
- package/dist/lib/import-export-bridge.d.ts.map +1 -1
- package/dist/lib/onboarding-fixtures.d.ts.map +1 -1
- package/dist/mcp/index.js +221 -39
- package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
- package/dist/mcp.js +4 -1
- package/dist/project-registration/authority.d.ts.map +1 -1
- package/dist/project-registration/sqlite.d.ts.map +1 -1
- package/dist/project-registration.js +156 -17
- package/dist/registry.js +86 -4
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/v1.generated.d.ts +3 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +221 -39
- package/dist/server/openapi.d.ts +13 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage.js +128 -12
- package/dist/types/index.d.ts +5 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +4 -1
package/dist/cli/index.js
CHANGED
|
@@ -2123,7 +2123,7 @@ var package_default;
|
|
|
2123
2123
|
var init_package = __esm(() => {
|
|
2124
2124
|
package_default = {
|
|
2125
2125
|
name: "@hasna/todos",
|
|
2126
|
-
version: "0.15.
|
|
2126
|
+
version: "0.15.35",
|
|
2127
2127
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
2128
2128
|
type: "module",
|
|
2129
2129
|
main: "dist/index.js",
|
|
@@ -2198,6 +2198,9 @@ var init_package = __esm(() => {
|
|
|
2198
2198
|
"verify:release-review": "bun run scripts/verify-npm-release-agent-review.ts",
|
|
2199
2199
|
"verify:attested-container-candidate": "bun run scripts/attested-container-candidate.ts verify",
|
|
2200
2200
|
"test:attested-container-candidate": "bun test scripts/attested-container-candidate.test.ts",
|
|
2201
|
+
"emit:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts emit",
|
|
2202
|
+
"verify:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts verify",
|
|
2203
|
+
"test:attested-container-compatibility-vector": "bun test scripts/attested-container-compatibility-vector.test.ts",
|
|
2201
2204
|
"issue:release-review": "bun run scripts/issue-npm-release-agent-review.ts",
|
|
2202
2205
|
prepublishOnly: "bun run scripts/verify-public-release.ts --mode=publish",
|
|
2203
2206
|
postinstall: "mkdir -p $HOME/.hasna/todos $HOME/.hasna/todos/training 2>/dev/null || true"
|
|
@@ -5819,7 +5822,31 @@ function classifyRemoteRequestError(baseUrl, route, error) {
|
|
|
5819
5822
|
}
|
|
5820
5823
|
throw error;
|
|
5821
5824
|
}
|
|
5822
|
-
function
|
|
5825
|
+
function withBoundedRemoteRequest(options, requestTimeoutMs, run) {
|
|
5826
|
+
const controller = new AbortController;
|
|
5827
|
+
const onBaseAbort = () => controller.abort();
|
|
5828
|
+
if (options?.signal) {
|
|
5829
|
+
if (options.signal.aborted)
|
|
5830
|
+
controller.abort();
|
|
5831
|
+
else
|
|
5832
|
+
options.signal.addEventListener("abort", onBaseAbort, { once: true });
|
|
5833
|
+
}
|
|
5834
|
+
let timer;
|
|
5835
|
+
const deadline = new Promise((_, reject) => {
|
|
5836
|
+
timer = setTimeout(() => {
|
|
5837
|
+
controller.abort();
|
|
5838
|
+
reject(new DOMException(`Todos remote request exceeded the ${requestTimeoutMs}ms bounded request timeout`, "AbortError"));
|
|
5839
|
+
}, requestTimeoutMs);
|
|
5840
|
+
});
|
|
5841
|
+
const attempt = run({ ...options, signal: controller.signal });
|
|
5842
|
+
attempt.catch(() => {});
|
|
5843
|
+
return Promise.race([attempt, deadline]).finally(() => {
|
|
5844
|
+
if (timer)
|
|
5845
|
+
clearTimeout(timer);
|
|
5846
|
+
options?.signal?.removeEventListener("abort", onBaseAbort);
|
|
5847
|
+
});
|
|
5848
|
+
}
|
|
5849
|
+
function protectRemoteClient(client, requestTimeoutMs) {
|
|
5823
5850
|
const baseUrl = remoteAuthorityBase(client);
|
|
5824
5851
|
const protect = async (route, request) => {
|
|
5825
5852
|
try {
|
|
@@ -5828,25 +5855,26 @@ function protectRemoteClient(client) {
|
|
|
5828
5855
|
return classifyRemoteRequestError(baseUrl, route, error);
|
|
5829
5856
|
}
|
|
5830
5857
|
};
|
|
5858
|
+
const bounded = (options, run) => withBoundedRemoteRequest(options, requestTimeoutMs, run);
|
|
5831
5859
|
const transport = client.transport;
|
|
5832
5860
|
const protectedTransport = {
|
|
5833
5861
|
baseUrl: transport.baseUrl,
|
|
5834
|
-
request: (method, path, body, options) => protect(path, () => transport.request(method, path, body,
|
|
5835
|
-
get: (path, options) => protect(path, () => transport.get(path,
|
|
5836
|
-
post: (path, body, options) => protect(path, () => transport.post(path, body,
|
|
5837
|
-
put: (path, body, options) => protect(path, () => transport.put(path, body,
|
|
5838
|
-
patch: (path, body, options) => protect(path, () => transport.patch(path, body,
|
|
5839
|
-
del: (path, body, options) => protect(path, () => transport.del(path, body,
|
|
5862
|
+
request: (method, path, body, options) => protect(path, () => bounded(options, (opts) => transport.request(method, path, body, opts))),
|
|
5863
|
+
get: (path, options) => protect(path, () => bounded(options, (opts) => transport.get(path, opts))),
|
|
5864
|
+
post: (path, body, options) => protect(path, () => bounded(options, (opts) => transport.post(path, body, opts))),
|
|
5865
|
+
put: (path, body, options) => protect(path, () => bounded(options, (opts) => transport.put(path, body, opts))),
|
|
5866
|
+
patch: (path, body, options) => protect(path, () => bounded(options, (opts) => transport.patch(path, body, opts))),
|
|
5867
|
+
del: (path, body, options) => protect(path, () => bounded(options, (opts) => transport.del(path, body, opts)))
|
|
5840
5868
|
};
|
|
5841
5869
|
return {
|
|
5842
5870
|
name: client.name,
|
|
5843
5871
|
baseUrl: client.baseUrl,
|
|
5844
5872
|
transport: protectedTransport,
|
|
5845
|
-
list: (resource, options) => protect(`/${resource}`, () => client.list(resource,
|
|
5846
|
-
get: (resource, id, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => client.get(resource, id,
|
|
5847
|
-
create: (resource, body, options) => protect(`/${resource}`, () => client.create(resource, body,
|
|
5848
|
-
update: (resource, id, patch, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => client.update(resource, id, patch,
|
|
5849
|
-
delete: (resource, id, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => client.delete(resource, id,
|
|
5873
|
+
list: (resource, options) => protect(`/${resource}`, () => bounded(options, (opts) => client.list(resource, opts))),
|
|
5874
|
+
get: (resource, id, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => bounded(options, (opts) => client.get(resource, id, opts))),
|
|
5875
|
+
create: (resource, body, options) => protect(`/${resource}`, () => bounded(options, (opts) => client.create(resource, body, opts))),
|
|
5876
|
+
update: (resource, id, patch, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => bounded(options, (opts) => client.update(resource, id, patch, opts))),
|
|
5877
|
+
delete: (resource, id, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => bounded(options, (opts) => client.delete(resource, id, opts)))
|
|
5850
5878
|
};
|
|
5851
5879
|
}
|
|
5852
5880
|
function remoteAuthorityBase(client) {
|
|
@@ -5867,17 +5895,18 @@ async function requiredRemoteRoute(client, route, request, recognized404Codes =
|
|
|
5867
5895
|
throw error;
|
|
5868
5896
|
}
|
|
5869
5897
|
}
|
|
5870
|
-
function getTodosCloudClient(env = process.env) {
|
|
5898
|
+
function getTodosCloudClient(env = process.env, requestTimeoutMs = REMOTE_REQUEST_TIMEOUT_MS) {
|
|
5871
5899
|
if (requestedTransport(env) !== "http")
|
|
5872
5900
|
return null;
|
|
5873
5901
|
const resolved = resolveStorageClient("todos", requireTodosRemoteAuthorityEnv(env), {
|
|
5874
|
-
fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" })
|
|
5902
|
+
fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" }),
|
|
5903
|
+
timeoutMs: requestTimeoutMs
|
|
5875
5904
|
});
|
|
5876
5905
|
if (resolved.transport === "cloud-http")
|
|
5877
|
-
return protectRemoteClient(resolved.client);
|
|
5906
|
+
return protectRemoteClient(resolved.client, requestTimeoutMs);
|
|
5878
5907
|
const transportName = resolved.transport;
|
|
5879
5908
|
if (transportName === "http")
|
|
5880
|
-
return protectRemoteClient(resolved.client);
|
|
5909
|
+
return protectRemoteClient(resolved.client, requestTimeoutMs);
|
|
5881
5910
|
return null;
|
|
5882
5911
|
}
|
|
5883
5912
|
function isCloudRouting(env = process.env) {
|
|
@@ -7535,7 +7564,7 @@ async function cloudTimeline(client, options = {}) {
|
|
|
7535
7564
|
const limit = options.limit ?? 50;
|
|
7536
7565
|
return { entries: entries.slice(offset, offset + limit), total, limit, offset };
|
|
7537
7566
|
}
|
|
7538
|
-
var UUID_RE, COMPLETION_EVIDENCE_FIELDS, completionCapabilityCache, retryCapabilityCache, taskCreatorCapabilityCache, gitRefCapabilityCache, remoteCommandCapabilityCache, LEGACY_STORAGE_MODE_KEYS, PRIORITY_RANK, listTagsCapabilityCache, PLAN_COMPLETION_PROTECTED_FIELDS, RELATION_HYDRATION_CONCURRENCY = 6;
|
|
7567
|
+
var UUID_RE, COMPLETION_EVIDENCE_FIELDS, completionCapabilityCache, retryCapabilityCache, taskCreatorCapabilityCache, gitRefCapabilityCache, remoteCommandCapabilityCache, LEGACY_STORAGE_MODE_KEYS, REMOTE_REQUEST_TIMEOUT_MS = 1e4, PRIORITY_RANK, listTagsCapabilityCache, PLAN_COMPLETION_PROTECTED_FIELDS, RELATION_HYDRATION_CONCURRENCY = 6;
|
|
7539
7568
|
var init_cloud_router = __esm(() => {
|
|
7540
7569
|
init_types();
|
|
7541
7570
|
init_redaction();
|
|
@@ -10035,6 +10064,10 @@ var init_migrations = __esm(() => {
|
|
|
10035
10064
|
);
|
|
10036
10065
|
INSERT OR IGNORE INTO _migrations (id) VALUES (70);
|
|
10037
10066
|
COMMIT;
|
|
10067
|
+
`,
|
|
10068
|
+
`BEGIN;
|
|
10069
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (71);
|
|
10070
|
+
COMMIT;
|
|
10038
10071
|
`
|
|
10039
10072
|
];
|
|
10040
10073
|
});
|
|
@@ -10708,6 +10741,8 @@ function ensureSchema(db) {
|
|
|
10708
10741
|
ensureColumn("projects", "task_list_id", "TEXT");
|
|
10709
10742
|
ensureColumn("projects", "task_prefix", "TEXT");
|
|
10710
10743
|
ensureColumn("projects", "task_counter", "INTEGER NOT NULL DEFAULT 0");
|
|
10744
|
+
ensureColumn("projects", "parent_id", "TEXT REFERENCES projects(id) ON DELETE SET NULL");
|
|
10745
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_projects_parent_id ON projects(parent_id)");
|
|
10711
10746
|
ensureColumn("tasks", "plan_id", "TEXT REFERENCES plans(id) ON DELETE SET NULL");
|
|
10712
10747
|
ensureColumn("tasks", "task_list_id", "TEXT REFERENCES task_lists(id) ON DELETE SET NULL");
|
|
10713
10748
|
ensureColumn("tasks", "short_id", "TEXT");
|
|
@@ -12779,10 +12814,12 @@ __export(exports_projects, {
|
|
|
12779
12814
|
renameProject: () => renameProject,
|
|
12780
12815
|
removeProjectSource: () => removeProjectSource,
|
|
12781
12816
|
removeMachineLocalPath: () => removeMachineLocalPath,
|
|
12817
|
+
orderProjectsParentFirst: () => orderProjectsParentFirst,
|
|
12782
12818
|
nextTaskShortId: () => nextTaskShortId,
|
|
12783
12819
|
listProjects: () => listProjects,
|
|
12784
12820
|
listProjectSources: () => listProjectSources,
|
|
12785
12821
|
listMachineLocalPaths: () => listMachineLocalPaths,
|
|
12822
|
+
listChildProjects: () => listChildProjects,
|
|
12786
12823
|
getProjectWithSources: () => getProjectWithSources,
|
|
12787
12824
|
getProjectByPath: () => getProjectByPath,
|
|
12788
12825
|
getProject: () => getProject,
|
|
@@ -12790,6 +12827,7 @@ __export(exports_projects, {
|
|
|
12790
12827
|
ensureProject: () => ensureProject,
|
|
12791
12828
|
deleteProject: () => deleteProject,
|
|
12792
12829
|
createProject: () => createProject,
|
|
12830
|
+
assertNotProjectAncestor: () => assertNotProjectAncestor,
|
|
12793
12831
|
addProjectSource: () => addProjectSource
|
|
12794
12832
|
});
|
|
12795
12833
|
function slugify(name) {
|
|
@@ -12821,17 +12859,24 @@ function createProject(input, db) {
|
|
|
12821
12859
|
const id = uuid();
|
|
12822
12860
|
const timestamp2 = now();
|
|
12823
12861
|
const derivedSlug = slugify(input.name);
|
|
12824
|
-
const taskListId = input.task_list_id === undefined ?
|
|
12862
|
+
const taskListId = input.task_list_id === undefined ? derivedSlug : slugify(input.task_list_id);
|
|
12825
12863
|
if (!derivedSlug || !taskListId)
|
|
12826
12864
|
throw new Error("Project name and task-list slug must be non-empty");
|
|
12827
12865
|
const slugConflict = d.query("SELECT id FROM projects WHERE task_list_id = ? LIMIT 1").get(taskListId);
|
|
12828
12866
|
if (slugConflict || !claimCanonicalSlug("project", "global", taskListId, id, d)) {
|
|
12829
12867
|
throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${taskListId}" already exists`);
|
|
12830
12868
|
}
|
|
12869
|
+
const parentId = input.parent_id ?? null;
|
|
12870
|
+
if (parentId !== null) {
|
|
12871
|
+
const parent = getProject(parentId, d);
|
|
12872
|
+
if (!parent)
|
|
12873
|
+
throw new ProjectNotFoundError(parentId);
|
|
12874
|
+
assertNotProjectAncestor(id, parentId, d);
|
|
12875
|
+
}
|
|
12831
12876
|
const taskPrefix = input.task_prefix || generatePrefix(input.name, d);
|
|
12832
12877
|
const machineId = currentStorageMachineId(d);
|
|
12833
|
-
d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at, machine_id)
|
|
12834
|
-
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp2, timestamp2, machineId]);
|
|
12878
|
+
d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, parent_id, created_at, updated_at, machine_id)
|
|
12879
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, parentId, timestamp2, timestamp2, machineId]);
|
|
12835
12880
|
return getProject(id, d);
|
|
12836
12881
|
})();
|
|
12837
12882
|
}
|
|
@@ -12856,6 +12901,64 @@ function listProjects(db) {
|
|
|
12856
12901
|
const d = db || getDatabase();
|
|
12857
12902
|
return d.query("SELECT * FROM projects ORDER BY name").all();
|
|
12858
12903
|
}
|
|
12904
|
+
function listChildProjects(parentId, db) {
|
|
12905
|
+
const d = db || getDatabase();
|
|
12906
|
+
return d.query("SELECT * FROM projects WHERE parent_id = ? ORDER BY name").all(parentId);
|
|
12907
|
+
}
|
|
12908
|
+
function orderProjectsParentFirst(projects) {
|
|
12909
|
+
const projectId = (project) => {
|
|
12910
|
+
const id = project.id;
|
|
12911
|
+
return typeof id === "string" && id.length > 0 ? id : null;
|
|
12912
|
+
};
|
|
12913
|
+
const parentId = (project) => {
|
|
12914
|
+
const parent = project.parent_id;
|
|
12915
|
+
return parent == null ? null : String(parent);
|
|
12916
|
+
};
|
|
12917
|
+
const byId = new Set;
|
|
12918
|
+
for (const project of projects) {
|
|
12919
|
+
const id = projectId(project);
|
|
12920
|
+
if (id !== null)
|
|
12921
|
+
byId.add(id);
|
|
12922
|
+
}
|
|
12923
|
+
const ordered = [];
|
|
12924
|
+
const emitted = new Set;
|
|
12925
|
+
let remaining = [...projects];
|
|
12926
|
+
let progress = true;
|
|
12927
|
+
while (progress && remaining.length > 0) {
|
|
12928
|
+
progress = false;
|
|
12929
|
+
const deferred = [];
|
|
12930
|
+
for (const project of remaining) {
|
|
12931
|
+
const parent = parentId(project);
|
|
12932
|
+
if (parent === null || emitted.has(parent) || !byId.has(parent)) {
|
|
12933
|
+
ordered.push(project);
|
|
12934
|
+
const id = projectId(project);
|
|
12935
|
+
if (id !== null)
|
|
12936
|
+
emitted.add(id);
|
|
12937
|
+
progress = true;
|
|
12938
|
+
} else {
|
|
12939
|
+
deferred.push(project);
|
|
12940
|
+
}
|
|
12941
|
+
}
|
|
12942
|
+
remaining = deferred;
|
|
12943
|
+
}
|
|
12944
|
+
ordered.push(...remaining);
|
|
12945
|
+
return ordered;
|
|
12946
|
+
}
|
|
12947
|
+
function assertNotProjectAncestor(projectId, ancestorId, db) {
|
|
12948
|
+
const d = db || getDatabase();
|
|
12949
|
+
let cursor = ancestorId;
|
|
12950
|
+
const seen = new Set;
|
|
12951
|
+
while (cursor !== null) {
|
|
12952
|
+
if (cursor === projectId) {
|
|
12953
|
+
throw new ResourceConflictError("PROJECT_PARENT_CYCLE", `Project "${projectId}" cannot be placed under its own descendant`);
|
|
12954
|
+
}
|
|
12955
|
+
if (seen.has(cursor))
|
|
12956
|
+
break;
|
|
12957
|
+
seen.add(cursor);
|
|
12958
|
+
const row = d.query("SELECT parent_id FROM projects WHERE id = ?").get(cursor);
|
|
12959
|
+
cursor = row?.parent_id ?? null;
|
|
12960
|
+
}
|
|
12961
|
+
}
|
|
12859
12962
|
function updateProject(id, input, db) {
|
|
12860
12963
|
const d = db || getDatabase();
|
|
12861
12964
|
const project = getProject(id, d);
|
|
@@ -12878,6 +12981,16 @@ function updateProject(id, input, db) {
|
|
|
12878
12981
|
sets.push("path = ?");
|
|
12879
12982
|
params.push(input.path);
|
|
12880
12983
|
}
|
|
12984
|
+
if (input.parent_id !== undefined) {
|
|
12985
|
+
if (input.parent_id !== null) {
|
|
12986
|
+
const parent = getProject(input.parent_id, d);
|
|
12987
|
+
if (!parent)
|
|
12988
|
+
throw new ProjectNotFoundError(input.parent_id);
|
|
12989
|
+
assertNotProjectAncestor(id, input.parent_id, d);
|
|
12990
|
+
}
|
|
12991
|
+
sets.push("parent_id = ?");
|
|
12992
|
+
params.push(input.parent_id);
|
|
12993
|
+
}
|
|
12881
12994
|
params.push(id);
|
|
12882
12995
|
d.run(`UPDATE projects SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
12883
12996
|
return getProject(id, d);
|
|
@@ -23142,11 +23255,14 @@ function registerTaskCommands(program2) {
|
|
|
23142
23255
|
const narrowsAfterQuery = Boolean(opts.dueToday) || Boolean(opts.overdue) || creatorFilterActive && cloud || taskListFilterActive;
|
|
23143
23256
|
const withholdLimit = requestedLimit !== undefined && (reordersAfterQuery || narrowsAfterQuery);
|
|
23144
23257
|
const scanCeiling = cloud && (withholdLimit || requestedLimit === undefined) ? Math.max(requestedLimit ?? 0, listScanLimit()) : undefined;
|
|
23258
|
+
const probeLimit = !withholdLimit && requestedLimit !== undefined ? requestedLimit + 1 : undefined;
|
|
23145
23259
|
const serverFilter = (() => {
|
|
23146
23260
|
const base = withholdLimit ? (() => {
|
|
23147
23261
|
const { limit: _dropped, ...rest } = filter;
|
|
23148
23262
|
return rest;
|
|
23149
23263
|
})() : filter;
|
|
23264
|
+
if (probeLimit !== undefined)
|
|
23265
|
+
return { ...base, limit: probeLimit };
|
|
23150
23266
|
return scanCeiling === undefined ? base : { ...base, limit: scanCeiling };
|
|
23151
23267
|
})();
|
|
23152
23268
|
let tasks = cloud ? await cloudListTasks(cloud, serverFilter) : listTasks(serverFilter);
|
|
@@ -23195,8 +23311,21 @@ function registerTaskCommands(program2) {
|
|
|
23195
23311
|
return 0;
|
|
23196
23312
|
});
|
|
23197
23313
|
}
|
|
23198
|
-
|
|
23199
|
-
|
|
23314
|
+
let truncatedByLimit = false;
|
|
23315
|
+
if (requestedLimit !== undefined) {
|
|
23316
|
+
if (withholdLimit) {
|
|
23317
|
+
truncatedByLimit = tasks.length > requestedLimit;
|
|
23318
|
+
tasks = tasks.slice(0, requestedLimit);
|
|
23319
|
+
} else if (tasks.length > requestedLimit) {
|
|
23320
|
+
truncatedByLimit = true;
|
|
23321
|
+
tasks = tasks.slice(0, requestedLimit);
|
|
23322
|
+
}
|
|
23323
|
+
}
|
|
23324
|
+
if (truncatedByLimit) {
|
|
23325
|
+
console.error(chalk3.yellow(`Warning: the matching set has more than --limit ${requestedLimit} rows, so only the first
|
|
23326
|
+
` + ` ${requestedLimit} are shown. This read is bounded \u2014 raise --limit or narrow the
|
|
23327
|
+
` + ` query (--project, --status, --assigned) to see the full population.`));
|
|
23328
|
+
}
|
|
23200
23329
|
if (rosterPromise && assignedFilter) {
|
|
23201
23330
|
try {
|
|
23202
23331
|
const roster = await rosterPromise;
|
|
@@ -25296,7 +25425,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
25296
25425
|
}
|
|
25297
25426
|
}
|
|
25298
25427
|
};
|
|
25299
|
-
applyRows("projects", "projects", PROJECT_COLUMNS, snapshot.projects, "updated_at");
|
|
25428
|
+
applyRows("projects", "projects", PROJECT_COLUMNS, orderProjectsParentFirst(snapshot.projects), "updated_at");
|
|
25300
25429
|
applyRows("project_machine_paths", "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS, snapshot.projectMachinePaths ?? [], "updated_at");
|
|
25301
25430
|
applyRows("agents", "agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
|
|
25302
25431
|
applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
|
|
@@ -25527,6 +25656,7 @@ var init_sqlite_snapshot = __esm(() => {
|
|
|
25527
25656
|
"task_list_id",
|
|
25528
25657
|
"task_prefix",
|
|
25529
25658
|
"task_counter",
|
|
25659
|
+
"parent_id",
|
|
25530
25660
|
"created_at",
|
|
25531
25661
|
"updated_at",
|
|
25532
25662
|
"machine_id",
|
|
@@ -28531,7 +28661,7 @@ function addSourceOnce(projectId, type, name, uri, metadata, db) {
|
|
|
28531
28661
|
function bootstrapProject(options = {}, db) {
|
|
28532
28662
|
const d = db || getDatabase();
|
|
28533
28663
|
const discovery = discoverProjectWorkspace(options.path);
|
|
28534
|
-
const taskListSlug = options.taskListSlug ||
|
|
28664
|
+
const taskListSlug = options.taskListSlug || slugify(options.name || discovery.projectName);
|
|
28535
28665
|
if (options.dryRun) {
|
|
28536
28666
|
return {
|
|
28537
28667
|
dryRun: true,
|
|
@@ -30901,7 +31031,7 @@ function registerProjectCommands(program2) {
|
|
|
30901
31031
|
}
|
|
30902
31032
|
}
|
|
30903
31033
|
});
|
|
30904
|
-
program2.command("projects").description("List and manage projects").option("--add <path>", "Register a project by path").option("--show <project>", "Resolve and show a project").option("--update <project>", "Update a project's name, path, or description").option("--deregister <project>", "Deregister a project without deleting its tasks; refuses projects with incomplete tasks").option("--path-prefix <prefix>", "Require deregistered project path to start with this prefix").option("--dry-run", "Show what would change without modifying local state").option("--name <name>", "Project name (with --add)").option("--path <path>", "Project path (with --update)").option("--description <text>", "Project description (with --add or --update)").option("--task-list-id <id>", "Custom task list ID (with --add)").option("--ensure-task-list <project>", "Plan or apply creation of an existing project's declared task list").option("--rollback-task-list <project>", "Conditionally roll back a task list created by --ensure-task-list").option("--apply", "Apply --ensure-task-list or --rollback-task-list; ensure plans by default").option("--idempotency-key <key>", "Stable idempotency key for --ensure-task-list --apply").option("--receipt <id>", "Accepted ensure receipt for --rollback-task-list --apply").action(async (opts) => {
|
|
31034
|
+
program2.command("projects").description("List and manage projects").option("--add <path>", "Register a project by path").option("--show <project>", "Resolve and show a project").option("--update <project>", "Update a project's name, path, or description").option("--deregister <project>", "Deregister a project without deleting its tasks; refuses projects with incomplete tasks").option("--path-prefix <prefix>", "Require deregistered project path to start with this prefix").option("--dry-run", "Show what would change without modifying local state").option("--name <name>", "Project name (with --add)").option("--path <path>", "Project path (with --update)").option("--description <text>", "Project description (with --add or --update)").option("--task-list-id <id>", "Custom task list ID (with --add)").option("--parent <project>", "Parent project (id, path, task-list slug, or name) to create a sub-project under (with --add)").option("--ensure-task-list <project>", "Plan or apply creation of an existing project's declared task list").option("--rollback-task-list <project>", "Conditionally roll back a task list created by --ensure-task-list").option("--apply", "Apply --ensure-task-list or --rollback-task-list; ensure plans by default").option("--idempotency-key <key>", "Stable idempotency key for --ensure-task-list --apply").option("--receipt <id>", "Accepted ensure receipt for --rollback-task-list --apply").action(async (opts) => {
|
|
30905
31035
|
const globalOpts = program2.opts();
|
|
30906
31036
|
const cloud = getTodosCloudClient();
|
|
30907
31037
|
if (opts.ensureTaskList && opts.rollbackTaskList) {
|
|
@@ -31018,10 +31148,21 @@ function registerProjectCommands(program2) {
|
|
|
31018
31148
|
if (opts.add) {
|
|
31019
31149
|
const projectPath = resolve13(opts.add);
|
|
31020
31150
|
const name = opts.name || basename5(projectPath);
|
|
31151
|
+
let parentId;
|
|
31152
|
+
if (opts.parent !== undefined) {
|
|
31153
|
+
const parent = cloud ? await cloudResolveProject(cloud, opts.parent) : resolveExplicitProject(opts.parent);
|
|
31154
|
+
parentId = parent.id;
|
|
31155
|
+
}
|
|
31021
31156
|
const existing = cloud ? (await cloudListProjects(cloud)).find((project2) => project2.path === projectPath) : getProjectByPath(projectPath);
|
|
31022
31157
|
let project;
|
|
31023
31158
|
if (existing) {
|
|
31024
31159
|
project = existing;
|
|
31160
|
+
if (opts.parent !== undefined && existing.parent_id !== parentId) {
|
|
31161
|
+
if (cloud)
|
|
31162
|
+
project = await cloudUpdateProject(cloud, existing.id, { parent_id: parentId });
|
|
31163
|
+
else
|
|
31164
|
+
project = updateProject(existing.id, { parent_id: parentId });
|
|
31165
|
+
}
|
|
31025
31166
|
if (opts.taskListId) {
|
|
31026
31167
|
if (cloud && existing.task_list_id !== opts.taskListId) {
|
|
31027
31168
|
handleError(new Error("Remote project task-list slug changes require project-rename"));
|
|
@@ -31030,7 +31171,7 @@ function registerProjectCommands(program2) {
|
|
|
31030
31171
|
project = renameProject(existing.id, { new_slug: opts.taskListId }).project;
|
|
31031
31172
|
}
|
|
31032
31173
|
} else {
|
|
31033
|
-
const input = { name, path: projectPath, description: opts.description, task_list_id: opts.taskListId };
|
|
31174
|
+
const input = { name, path: projectPath, description: opts.description, task_list_id: opts.taskListId, parent_id: parentId };
|
|
31034
31175
|
project = cloud ? await cloudCreateProject(cloud, input) : createProject(input);
|
|
31035
31176
|
}
|
|
31036
31177
|
if (!cloud) {
|
|
@@ -31047,6 +31188,8 @@ function registerProjectCommands(program2) {
|
|
|
31047
31188
|
console.log(chalk5.green(`Project registered: ${project.name} (${project.path})`));
|
|
31048
31189
|
if (project.task_list_id)
|
|
31049
31190
|
console.log(chalk5.dim(` Task list: ${project.task_list_id}`));
|
|
31191
|
+
if (parentId)
|
|
31192
|
+
console.log(chalk5.dim(` Parent: ${parentId}`));
|
|
31050
31193
|
}
|
|
31051
31194
|
return;
|
|
31052
31195
|
}
|
|
@@ -32538,8 +32681,12 @@ class PostgresJsonRecordStore {
|
|
|
32538
32681
|
OR ($11::text <> $2
|
|
32539
32682
|
AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE object_id = $2)
|
|
32540
32683
|
AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE cycle))) AS parent_acyclic,
|
|
32541
|
-
(SELECT
|
|
32542
|
-
|
|
32684
|
+
(COALESCE((SELECT payload->>'plan_id' FROM locked_task), '')
|
|
32685
|
+
IS DISTINCT FROM COALESCE($3::jsonb->>'plan_id', '')) AS membership_changed,
|
|
32686
|
+
(NOT (COALESCE((SELECT payload->>'plan_id' FROM locked_task), '')
|
|
32687
|
+
IS DISTINCT FROM COALESCE($3::jsonb->>'plan_id', ''))
|
|
32688
|
+
OR $8::text IS NULL
|
|
32689
|
+
OR EXISTS (SELECT 1 FROM locked_plans WHERE object_id = $8)) AS target_plan_found,
|
|
32543
32690
|
(SELECT payload->>'project_id' FROM locked_plans WHERE object_id = $8) AS target_project_id
|
|
32544
32691
|
), guarded AS (
|
|
32545
32692
|
SELECT
|
|
@@ -32562,7 +32709,6 @@ class PostgresJsonRecordStore {
|
|
|
32562
32709
|
AND guarded.version_matches
|
|
32563
32710
|
AND guarded.parent_found
|
|
32564
32711
|
AND guarded.parent_acyclic
|
|
32565
|
-
AND guarded.all_plans_found
|
|
32566
32712
|
AND guarded.target_plan_found
|
|
32567
32713
|
AND NOT guarded.project_conflict
|
|
32568
32714
|
ON CONFLICT (service, object_type, object_id) DO UPDATE SET
|
|
@@ -32578,7 +32724,7 @@ class PostgresJsonRecordStore {
|
|
|
32578
32724
|
RETURNING payload
|
|
32579
32725
|
)
|
|
32580
32726
|
SELECT guarded.task_found, guarded.version_matches, guarded.parent_found, guarded.parent_acyclic,
|
|
32581
|
-
guarded.
|
|
32727
|
+
guarded.membership_changed, guarded.target_plan_found, guarded.project_conflict,
|
|
32582
32728
|
(SELECT payload FROM stored) AS payload,
|
|
32583
32729
|
(SELECT payload FROM locked_task) AS current_payload
|
|
32584
32730
|
FROM guarded`, [
|
|
@@ -32610,7 +32756,7 @@ class PostgresJsonRecordStore {
|
|
|
32610
32756
|
if (parentGuard && !row?.parent_acyclic && parentGuard.parentId) {
|
|
32611
32757
|
throw new ResourceConflictError("TASK_PARENT_CYCLE", `TASK_PARENT_CYCLE: assigning parent ${parentGuard.parentId} to task ${value.id} would create or retain a parent cycle`);
|
|
32612
32758
|
}
|
|
32613
|
-
if (!row?.
|
|
32759
|
+
if (!row?.target_plan_found) {
|
|
32614
32760
|
throw new PlanProjectLinkError("PLAN_PROJECT_LINK_PLAN_NOT_FOUND", `Plan membership changed through a missing plan: ${targetPlanId ?? planIds.join(", ")}`, { plan_ids: planIds, target_plan_id: targetPlanId });
|
|
32615
32761
|
}
|
|
32616
32762
|
if (row.project_conflict) {
|
|
@@ -33819,17 +33965,26 @@ async function getChangedSince(since, filters, store) {
|
|
|
33819
33965
|
async function createProject2(input, store, context) {
|
|
33820
33966
|
const timestamp2 = new Date().toISOString();
|
|
33821
33967
|
const derivedSlug = slugifyRaw(input.name);
|
|
33822
|
-
const taskListId = input.task_list_id === undefined ?
|
|
33968
|
+
const taskListId = input.task_list_id === undefined ? derivedSlug : slugifyRaw(input.task_list_id);
|
|
33823
33969
|
if (!derivedSlug || !taskListId)
|
|
33824
33970
|
throw new Error("Project name and task-list slug must be non-empty");
|
|
33971
|
+
const parentId = input.parent_id ?? null;
|
|
33972
|
+
const id = randomUUID4();
|
|
33973
|
+
if (parentId !== null) {
|
|
33974
|
+
const parent = await store.get("projects", parentId);
|
|
33975
|
+
if (!parent)
|
|
33976
|
+
throw new ProjectNotFoundError(parentId);
|
|
33977
|
+
await assertNotProjectAncestorPostgres(id, parentId, store);
|
|
33978
|
+
}
|
|
33825
33979
|
const project = {
|
|
33826
|
-
id
|
|
33980
|
+
id,
|
|
33827
33981
|
name: input.name,
|
|
33828
33982
|
path: input.path,
|
|
33829
33983
|
description: input.description ?? null,
|
|
33830
33984
|
task_list_id: taskListId,
|
|
33831
33985
|
task_prefix: input.task_prefix ?? await generateProjectPrefix(input.name, store),
|
|
33832
33986
|
task_counter: 0,
|
|
33987
|
+
parent_id: parentId,
|
|
33833
33988
|
created_at: timestamp2,
|
|
33834
33989
|
updated_at: timestamp2,
|
|
33835
33990
|
machine_id: store.machineId(context),
|
|
@@ -33837,12 +33992,38 @@ async function createProject2(input, store, context) {
|
|
|
33837
33992
|
};
|
|
33838
33993
|
return store.upsert("projects", project, context);
|
|
33839
33994
|
}
|
|
33995
|
+
async function assertNotProjectAncestorPostgres(projectId, candidateParentId, store) {
|
|
33996
|
+
const all = await store.list("projects");
|
|
33997
|
+
const byId = new Map(all.map((project) => [project.id, project.parent_id ?? null]));
|
|
33998
|
+
let cursor = candidateParentId;
|
|
33999
|
+
const seen = new Set;
|
|
34000
|
+
while (cursor !== null) {
|
|
34001
|
+
if (cursor === projectId) {
|
|
34002
|
+
throw new ResourceConflictError("PROJECT_PARENT_CYCLE", `Project "${projectId}" cannot be placed under its own descendant`);
|
|
34003
|
+
}
|
|
34004
|
+
if (seen.has(cursor))
|
|
34005
|
+
break;
|
|
34006
|
+
seen.add(cursor);
|
|
34007
|
+
cursor = byId.get(cursor) ?? null;
|
|
34008
|
+
}
|
|
34009
|
+
}
|
|
33840
34010
|
async function updateProject2(id, input, store) {
|
|
33841
34011
|
if ("task_list_id" in input) {
|
|
33842
34012
|
throw new Error("task_list_id cannot be changed by updateProject; use renameProject for an atomic canonical rename");
|
|
33843
34013
|
}
|
|
33844
34014
|
const project = await requireRecord("projects", id, store);
|
|
33845
|
-
|
|
34015
|
+
if (input.parent_id !== undefined && input.parent_id !== null) {
|
|
34016
|
+
const parent = await store.get("projects", input.parent_id);
|
|
34017
|
+
if (!parent)
|
|
34018
|
+
throw new ProjectNotFoundError(input.parent_id);
|
|
34019
|
+
await assertNotProjectAncestorPostgres(id, input.parent_id, store);
|
|
34020
|
+
}
|
|
34021
|
+
const updated = {
|
|
34022
|
+
...project,
|
|
34023
|
+
...definedPatch(input),
|
|
34024
|
+
...input.parent_id !== undefined ? { parent_id: input.parent_id } : {},
|
|
34025
|
+
updated_at: new Date().toISOString()
|
|
34026
|
+
};
|
|
33846
34027
|
return store.upsert("projects", updated);
|
|
33847
34028
|
}
|
|
33848
34029
|
async function createPlan2(input, store, context) {
|
|
@@ -35234,7 +35415,7 @@ class StagedSqliteTodosProjectRegistrationTransaction {
|
|
|
35234
35415
|
}
|
|
35235
35416
|
async createProject(input) {
|
|
35236
35417
|
const derivedSlug = normalizeSlug(input.name);
|
|
35237
|
-
const taskListId = input.task_list_id === undefined ?
|
|
35418
|
+
const taskListId = input.task_list_id === undefined ? derivedSlug : normalizeSlug(input.task_list_id);
|
|
35238
35419
|
if (!derivedSlug || !taskListId) {
|
|
35239
35420
|
throw new Error("Project name and task-list slug must be non-empty");
|
|
35240
35421
|
}
|
|
@@ -35246,6 +35427,7 @@ class StagedSqliteTodosProjectRegistrationTransaction {
|
|
|
35246
35427
|
task_list_id: taskListId,
|
|
35247
35428
|
task_prefix: input.task_prefix ?? this.availableProjectPrefix(input.name),
|
|
35248
35429
|
task_counter: 0,
|
|
35430
|
+
parent_id: input.parent_id ?? null,
|
|
35249
35431
|
created_at: now(),
|
|
35250
35432
|
updated_at: now(),
|
|
35251
35433
|
machine_id: currentStorageMachineId(this.db)
|
|
@@ -35256,14 +35438,15 @@ class StagedSqliteTodosProjectRegistrationTransaction {
|
|
|
35256
35438
|
try {
|
|
35257
35439
|
const result = this.db.run(`INSERT INTO projects (
|
|
35258
35440
|
id, name, path, description, task_list_id, task_prefix,
|
|
35259
|
-
task_counter, created_at, updated_at, machine_id
|
|
35260
|
-
) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [
|
|
35441
|
+
task_counter, parent_id, created_at, updated_at, machine_id
|
|
35442
|
+
) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?)`, [
|
|
35261
35443
|
project.id,
|
|
35262
35444
|
project.name,
|
|
35263
35445
|
project.path,
|
|
35264
35446
|
project.description,
|
|
35265
35447
|
project.task_list_id,
|
|
35266
35448
|
project.task_prefix,
|
|
35449
|
+
project.parent_id,
|
|
35267
35450
|
project.created_at,
|
|
35268
35451
|
project.updated_at,
|
|
35269
35452
|
project.machine_id ?? null
|
|
@@ -35660,7 +35843,10 @@ function taskListSlug(projectSlug) {
|
|
|
35660
35843
|
if (!slug || slug !== projectSlug) {
|
|
35661
35844
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "project_slug must be canonical kebab-case");
|
|
35662
35845
|
}
|
|
35663
|
-
return
|
|
35846
|
+
return slug;
|
|
35847
|
+
}
|
|
35848
|
+
function legacyTaskListSlug(projectSlug) {
|
|
35849
|
+
return `todos-${normalizeSlug(projectSlug)}`;
|
|
35664
35850
|
}
|
|
35665
35851
|
function deterministicTaskPrefix(projectSlug) {
|
|
35666
35852
|
const letters = projectSlug.replace(/[^a-z0-9]/gi, "").toUpperCase();
|
|
@@ -36179,6 +36365,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
36179
36365
|
created_by_operation: false
|
|
36180
36366
|
};
|
|
36181
36367
|
}
|
|
36368
|
+
if (request.bind_existing === true && conflict2.path === path && conflict2.task_list_id === legacyTaskListSlug(request.project_slug)) {
|
|
36369
|
+
return {
|
|
36370
|
+
record: boundExistingProjectRecord(conflict2),
|
|
36371
|
+
created_by_operation: false
|
|
36372
|
+
};
|
|
36373
|
+
}
|
|
36182
36374
|
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict2.id });
|
|
36183
36375
|
}
|
|
36184
36376
|
await this.fault("before_object_write", request);
|
|
@@ -36215,6 +36407,15 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
36215
36407
|
}
|
|
36216
36408
|
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict.id });
|
|
36217
36409
|
}
|
|
36410
|
+
if (request.bind_existing === true) {
|
|
36411
|
+
const legacy = await transaction.findTaskListConflict(todosProjectId, legacyTaskListSlug(request.project_slug));
|
|
36412
|
+
if (legacy && legacy.project_id === todosProjectId) {
|
|
36413
|
+
return {
|
|
36414
|
+
record: boundExistingTaskListRecord(legacy),
|
|
36415
|
+
created_by_operation: false
|
|
36416
|
+
};
|
|
36417
|
+
}
|
|
36418
|
+
}
|
|
36218
36419
|
await this.fault("before_object_write", request);
|
|
36219
36420
|
const taskList = await transaction.createTaskList({
|
|
36220
36421
|
name: request.project_name,
|
|
@@ -51904,7 +52105,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
51904
52105
|
path: { type: "string", minLength: 1 },
|
|
51905
52106
|
description: { type: "string" },
|
|
51906
52107
|
task_list_id: { type: "string", minLength: 1, pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
|
|
51907
|
-
task_prefix: { type: "string", minLength: 1 }
|
|
52108
|
+
task_prefix: { type: "string", minLength: 1 },
|
|
52109
|
+
parent_id: { type: "string", minLength: 1 }
|
|
51908
52110
|
}
|
|
51909
52111
|
},
|
|
51910
52112
|
UpdateProjectInput: {
|
|
@@ -51914,7 +52116,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
51914
52116
|
properties: {
|
|
51915
52117
|
name: { type: "string", minLength: 1 },
|
|
51916
52118
|
path: { type: "string", minLength: 1 },
|
|
51917
|
-
description: { type: "string", nullable: true }
|
|
52119
|
+
description: { type: "string", nullable: true },
|
|
52120
|
+
parent_id: { type: "string", minLength: 1, nullable: true }
|
|
51918
52121
|
}
|
|
51919
52122
|
},
|
|
51920
52123
|
RenameProjectInput: {
|
|
@@ -54213,6 +54416,7 @@ var init_openapi = __esm(() => {
|
|
|
54213
54416
|
task_list_id: { type: "string", nullable: true },
|
|
54214
54417
|
task_prefix: { type: "string", nullable: true },
|
|
54215
54418
|
task_counter: { type: "number" },
|
|
54419
|
+
parent_id: { type: "string", nullable: true },
|
|
54216
54420
|
created_at: { type: "string" },
|
|
54217
54421
|
updated_at: { type: "string" }
|
|
54218
54422
|
}
|
|
@@ -55283,7 +55487,7 @@ function validateProjectPatch(value) {
|
|
|
55283
55487
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
55284
55488
|
return { ok: false, message: "project patch must be an object" };
|
|
55285
55489
|
const body2 = value;
|
|
55286
|
-
const allowed = new Set(["name", "path", "description"]);
|
|
55490
|
+
const allowed = new Set(["name", "path", "description", "parent_id"]);
|
|
55287
55491
|
const unknown = Object.keys(body2).find((key2) => !allowed.has(key2));
|
|
55288
55492
|
if (unknown)
|
|
55289
55493
|
return { ok: false, message: `unknown project field: ${unknown}` };
|
|
@@ -55295,13 +55499,15 @@ function validateProjectPatch(value) {
|
|
|
55295
55499
|
return { ok: false, message: "path must be a non-empty string" };
|
|
55296
55500
|
if (body2["description"] !== undefined && body2["description"] !== null && typeof body2["description"] !== "string")
|
|
55297
55501
|
return { ok: false, message: "description must be a string or null" };
|
|
55502
|
+
if (body2["parent_id"] !== undefined && body2["parent_id"] !== null && (typeof body2["parent_id"] !== "string" || !body2["parent_id"].trim()))
|
|
55503
|
+
return { ok: false, message: "parent_id must be a string or null" };
|
|
55298
55504
|
return { ok: true, patch: body2 };
|
|
55299
55505
|
}
|
|
55300
55506
|
function validateProjectCreate(value) {
|
|
55301
55507
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
55302
55508
|
return { ok: false, message: "project body must be an object" };
|
|
55303
55509
|
const body2 = value;
|
|
55304
|
-
const allowed = new Set(["name", "path", "description", "task_list_id", "task_prefix"]);
|
|
55510
|
+
const allowed = new Set(["name", "path", "description", "task_list_id", "task_prefix", "parent_id"]);
|
|
55305
55511
|
const unknown = Object.keys(body2).find((key2) => !allowed.has(key2));
|
|
55306
55512
|
if (unknown)
|
|
55307
55513
|
return { ok: false, message: `unknown project field: ${unknown}` };
|
|
@@ -55319,6 +55525,9 @@ function validateProjectCreate(value) {
|
|
|
55319
55525
|
if (body2["task_prefix"] !== undefined && (typeof body2["task_prefix"] !== "string" || !body2["task_prefix"].trim())) {
|
|
55320
55526
|
return { ok: false, message: "task_prefix must be a non-empty string" };
|
|
55321
55527
|
}
|
|
55528
|
+
if (body2["parent_id"] !== undefined && (typeof body2["parent_id"] !== "string" || !body2["parent_id"].trim())) {
|
|
55529
|
+
return { ok: false, message: "parent_id must be a non-empty string" };
|
|
55530
|
+
}
|
|
55322
55531
|
return { ok: true, input: body2 };
|
|
55323
55532
|
}
|
|
55324
55533
|
function validatePlanCreate(value) {
|
|
@@ -62034,6 +62243,7 @@ function registerTaskProjectTools(server, ctx) {
|
|
|
62034
62243
|
name: exports_external.string().describe("Project name"),
|
|
62035
62244
|
path: exports_external.string().describe("Unique filesystem path for the project"),
|
|
62036
62245
|
description: exports_external.string().optional(),
|
|
62246
|
+
parent_id: exports_external.string().optional().describe("Optional parent project id to create this as a sub-project"),
|
|
62037
62247
|
status: exports_external.enum(["active", "completed", "on_hold", "archived"]).optional(),
|
|
62038
62248
|
short_id: exports_external.string().nullable().optional().describe("Short ID (auto-generated if omitted)"),
|
|
62039
62249
|
metadata: exports_external.record(exports_external.unknown()).optional()
|
|
@@ -69584,6 +69794,7 @@ function createAgentProjectDemoBundle() {
|
|
|
69584
69794
|
task_list_id: ids.list,
|
|
69585
69795
|
task_prefix: "DEMO",
|
|
69586
69796
|
task_counter: 4,
|
|
69797
|
+
parent_id: null,
|
|
69587
69798
|
created_at: createdAt,
|
|
69588
69799
|
updated_at: completedAt
|
|
69589
69800
|
});
|
|
@@ -89333,6 +89544,7 @@ var init_json_contracts = __esm(() => {
|
|
|
89333
89544
|
task_list_id: nullableIdField,
|
|
89334
89545
|
task_prefix: field(["string", "null"], "Optional task prefix.", true),
|
|
89335
89546
|
task_counter: field("integer", "Monotonic project task counter."),
|
|
89547
|
+
parent_id: field(["string", "null"], "Optional parent project id; null means top-level.", true),
|
|
89336
89548
|
created_at: isoDateField,
|
|
89337
89549
|
updated_at: isoDateField
|
|
89338
89550
|
},
|