@hasna/todos 0.16.0 → 0.17.0
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/CHANGELOG.md +109 -0
- package/README.md +59 -22
- package/dist/cli/cloud-router.d.ts +63 -21
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
- package/dist/cli/commands/template-commands.d.ts.map +1 -1
- package/dist/cli/index.js +2591 -503
- package/dist/cli/stage-a.d.ts +2 -12
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/cli/template-remote.d.ts +90 -0
- package/dist/cli/template-remote.d.ts.map +1 -0
- package/dist/cli-mcp-parity.d.ts.map +1 -1
- package/dist/contracts.js +77 -30
- package/dist/db/agents.d.ts +8 -0
- package/dist/db/agents.d.ts.map +1 -1
- package/dist/db/database.d.ts +6 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/identity-mapping.d.ts +25 -0
- package/dist/db/identity-mapping.d.ts.map +1 -1
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/index.js +496 -58
- package/dist/lib/feature-manifest.d.ts.map +1 -1
- package/dist/lib/local-opt-in.d.ts +14 -0
- package/dist/lib/local-opt-in.d.ts.map +1 -1
- package/dist/lib/paths.d.ts +11 -1
- package/dist/lib/paths.d.ts.map +1 -1
- package/dist/mcp/index.d.ts +52 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +15162 -30695
- package/dist/mcp/tools/agents.d.ts.map +1 -1
- package/dist/mcp/tools/task-adv-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-auto-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp/tools/task-meta-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-resources.d.ts.map +1 -1
- package/dist/mcp/tools/templates.d.ts.map +1 -1
- package/dist/mcp.js +3 -3
- package/dist/project-registration.js +466 -35
- package/dist/registry.js +77 -30
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/client.d.ts +3 -1
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/index.js +48 -12
- package/dist/sdk/resolve.d.ts +23 -13
- package/dist/sdk/resolve.d.ts.map +1 -1
- package/dist/sdk/types.d.ts +17 -2
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/v1.generated.d.ts +115 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +3972 -916
- package/dist/server/openapi.d.ts +837 -50
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/rate-limit-config.d.ts +6 -0
- package/dist/server/rate-limit-config.d.ts.map +1 -0
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +74 -3
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/local-sqlite.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage.js +464 -33
- package/dist/task-manifest.js +8 -2
- package/dist/types/index.d.ts +5 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/mcp/tools/code-tools.d.ts +0 -15
- package/dist/mcp/tools/code-tools.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -4117,20 +4117,17 @@ function ensureAgentIdentitySchema(db) {
|
|
|
4117
4117
|
SELECT RAISE(ABORT, 'IDENTITY_SOURCE_LINEAGE_IMMUTABLE');
|
|
4118
4118
|
END;
|
|
4119
4119
|
|
|
4120
|
+
-- Drop first so an older definition is replaced rather than silently
|
|
4121
|
+
-- retained (the upgrade path). AGENT_IDENTITY_HISTORY_TRIGGER_DDL then
|
|
4122
|
+
-- re-creates them with CREATE TRIGGER IF NOT EXISTS. The two statements
|
|
4123
|
+
-- do different jobs and both are kept: the DROP is what refreshes a
|
|
4124
|
+
-- stale definition, and the IF NOT EXISTS is what keeps the CREATE safe
|
|
4125
|
+
-- on its own \u2014 so a caller that reaches it without a preceding DROP, in
|
|
4126
|
+
-- its own connection or transaction, gets a no-op instead of
|
|
4127
|
+
-- "trigger ... already exists".
|
|
4120
4128
|
DROP TRIGGER IF EXISTS trg_agent_identity_mapping_history_immutable;
|
|
4121
|
-
CREATE TRIGGER trg_agent_identity_mapping_history_immutable
|
|
4122
|
-
BEFORE UPDATE OF local_agent_id, observed_label, evidence, mapping_basis, status, revision, created_at
|
|
4123
|
-
ON agent_identity_source_mappings
|
|
4124
|
-
BEGIN
|
|
4125
|
-
SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
|
|
4126
|
-
END;
|
|
4127
|
-
|
|
4128
4129
|
DROP TRIGGER IF EXISTS trg_agent_identity_mapping_history_append_only;
|
|
4129
|
-
|
|
4130
|
-
BEFORE DELETE ON agent_identity_source_mappings
|
|
4131
|
-
BEGIN
|
|
4132
|
-
SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
|
|
4133
|
-
END;
|
|
4130
|
+
${AGENT_IDENTITY_HISTORY_TRIGGER_DDL}
|
|
4134
4131
|
`);
|
|
4135
4132
|
db.run("INSERT OR IGNORE INTO _migrations (id) VALUES (?)", [IDENTITY_MIGRATION_ID]);
|
|
4136
4133
|
});
|
|
@@ -4170,7 +4167,20 @@ function storeAgentAlias(agentId, label, aliasKind, status, db) {
|
|
|
4170
4167
|
function recordAgentAlias(agentId, label, db) {
|
|
4171
4168
|
return storeAgentAlias(agentId, label, "historical", "active", db);
|
|
4172
4169
|
}
|
|
4173
|
-
var IDENTITY_PROJECTION_CONTRACT, IDENTITY_MIGRATION_ID = 65
|
|
4170
|
+
var IDENTITY_PROJECTION_CONTRACT, IDENTITY_MIGRATION_ID = 65, AGENT_IDENTITY_HISTORY_TRIGGER_DDL = `
|
|
4171
|
+
CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_history_immutable
|
|
4172
|
+
BEFORE UPDATE OF local_agent_id, observed_label, evidence, mapping_basis, status, revision, created_at
|
|
4173
|
+
ON agent_identity_source_mappings
|
|
4174
|
+
BEGIN
|
|
4175
|
+
SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
|
|
4176
|
+
END;
|
|
4177
|
+
|
|
4178
|
+
CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_history_append_only
|
|
4179
|
+
BEFORE DELETE ON agent_identity_source_mappings
|
|
4180
|
+
BEGIN
|
|
4181
|
+
SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
|
|
4182
|
+
END;
|
|
4183
|
+
`;
|
|
4174
4184
|
var init_identity_mapping = __esm(() => {
|
|
4175
4185
|
init_types();
|
|
4176
4186
|
init_types();
|
|
@@ -4195,7 +4205,7 @@ var init_identity_mapping = __esm(() => {
|
|
|
4195
4205
|
// src/lib/paths.ts
|
|
4196
4206
|
import { existsSync as existsSync2 } from "fs";
|
|
4197
4207
|
import { homedir } from "os";
|
|
4198
|
-
import { join, resolve as resolve2 } from "path";
|
|
4208
|
+
import { isAbsolute, join, resolve as resolve2 } from "path";
|
|
4199
4209
|
import { homedir as pathsResolverHomedir } from "os";
|
|
4200
4210
|
import { join as pathsResolverJoin } from "path";
|
|
4201
4211
|
function pathsResolverAssertApp(app) {
|
|
@@ -4252,8 +4262,14 @@ function dataDir(options) {
|
|
|
4252
4262
|
function effectiveHome(env2 = process.env) {
|
|
4253
4263
|
return env2.HOME || env2.USERPROFILE || homedir();
|
|
4254
4264
|
}
|
|
4265
|
+
function hasnaHomeRoot(env2 = process.env) {
|
|
4266
|
+
const override = env2["HASNA_HOME"]?.trim();
|
|
4267
|
+
if (override && isAbsolute(override))
|
|
4268
|
+
return override;
|
|
4269
|
+
return join(effectiveHome(env2), ".hasna");
|
|
4270
|
+
}
|
|
4255
4271
|
function legacyHomeDir(env2 = process.env) {
|
|
4256
|
-
return join(
|
|
4272
|
+
return join(hasnaHomeRoot(env2), "todos");
|
|
4257
4273
|
}
|
|
4258
4274
|
function resolverHome(env2 = process.env) {
|
|
4259
4275
|
return dataDir({ app: "todos", home: effectiveHome(env2), env: env2 });
|
|
@@ -4672,15 +4688,18 @@ __export(exports_database, {
|
|
|
4672
4688
|
resolvePartialId: () => resolvePartialId,
|
|
4673
4689
|
resolveAssignedToAliases: () => resolveAssignedToAliases,
|
|
4674
4690
|
resetDatabase: () => resetDatabase,
|
|
4691
|
+
refuseLocalStore: () => refuseLocalStore,
|
|
4675
4692
|
now: () => now,
|
|
4676
4693
|
lowerInClause: () => lowerInClause,
|
|
4677
4694
|
lockExpiryCutoff: () => lockExpiryCutoff,
|
|
4678
4695
|
isLockExpired: () => isLockExpired,
|
|
4696
|
+
isLocalStoreRefused: () => isLocalStoreRefused,
|
|
4679
4697
|
getDatabasePath: () => getDatabasePath,
|
|
4680
4698
|
getDatabase: () => getDatabase,
|
|
4681
4699
|
closeDatabase: () => closeDatabase,
|
|
4682
4700
|
clearExpiredLocks: () => clearExpiredLocks,
|
|
4683
4701
|
assignedToAliasSet: () => assignedToAliasSet,
|
|
4702
|
+
allowLocalStore: () => allowLocalStore,
|
|
4684
4703
|
LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
|
|
4685
4704
|
});
|
|
4686
4705
|
import { Database } from "bun:sqlite";
|
|
@@ -4797,7 +4816,18 @@ function maybeInstallShadowCapture(db) {
|
|
|
4797
4816
|
console.error(`[todos] shadow capture install failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4798
4817
|
}
|
|
4799
4818
|
}
|
|
4819
|
+
function refuseLocalStore(message) {
|
|
4820
|
+
localStoreRefusal = message;
|
|
4821
|
+
}
|
|
4822
|
+
function allowLocalStore() {
|
|
4823
|
+
localStoreRefusal = null;
|
|
4824
|
+
}
|
|
4825
|
+
function isLocalStoreRefused() {
|
|
4826
|
+
return localStoreRefusal !== null;
|
|
4827
|
+
}
|
|
4800
4828
|
function getDatabase(dbPath) {
|
|
4829
|
+
if (localStoreRefusal !== null)
|
|
4830
|
+
throw new Error(localStoreRefusal);
|
|
4801
4831
|
if (dbPath === undefined && !selectsTodosLocalStore()) {
|
|
4802
4832
|
throw new Error("API_DATABASE_FALLBACK_FORBIDDEN: this operation must use the shared Todos API; implicit SQLite access is unavailable");
|
|
4803
4833
|
}
|
|
@@ -4938,7 +4968,7 @@ function lowerInClause(column, values, params) {
|
|
|
4938
4968
|
params.push(...values.map((v) => v.toLowerCase()));
|
|
4939
4969
|
return `LOWER(${column}) IN (${values.map(() => "?").join(",")})`;
|
|
4940
4970
|
}
|
|
4941
|
-
var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, ALLOWED_TABLES;
|
|
4971
|
+
var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, localStoreRefusal = null, ALLOWED_TABLES;
|
|
4942
4972
|
var init_database = __esm(() => {
|
|
4943
4973
|
init_local_opt_in();
|
|
4944
4974
|
init_schema();
|
|
@@ -10679,7 +10709,9 @@ function listTasks(filter = {}, db) {
|
|
|
10679
10709
|
params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
|
|
10680
10710
|
} catch {}
|
|
10681
10711
|
}
|
|
10682
|
-
if (
|
|
10712
|
+
if (filter.archived_only) {
|
|
10713
|
+
conditions.push("archived_at IS NOT NULL");
|
|
10714
|
+
} else if (!filter.include_archived) {
|
|
10683
10715
|
conditions.push("archived_at IS NULL");
|
|
10684
10716
|
}
|
|
10685
10717
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
@@ -10692,7 +10724,8 @@ function listTasks(filter = {}, db) {
|
|
|
10692
10724
|
params.push(filter.offset);
|
|
10693
10725
|
}
|
|
10694
10726
|
}
|
|
10695
|
-
const
|
|
10727
|
+
const orderBy = filter.archived_only ? "archived_at DESC, id DESC" : `${PRIORITY_RANK}, created_at DESC, id ASC`;
|
|
10728
|
+
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${orderBy}${limitClause}`).all(...params);
|
|
10696
10729
|
return rows.map(rowToTask);
|
|
10697
10730
|
}
|
|
10698
10731
|
function getTaskByFingerprint(fingerprint, db) {
|
|
@@ -10835,7 +10868,9 @@ function countTasks(filter = {}, db) {
|
|
|
10835
10868
|
}
|
|
10836
10869
|
}
|
|
10837
10870
|
addMetadataConditions(filter.metadata, conditions, params);
|
|
10838
|
-
if (
|
|
10871
|
+
if (filter.archived_only) {
|
|
10872
|
+
conditions.push("archived_at IS NOT NULL");
|
|
10873
|
+
} else if (!filter.include_archived) {
|
|
10839
10874
|
conditions.push("archived_at IS NULL");
|
|
10840
10875
|
}
|
|
10841
10876
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
@@ -10972,6 +11007,10 @@ function updateTaskStored(id, input, db) {
|
|
|
10972
11007
|
sets.push("completed_at = ?");
|
|
10973
11008
|
params.push(input.completed_at);
|
|
10974
11009
|
}
|
|
11010
|
+
if (input.archived_at !== undefined) {
|
|
11011
|
+
sets.push("archived_at = ?");
|
|
11012
|
+
params.push(input.archived_at);
|
|
11013
|
+
}
|
|
10975
11014
|
if (input.confidence !== undefined) {
|
|
10976
11015
|
sets.push("confidence = ?");
|
|
10977
11016
|
params.push(input.confidence);
|
|
@@ -11035,6 +11074,8 @@ function updateTaskStored(id, input, db) {
|
|
|
11035
11074
|
logTaskChange(id, "update", "parent_id", task.parent_id, input.parent_id, agentId, d);
|
|
11036
11075
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
11037
11076
|
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
11077
|
+
if (input.archived_at !== undefined && input.archived_at !== task.archived_at)
|
|
11078
|
+
logTaskChange(id, "update", "archived_at", task.archived_at, input.archived_at, agentId, d);
|
|
11038
11079
|
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
11039
11080
|
logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
|
|
11040
11081
|
if (input.approved_by !== undefined)
|
|
@@ -13845,7 +13886,7 @@ var init_dispatches = __esm(() => {
|
|
|
13845
13886
|
// package.json
|
|
13846
13887
|
var package_default = {
|
|
13847
13888
|
name: "@hasna/todos",
|
|
13848
|
-
version: "0.
|
|
13889
|
+
version: "0.17.0",
|
|
13849
13890
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
13850
13891
|
type: "module",
|
|
13851
13892
|
main: "dist/index.js",
|
|
@@ -13958,7 +13999,7 @@ var package_default = {
|
|
|
13958
13999
|
url: "https://github.com/hasna/apps.git",
|
|
13959
14000
|
directory: "apps/todos"
|
|
13960
14001
|
},
|
|
13961
|
-
homepage: "https://github.com/hasna/apps",
|
|
14002
|
+
homepage: "https://github.com/hasna/apps/tree/main/apps/todos#readme",
|
|
13962
14003
|
bugs: {
|
|
13963
14004
|
url: "https://github.com/hasna/apps/issues"
|
|
13964
14005
|
},
|
|
@@ -14381,7 +14422,7 @@ var MCP_TOOL_GROUPS = {
|
|
|
14381
14422
|
"machines_topology",
|
|
14382
14423
|
"machines_unarchive"
|
|
14383
14424
|
],
|
|
14384
|
-
maintenance: ["
|
|
14425
|
+
maintenance: ["get_sla_breaches", "notify_upcoming_deadlines", "run_doctor", "score_task"]
|
|
14385
14426
|
};
|
|
14386
14427
|
var MCP_PROFILE_GROUPS = {
|
|
14387
14428
|
minimal: ["core", "loops"],
|
|
@@ -19806,17 +19847,23 @@ var TODOS_CLI_MCP_PARITY = [
|
|
|
19806
19847
|
"todos extract",
|
|
19807
19848
|
"todos extract-watch"
|
|
19808
19849
|
],
|
|
19809
|
-
mcpTools: [
|
|
19810
|
-
"extract_todos",
|
|
19811
|
-
"watch_source_todos"
|
|
19812
|
-
],
|
|
19850
|
+
mcpTools: [],
|
|
19813
19851
|
jsonContracts: ["source_code_index", "source_todo_comment", "task", "structured_error", "api_error"],
|
|
19814
19852
|
errorContracts: ["structured_error", "api_error"],
|
|
19815
|
-
status: "
|
|
19816
|
-
intentionalGaps: [
|
|
19853
|
+
status: "intentional-gap",
|
|
19854
|
+
intentionalGaps: [
|
|
19855
|
+
{
|
|
19856
|
+
cliCommand: "todos extract",
|
|
19857
|
+
reason: "The source TODO index scans the local checkout the CLI runs in; the former extract_todos MCP tool read the agent host's filesystem behind a task-store tool surface and had no hosted arm, so it was removed (fleet alignment 2026-09-11)."
|
|
19858
|
+
},
|
|
19859
|
+
{
|
|
19860
|
+
cliCommand: "todos extract-watch",
|
|
19861
|
+
reason: "A polling filesystem watcher is a long-running local process, not an MCP tool call; the former watch_source_todos tool was removed with extract_todos \u2014 run `todos extract-watch` from the checkout instead."
|
|
19862
|
+
}
|
|
19863
|
+
],
|
|
19864
|
+
gapReason: "Source scanning and watching are local-filesystem CLI operations; no MCP tool mapping.",
|
|
19817
19865
|
example: {
|
|
19818
|
-
cli: "todos extract . --dry-run --index --json"
|
|
19819
|
-
mcpTool: "extract_todos"
|
|
19866
|
+
cli: "todos extract . --dry-run --index --json"
|
|
19820
19867
|
}
|
|
19821
19868
|
},
|
|
19822
19869
|
{
|
|
@@ -26405,6 +26452,7 @@ function searchTasks(options, projectId, taskListId, db) {
|
|
|
26405
26452
|
|
|
26406
26453
|
// src/storage/local-sqlite.ts
|
|
26407
26454
|
init_tasks();
|
|
26455
|
+
init_task_graph();
|
|
26408
26456
|
init_projects();
|
|
26409
26457
|
init_plans();
|
|
26410
26458
|
|
|
@@ -26968,6 +27016,19 @@ function listAgents(opts, db) {
|
|
|
26968
27016
|
}
|
|
26969
27017
|
return d.query("SELECT * FROM agents WHERE status = 'active' ORDER BY name").all().map(rowToAgent);
|
|
26970
27018
|
}
|
|
27019
|
+
function listAgentsPage(options, db) {
|
|
27020
|
+
if (!Number.isSafeInteger(options.limit) || options.limit < 1 || options.limit > 500) {
|
|
27021
|
+
throw new Error("Agent page limit must be an integer from 1 to 500");
|
|
27022
|
+
}
|
|
27023
|
+
if (!Number.isSafeInteger(options.offset) || options.offset < 0) {
|
|
27024
|
+
throw new Error("Agent page offset must be a non-negative integer");
|
|
27025
|
+
}
|
|
27026
|
+
const d = db || getDatabase();
|
|
27027
|
+
const where = options.include_archived ? "" : "WHERE status = 'active'";
|
|
27028
|
+
const total = d.query(`SELECT COUNT(*) AS total FROM agents ${where}`).get().total;
|
|
27029
|
+
const agents = d.query(`SELECT * FROM agents ${where} ORDER BY LOWER(name), id LIMIT ? OFFSET ?`).all(options.limit, options.offset).map(rowToAgent);
|
|
27030
|
+
return { agents, total };
|
|
27031
|
+
}
|
|
26971
27032
|
function updateAgentActivity(id, db) {
|
|
26972
27033
|
const d = db || getDatabase();
|
|
26973
27034
|
d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), id]);
|
|
@@ -28296,6 +28357,98 @@ function exportSqliteMachines(db) {
|
|
|
28296
28357
|
}
|
|
28297
28358
|
|
|
28298
28359
|
// src/storage/local-sqlite.ts
|
|
28360
|
+
function bulkCreateAtomicSqlite(db, inputs) {
|
|
28361
|
+
return db.transaction(() => {
|
|
28362
|
+
const tempIds = new Map;
|
|
28363
|
+
const created = [];
|
|
28364
|
+
const dependencies = [];
|
|
28365
|
+
for (const input of inputs) {
|
|
28366
|
+
const { temp_id, depends_on: _dependsOn, ...taskInput } = input;
|
|
28367
|
+
const task2 = createTask(taskInput, db);
|
|
28368
|
+
if (temp_id)
|
|
28369
|
+
tempIds.set(temp_id, task2.id);
|
|
28370
|
+
created.push({ temp_id: temp_id ?? null, id: task2.id, short_id: task2.short_id, title: task2.title });
|
|
28371
|
+
}
|
|
28372
|
+
for (let index = 0;index < inputs.length; index++) {
|
|
28373
|
+
const input = inputs[index];
|
|
28374
|
+
const taskId = created[index].id;
|
|
28375
|
+
const seenDependencies = new Set;
|
|
28376
|
+
for (const reference of input.depends_on ?? []) {
|
|
28377
|
+
const dependencyId = tempIds.get(reference) ?? resolveTaskRefLocal(db, reference)?.id;
|
|
28378
|
+
if (!dependencyId)
|
|
28379
|
+
throw new TaskNotFoundError(reference);
|
|
28380
|
+
if (seenDependencies.has(dependencyId)) {
|
|
28381
|
+
throw new ResourceConflictError("BULK_CREATE_DUPLICATE_DEPENDENCY", `Multiple references resolve to dependency ${dependencyId}`);
|
|
28382
|
+
}
|
|
28383
|
+
seenDependencies.add(dependencyId);
|
|
28384
|
+
addDependency(taskId, dependencyId, db);
|
|
28385
|
+
dependencies.push({ task_id: taskId, depends_on: dependencyId });
|
|
28386
|
+
}
|
|
28387
|
+
}
|
|
28388
|
+
return { schema_version: 1, atomic: true, created, dependencies };
|
|
28389
|
+
})();
|
|
28390
|
+
}
|
|
28391
|
+
function deleteTaskHierarchySqlite(db, rootId) {
|
|
28392
|
+
const rows = db.query(`WITH RECURSIVE tree(id, depth) AS (
|
|
28393
|
+
SELECT id, 0 FROM tasks WHERE id = ?
|
|
28394
|
+
UNION ALL
|
|
28395
|
+
SELECT child.id, tree.depth + 1 FROM tasks child JOIN tree ON child.parent_id = tree.id
|
|
28396
|
+
) SELECT id FROM tree ORDER BY depth DESC, id`).all(rootId);
|
|
28397
|
+
if (rows.length === 0)
|
|
28398
|
+
throw new TaskNotFoundError(rootId);
|
|
28399
|
+
for (const row of rows) {
|
|
28400
|
+
if (!deleteTask(row.id, db))
|
|
28401
|
+
throw new Error(`Atomic hierarchy delete lost task ${row.id}`);
|
|
28402
|
+
}
|
|
28403
|
+
}
|
|
28404
|
+
function bulkDeleteAtomicSqlite(db, ids2, force) {
|
|
28405
|
+
return db.transaction(() => {
|
|
28406
|
+
const resolved = ids2.map((reference) => ({ reference, task: resolveTaskRefLocal(db, reference) }));
|
|
28407
|
+
const seen = new Set;
|
|
28408
|
+
for (const item of resolved) {
|
|
28409
|
+
if (!item.task)
|
|
28410
|
+
continue;
|
|
28411
|
+
if (seen.has(item.task.id)) {
|
|
28412
|
+
throw new ResourceConflictError("BULK_DELETE_DUPLICATE_TASK", `Multiple references resolve to task ${item.task.id}`);
|
|
28413
|
+
}
|
|
28414
|
+
seen.add(item.task.id);
|
|
28415
|
+
}
|
|
28416
|
+
const childState = new Map;
|
|
28417
|
+
for (const item of resolved) {
|
|
28418
|
+
if (!item.task)
|
|
28419
|
+
continue;
|
|
28420
|
+
childState.set(item.task.id, Boolean(db.query("SELECT id FROM tasks WHERE parent_id = ? LIMIT 1").get(item.task.id)));
|
|
28421
|
+
}
|
|
28422
|
+
const planned = resolved.filter((item) => item.task && (force || !childState.get(item.task.id)));
|
|
28423
|
+
const plannedIds = new Set(planned.map((item) => item.task.id));
|
|
28424
|
+
const roots = planned.filter((item) => {
|
|
28425
|
+
if (!force)
|
|
28426
|
+
return true;
|
|
28427
|
+
let parentId = item.task.parent_id;
|
|
28428
|
+
while (parentId) {
|
|
28429
|
+
if (plannedIds.has(parentId))
|
|
28430
|
+
return false;
|
|
28431
|
+
parentId = getTask(parentId, db)?.parent_id ?? null;
|
|
28432
|
+
}
|
|
28433
|
+
return true;
|
|
28434
|
+
});
|
|
28435
|
+
for (const item of roots)
|
|
28436
|
+
deleteTaskHierarchySqlite(db, item.task.id);
|
|
28437
|
+
return {
|
|
28438
|
+
schema_version: 1,
|
|
28439
|
+
atomic: true,
|
|
28440
|
+
force,
|
|
28441
|
+
results: resolved.map((item) => {
|
|
28442
|
+
if (!item.task)
|
|
28443
|
+
return { requested_id: item.reference, task_id: null, outcome: "missing", reason: "not_found" };
|
|
28444
|
+
if (!force && childState.get(item.task.id)) {
|
|
28445
|
+
return { requested_id: item.reference, task_id: item.task.id, outcome: "skipped", reason: "has_children" };
|
|
28446
|
+
}
|
|
28447
|
+
return { requested_id: item.reference, task_id: item.task.id, outcome: "deleted", reason: null };
|
|
28448
|
+
})
|
|
28449
|
+
};
|
|
28450
|
+
})();
|
|
28451
|
+
}
|
|
28299
28452
|
var TASK_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
28300
28453
|
function resolveTaskRefLocal(db, ref) {
|
|
28301
28454
|
const raw = ref.trim().toLowerCase();
|
|
@@ -28392,6 +28545,8 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
28392
28545
|
},
|
|
28393
28546
|
handoffStaleLock: (input) => handoffStaleTaskLock(input, database()),
|
|
28394
28547
|
delete: (id) => deleteTask(id, database()),
|
|
28548
|
+
bulkCreateAtomic: (inputs, _context) => bulkCreateAtomicSqlite(database(), inputs),
|
|
28549
|
+
bulkDeleteAtomic: (ids2, force, _context) => bulkDeleteAtomicSqlite(database(), ids2, force),
|
|
28395
28550
|
start: (id, agentId) => startTask(id, agentId, database()),
|
|
28396
28551
|
complete: (id, agentId, options2) => completeTask(id, agentId, database(), options2),
|
|
28397
28552
|
fail: (id, agentId, reason, options2) => failTask(id, agentId, reason, options2, database()),
|
|
@@ -28400,6 +28555,30 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
28400
28555
|
getActiveWork: (filters) => getActiveWork(filters, database()),
|
|
28401
28556
|
getChangedSince: (since, filters) => getTasksChangedSince(since, filters, database())
|
|
28402
28557
|
},
|
|
28558
|
+
dependencies: {
|
|
28559
|
+
add: (taskId, dependsOn) => {
|
|
28560
|
+
addDependency(taskId, dependsOn, database());
|
|
28561
|
+
return { task_id: taskId, depends_on: dependsOn };
|
|
28562
|
+
},
|
|
28563
|
+
remove: (taskId, dependsOn) => removeDependency(taskId, dependsOn, database()),
|
|
28564
|
+
list: (taskId) => {
|
|
28565
|
+
const dependencies = getTaskDependencies(taskId, database());
|
|
28566
|
+
const blocks = getTaskDependents(taskId, database());
|
|
28567
|
+
return { dependencies, blocks, blocked_by: blocks };
|
|
28568
|
+
},
|
|
28569
|
+
listPage: ({ limit, offset }) => {
|
|
28570
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
|
|
28571
|
+
throw new Error("SQLite dependency page limit must be an integer from 1 to 500");
|
|
28572
|
+
}
|
|
28573
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
28574
|
+
throw new Error("SQLite dependency page offset must be a non-negative integer");
|
|
28575
|
+
}
|
|
28576
|
+
const totalRow = database().query("SELECT COUNT(*) AS total FROM task_dependencies").get();
|
|
28577
|
+
const dependencies = database().query("SELECT task_id, depends_on FROM task_dependencies ORDER BY task_id, depends_on LIMIT ? OFFSET ?").all(limit, offset);
|
|
28578
|
+
return { dependencies, total: totalRow.total };
|
|
28579
|
+
},
|
|
28580
|
+
listAll: () => database().query("SELECT task_id, depends_on FROM task_dependencies ORDER BY task_id, depends_on").all()
|
|
28581
|
+
},
|
|
28403
28582
|
projects: {
|
|
28404
28583
|
create: (input) => createProject(input, database()),
|
|
28405
28584
|
get: (id) => getProject(id, database()),
|
|
@@ -28443,6 +28622,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
28443
28622
|
get: (id) => getAgent(id, database()),
|
|
28444
28623
|
getByName: (name) => getAgentByName(name, database()),
|
|
28445
28624
|
list: (options2) => listAgents(options2, database()),
|
|
28625
|
+
listPage: (options2) => listAgentsPage(options2, database()),
|
|
28446
28626
|
update: (id, input) => updateAgent(id, input, database())
|
|
28447
28627
|
},
|
|
28448
28628
|
taskLists: {
|
|
@@ -28481,6 +28661,29 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
28481
28661
|
return comments;
|
|
28482
28662
|
},
|
|
28483
28663
|
getTaskHistory: (taskId) => getTaskHistory(taskId, database()),
|
|
28664
|
+
getTaskHistoryPage: (taskId, options2) => {
|
|
28665
|
+
if (!Number.isSafeInteger(options2.limit) || options2.limit < 1 || options2.limit > 500) {
|
|
28666
|
+
throw new Error("Task history limit must be an integer from 1 to 500");
|
|
28667
|
+
}
|
|
28668
|
+
if (!Number.isSafeInteger(options2.offset) || options2.offset < 0) {
|
|
28669
|
+
throw new Error("Task history offset must be a non-negative integer");
|
|
28670
|
+
}
|
|
28671
|
+
const conditions = ["task_id = ?"];
|
|
28672
|
+
const values = [taskId];
|
|
28673
|
+
if (options2.since) {
|
|
28674
|
+
conditions.push("created_at >= ?");
|
|
28675
|
+
values.push(options2.since);
|
|
28676
|
+
}
|
|
28677
|
+
if (options2.until) {
|
|
28678
|
+
conditions.push("created_at <= ?");
|
|
28679
|
+
values.push(options2.until);
|
|
28680
|
+
}
|
|
28681
|
+
const where = conditions.join(" AND ");
|
|
28682
|
+
const total = database().query(`SELECT COUNT(*) AS total FROM task_history WHERE ${where}`).get(...values).total;
|
|
28683
|
+
const direction = options2.order === "asc" ? "ASC" : "DESC";
|
|
28684
|
+
const history = database().query(`SELECT * FROM task_history WHERE ${where} ORDER BY created_at ${direction}, id ${direction} LIMIT ? OFFSET ?`).all(...values, options2.limit, options2.offset);
|
|
28685
|
+
return { history, total };
|
|
28686
|
+
},
|
|
28484
28687
|
getRecentActivity: (limit) => getRecentActivity(limit, database())
|
|
28485
28688
|
},
|
|
28486
28689
|
sync: {
|
|
@@ -30247,6 +30450,8 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
30247
30450
|
count: (filter = {}) => store.countTasks(filter),
|
|
30248
30451
|
update: (id, input, context) => updateTask2(id, input, store, context),
|
|
30249
30452
|
delete: (id, context) => store.deleteTaskHierarchy(id, context),
|
|
30453
|
+
bulkCreateAtomic: (inputs, context) => bulkCreateTasksAtomic(inputs, store, context),
|
|
30454
|
+
bulkDeleteAtomic: (ids2, force, context) => bulkDeleteTasksAtomic(ids2, force, store, context),
|
|
30250
30455
|
start: (id, agentId) => startTask2(id, agentId, store),
|
|
30251
30456
|
complete: (id, agentId, options2) => completeTask2(id, agentId, options2, store),
|
|
30252
30457
|
fail: (id, agentId, reason, options2) => failTask2(id, agentId, reason, options2, store),
|
|
@@ -30263,6 +30468,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
30263
30468
|
add: (taskId, dependsOn, context) => addDependency2(taskId, dependsOn, store, context),
|
|
30264
30469
|
remove: (taskId, dependsOn) => removeDependency2(taskId, dependsOn, store),
|
|
30265
30470
|
list: (taskId) => listDependencies(taskId, store),
|
|
30471
|
+
listPage: (page) => store.listDependencyPage(page),
|
|
30266
30472
|
listAll: () => store.list("dependencies")
|
|
30267
30473
|
},
|
|
30268
30474
|
verifications: {
|
|
@@ -30328,6 +30534,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
30328
30534
|
get: (id) => store.get("agents", id),
|
|
30329
30535
|
getByName: async (name) => matchAgentByName(await store.list("agents"), name),
|
|
30330
30536
|
list: async (options2) => (await store.list("agents")).filter((agent) => options2?.include_archived || agent.status !== "archived").sort((a, b) => a.name.localeCompare(b.name)),
|
|
30537
|
+
listPage: (options2) => store.listAgentPage(options2),
|
|
30331
30538
|
update: (id, input) => updateAgent2(id, input, store),
|
|
30332
30539
|
heartbeat: (idOrName, context) => heartbeatAgent(idOrName, store, context),
|
|
30333
30540
|
release: (idOrName, sessionId, context) => releaseAgent2(idOrName, sessionId, store, context)
|
|
@@ -30385,8 +30592,9 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
30385
30592
|
getCommentsPage: async (taskId, options2) => {
|
|
30386
30593
|
return (await store.listComments(taskId, options2)).map(redactComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
|
|
30387
30594
|
},
|
|
30388
|
-
getTaskHistory: async (taskId) => (await store.list("audit_history")).filter((entry2) => entry2.task_id === taskId).sort((a, b) =>
|
|
30389
|
-
|
|
30595
|
+
getTaskHistory: async (taskId) => (await store.list("audit_history")).filter((entry2) => entry2.task_id === taskId).sort((a, b) => b.created_at.localeCompare(a.created_at) || b.id.localeCompare(a.id)),
|
|
30596
|
+
getTaskHistoryPage: (taskId, options2) => store.listTaskHistoryPage(taskId, options2),
|
|
30597
|
+
getRecentActivity: (limit = 20) => store.listRecentActivity(limit)
|
|
30390
30598
|
},
|
|
30391
30599
|
machines: createPostgresMachineRegistry(options.client, options.service ?? "todos", options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE, () => store.ensureSchema()),
|
|
30392
30600
|
atomicProjectMigration: createAtomicProjectMigration({ client: options.client, table: options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE, service: options.service ?? "todos", ensureSchema: () => store.ensureSchema() }),
|
|
@@ -30423,6 +30631,8 @@ class PostgresJsonRecordStore {
|
|
|
30423
30631
|
}
|
|
30424
30632
|
async withDependencyGraphTransaction(fn) {
|
|
30425
30633
|
await this.ensureSchema();
|
|
30634
|
+
if (this.projectIntegrityLocked)
|
|
30635
|
+
return fn(this);
|
|
30426
30636
|
return this.withTaskParentIntegrityTransaction(async (client) => {
|
|
30427
30637
|
const scoped = new PostgresJsonRecordStore({ ...this.options, client });
|
|
30428
30638
|
scoped.schemaReady = Promise.resolve();
|
|
@@ -30569,6 +30779,128 @@ class PostgresJsonRecordStore {
|
|
|
30569
30779
|
async list(type) {
|
|
30570
30780
|
return (await this.listRecords(type)).map((record) => record.payload);
|
|
30571
30781
|
}
|
|
30782
|
+
async listAgentPage(options) {
|
|
30783
|
+
const { limit, offset } = options;
|
|
30784
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
|
|
30785
|
+
throw new Error("Postgres agent page limit must be an integer from 1 to 500");
|
|
30786
|
+
}
|
|
30787
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
30788
|
+
throw new Error("Postgres agent page offset must be a non-negative integer");
|
|
30789
|
+
}
|
|
30790
|
+
await this.ensureSchema();
|
|
30791
|
+
const result = await this.options.client.query(`/* todos:list-agent-page */ WITH filtered AS (
|
|
30792
|
+
SELECT object_id, payload
|
|
30793
|
+
FROM ${this.tableName}
|
|
30794
|
+
WHERE service = $1 AND object_type = 'agents' AND deleted_at IS NULL
|
|
30795
|
+
AND ($2::boolean OR COALESCE(payload->>'status', 'active') <> 'archived')
|
|
30796
|
+
), page AS (
|
|
30797
|
+
SELECT object_id, payload FROM filtered
|
|
30798
|
+
ORDER BY LOWER(payload->>'name'), object_id
|
|
30799
|
+
LIMIT $3 OFFSET $4
|
|
30800
|
+
)
|
|
30801
|
+
SELECT (SELECT count(*) FROM filtered) AS total,
|
|
30802
|
+
COALESCE((SELECT jsonb_agg(payload ORDER BY LOWER(payload->>'name'), object_id) FROM page), '[]'::jsonb) AS agents`, [this.service, options.include_archived === true, limit, offset]);
|
|
30803
|
+
const row = result.rows[0];
|
|
30804
|
+
const total = Number(row?.total);
|
|
30805
|
+
if (!Number.isSafeInteger(total) || total < 0 || !Array.isArray(row?.agents)) {
|
|
30806
|
+
throw new Error("Postgres agent page returned invalid count or agents");
|
|
30807
|
+
}
|
|
30808
|
+
if (row.agents.length > limit || row.agents.length > 0 && offset + row.agents.length > total) {
|
|
30809
|
+
throw new Error("Postgres agent page exceeded its requested bounds");
|
|
30810
|
+
}
|
|
30811
|
+
return { agents: row.agents.map((value) => payloadRecord2(value)), total };
|
|
30812
|
+
}
|
|
30813
|
+
async listRecentActivity(limit) {
|
|
30814
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e4) {
|
|
30815
|
+
throw new Error("Postgres recent activity limit must be an integer from 1 to 10000");
|
|
30816
|
+
}
|
|
30817
|
+
await this.ensureSchema();
|
|
30818
|
+
const result = await this.options.client.query(`/* todos:list-recent-activity */ SELECT payload
|
|
30819
|
+
FROM ${this.tableName}
|
|
30820
|
+
WHERE service = $1 AND object_type = 'audit_history' AND deleted_at IS NULL
|
|
30821
|
+
ORDER BY payload->>'created_at' DESC, object_id DESC
|
|
30822
|
+
LIMIT $2`, [this.service, limit]);
|
|
30823
|
+
return result.rows.map((row) => payloadRecord2(row.payload));
|
|
30824
|
+
}
|
|
30825
|
+
async listTaskHistoryPage(taskId, options) {
|
|
30826
|
+
const { limit, offset } = options;
|
|
30827
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
|
|
30828
|
+
throw new Error("Postgres task history limit must be an integer from 1 to 500");
|
|
30829
|
+
}
|
|
30830
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
30831
|
+
throw new Error("Postgres task history offset must be a non-negative integer");
|
|
30832
|
+
}
|
|
30833
|
+
await this.ensureSchema();
|
|
30834
|
+
const direction = options.order === "asc" ? "ASC" : "DESC";
|
|
30835
|
+
const result = await this.options.client.query(`/* todos:list-task-history-page */ WITH filtered AS (
|
|
30836
|
+
SELECT object_id, payload, payload->>'created_at' AS created_at
|
|
30837
|
+
FROM ${this.tableName}
|
|
30838
|
+
WHERE service = $1 AND object_type = 'audit_history' AND deleted_at IS NULL
|
|
30839
|
+
AND payload->>'task_id' = $2
|
|
30840
|
+
AND ($3::text IS NULL OR payload->>'created_at' >= $3)
|
|
30841
|
+
AND ($4::text IS NULL OR payload->>'created_at' <= $4)
|
|
30842
|
+
), page AS (
|
|
30843
|
+
SELECT object_id, payload, created_at FROM filtered
|
|
30844
|
+
ORDER BY created_at ${direction}, object_id ${direction}
|
|
30845
|
+
LIMIT $5 OFFSET $6
|
|
30846
|
+
)
|
|
30847
|
+
SELECT (SELECT count(*) FROM filtered) AS total,
|
|
30848
|
+
COALESCE((SELECT jsonb_agg(payload ORDER BY created_at ${direction}, object_id ${direction}) FROM page), '[]'::jsonb) AS history`, [this.service, taskId, options.since ?? null, options.until ?? null, limit, offset]);
|
|
30849
|
+
const row = result.rows[0];
|
|
30850
|
+
const total = Number(row?.total);
|
|
30851
|
+
if (!Number.isSafeInteger(total) || total < 0 || !Array.isArray(row?.history)) {
|
|
30852
|
+
throw new Error("Postgres task history page returned invalid count or history");
|
|
30853
|
+
}
|
|
30854
|
+
if (row.history.length > limit || row.history.length > 0 && offset + row.history.length > total) {
|
|
30855
|
+
throw new Error("Postgres task history page exceeded its requested bounds");
|
|
30856
|
+
}
|
|
30857
|
+
return { history: row.history.map((value) => payloadRecord2(value)), total };
|
|
30858
|
+
}
|
|
30859
|
+
async listDependencyPage(options) {
|
|
30860
|
+
const { limit, offset } = options;
|
|
30861
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
|
|
30862
|
+
throw new Error("Postgres dependency page limit must be an integer from 1 to 500");
|
|
30863
|
+
}
|
|
30864
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
30865
|
+
throw new Error("Postgres dependency page offset must be a non-negative integer");
|
|
30866
|
+
}
|
|
30867
|
+
await this.ensureSchema();
|
|
30868
|
+
const result = await this.options.client.query(`/* todos:list-dependencies-page */ WITH page AS (
|
|
30869
|
+
SELECT payload, updated_at, object_id
|
|
30870
|
+
FROM ${this.tableName}
|
|
30871
|
+
WHERE service = $1 AND object_type = 'dependencies' AND deleted_at IS NULL
|
|
30872
|
+
ORDER BY updated_at ASC, object_id ASC
|
|
30873
|
+
LIMIT $2 OFFSET $3
|
|
30874
|
+
)
|
|
30875
|
+
SELECT
|
|
30876
|
+
(SELECT COUNT(*)::text FROM ${this.tableName}
|
|
30877
|
+
WHERE service = $1 AND object_type = 'dependencies' AND deleted_at IS NULL) AS total,
|
|
30878
|
+
COALESCE(
|
|
30879
|
+
jsonb_agg(page.payload ORDER BY page.updated_at ASC, page.object_id ASC)
|
|
30880
|
+
FILTER (WHERE page.payload IS NOT NULL),
|
|
30881
|
+
'[]'::jsonb
|
|
30882
|
+
) AS dependencies
|
|
30883
|
+
FROM page`, [this.service, limit, offset]);
|
|
30884
|
+
const row = result.rows[0];
|
|
30885
|
+
const totalText = typeof row?.total === "string" ? row.total : typeof row?.total === "number" && Number.isSafeInteger(row.total) ? String(row.total) : null;
|
|
30886
|
+
if (totalText === null || !/^\d+$/.test(totalText)) {
|
|
30887
|
+
throw new Error("Postgres dependency page returned an invalid total");
|
|
30888
|
+
}
|
|
30889
|
+
const total = Number(totalText);
|
|
30890
|
+
if (!Number.isSafeInteger(total)) {
|
|
30891
|
+
throw new Error("Postgres dependency page total exceeds the safe integer range");
|
|
30892
|
+
}
|
|
30893
|
+
if (!Array.isArray(row?.dependencies)) {
|
|
30894
|
+
throw new Error("Postgres dependency page returned an invalid dependencies array");
|
|
30895
|
+
}
|
|
30896
|
+
if (row.dependencies.length > limit || offset + row.dependencies.length > total) {
|
|
30897
|
+
throw new Error("Postgres dependency page returned rows outside the requested bound");
|
|
30898
|
+
}
|
|
30899
|
+
return {
|
|
30900
|
+
dependencies: row.dependencies.map((value) => payloadRecord2(value)),
|
|
30901
|
+
total
|
|
30902
|
+
};
|
|
30903
|
+
}
|
|
30572
30904
|
async listComments(taskId, options = {}) {
|
|
30573
30905
|
await this.ensureSchema();
|
|
30574
30906
|
const limit = options.limit ?? 100;
|
|
@@ -30644,7 +30976,9 @@ class PostgresJsonRecordStore {
|
|
|
30644
30976
|
return "1=0";
|
|
30645
30977
|
return `${column} IN (${values.map((v) => p(v)).join(", ")})`;
|
|
30646
30978
|
};
|
|
30647
|
-
if (filter.
|
|
30979
|
+
if (filter.archived_only === true)
|
|
30980
|
+
conds.push(`(payload->>'archived_at' IS NOT NULL)`);
|
|
30981
|
+
else if (filter.include_archived === false)
|
|
30648
30982
|
conds.push(`(payload->>'archived_at' IS NULL)`);
|
|
30649
30983
|
if (filter.ids)
|
|
30650
30984
|
conds.push(inClause("payload->>'id'", filter.ids));
|
|
@@ -30701,7 +31035,7 @@ class PostgresJsonRecordStore {
|
|
|
30701
31035
|
async listTasks(filter) {
|
|
30702
31036
|
await this.ensureSchema();
|
|
30703
31037
|
const { where, params, queryRef } = await this.buildTaskFilterSql(filter);
|
|
30704
|
-
const orderBy = queryRef ? `ORDER BY ts_rank_cd(task_search_tsv, websearch_to_tsquery('simple', todos_immutable_unaccent(${queryRef}))) DESC, ${TASK_ORDER_TIEBREAK}` : TASK_ORDER_BY;
|
|
31038
|
+
const orderBy = filter.archived_only ? `ORDER BY payload->>'archived_at' DESC, object_id DESC` : queryRef ? `ORDER BY ts_rank_cd(task_search_tsv, websearch_to_tsquery('simple', todos_immutable_unaccent(${queryRef}))) DESC, ${TASK_ORDER_TIEBREAK}` : TASK_ORDER_BY;
|
|
30705
31039
|
let sql = `/* todos:list-tasks */ SELECT payload FROM ${this.tableName} WHERE ${where} ${orderBy}`;
|
|
30706
31040
|
if (filter.limit !== undefined) {
|
|
30707
31041
|
params.push(filter.limit);
|
|
@@ -30921,6 +31255,8 @@ class PostgresJsonRecordStore {
|
|
|
30921
31255
|
if (planIds.length === 0 && !parentGuard)
|
|
30922
31256
|
return this.upsert("tasks", value, context);
|
|
30923
31257
|
await this.ensureSchema();
|
|
31258
|
+
if (!queryClient && this.projectIntegrityLocked)
|
|
31259
|
+
queryClient = this.options.client;
|
|
30924
31260
|
if (!queryClient) {
|
|
30925
31261
|
return this.withTaskParentIntegrityTransaction((client2) => this.upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context, parentGuard, client2));
|
|
30926
31262
|
}
|
|
@@ -31458,9 +31794,11 @@ class PostgresJsonRecordStore {
|
|
|
31458
31794
|
}
|
|
31459
31795
|
async deleteTaskHierarchy(id, context = {}) {
|
|
31460
31796
|
await this.ensureSchema();
|
|
31461
|
-
|
|
31462
|
-
|
|
31463
|
-
|
|
31797
|
+
if (!this.projectIntegrityLocked) {
|
|
31798
|
+
return this.withDependencyGraphTransaction((scoped) => scoped.deleteTaskHierarchy(id, context));
|
|
31799
|
+
}
|
|
31800
|
+
const timestamp3 = new Date().toISOString();
|
|
31801
|
+
const result = await this.options.client.query(`/* todos:task-parent-integrity-delete */ WITH RECURSIVE
|
|
31464
31802
|
task_tree(object_id, path, cycle) AS (
|
|
31465
31803
|
SELECT task.object_id, ARRAY[task.object_id], false
|
|
31466
31804
|
FROM ${this.tableName} AS task
|
|
@@ -31522,13 +31860,12 @@ class PostgresJsonRecordStore {
|
|
|
31522
31860
|
SELECT EXISTS (SELECT 1 FROM task_tree WHERE object_id = $2) AS found,
|
|
31523
31861
|
(SELECT count(*) FROM tombstoned) AS deleted_count,
|
|
31524
31862
|
(SELECT count(*) FROM tombstoned_related) AS related_deleted_count`, [
|
|
31525
|
-
|
|
31526
|
-
|
|
31527
|
-
|
|
31528
|
-
|
|
31529
|
-
|
|
31530
|
-
|
|
31531
|
-
});
|
|
31863
|
+
this.service,
|
|
31864
|
+
id,
|
|
31865
|
+
timestamp3,
|
|
31866
|
+
context.requestId ?? this.sourceMachineId ?? null
|
|
31867
|
+
]);
|
|
31868
|
+
return Boolean(result.rows[0]?.found);
|
|
31532
31869
|
}
|
|
31533
31870
|
async getPlanProjectLinkReceipt(receiptId) {
|
|
31534
31871
|
const value = await this.get("plan_project_link_receipts", receiptId);
|
|
@@ -31984,6 +32321,102 @@ class PostgresJsonRecordStore {
|
|
|
31984
32321
|
updated_at = EXCLUDED.updated_at`, [this.service, name, value]);
|
|
31985
32322
|
}
|
|
31986
32323
|
}
|
|
32324
|
+
async function bulkCreateTasksAtomic(inputs, store, context) {
|
|
32325
|
+
return store.withDependencyGraphTransaction(async (scoped) => {
|
|
32326
|
+
const tempIds = new Map;
|
|
32327
|
+
const created = [];
|
|
32328
|
+
const dependencies = [];
|
|
32329
|
+
for (const input of inputs) {
|
|
32330
|
+
const { temp_id, depends_on: _dependsOn, ...taskInput } = input;
|
|
32331
|
+
const task2 = await createTask2(taskInput, scoped, context);
|
|
32332
|
+
if (temp_id)
|
|
32333
|
+
tempIds.set(temp_id, task2.id);
|
|
32334
|
+
created.push({ temp_id: temp_id ?? null, id: task2.id, short_id: task2.short_id, title: task2.title });
|
|
32335
|
+
}
|
|
32336
|
+
for (let index = 0;index < inputs.length; index++) {
|
|
32337
|
+
const input = inputs[index];
|
|
32338
|
+
const taskId = created[index].id;
|
|
32339
|
+
const seenDependencies = new Set;
|
|
32340
|
+
for (const reference of input.depends_on ?? []) {
|
|
32341
|
+
const exact = tempIds.get(reference) ?? (await scoped.get("tasks", reference))?.id;
|
|
32342
|
+
const dependencyId = exact ?? (await scoped.resolveTaskRef(reference))?.id;
|
|
32343
|
+
if (!dependencyId)
|
|
32344
|
+
throw new TaskNotFoundError(reference);
|
|
32345
|
+
if (seenDependencies.has(dependencyId)) {
|
|
32346
|
+
throw new ResourceConflictError("BULK_CREATE_DUPLICATE_DEPENDENCY", `Multiple references resolve to dependency ${dependencyId}`);
|
|
32347
|
+
}
|
|
32348
|
+
seenDependencies.add(dependencyId);
|
|
32349
|
+
dependencies.push(await addDependency2(taskId, dependencyId, scoped, context));
|
|
32350
|
+
}
|
|
32351
|
+
}
|
|
32352
|
+
return { schema_version: 1, atomic: true, created, dependencies };
|
|
32353
|
+
});
|
|
32354
|
+
}
|
|
32355
|
+
async function bulkDeleteTasksAtomic(ids2, force, store, context) {
|
|
32356
|
+
return store.withDependencyGraphTransaction(async (scoped) => {
|
|
32357
|
+
const resolved = [];
|
|
32358
|
+
for (const reference of ids2) {
|
|
32359
|
+
resolved.push({
|
|
32360
|
+
reference,
|
|
32361
|
+
task: await scoped.get("tasks", reference) ?? await scoped.resolveTaskRef(reference)
|
|
32362
|
+
});
|
|
32363
|
+
}
|
|
32364
|
+
const seen = new Set;
|
|
32365
|
+
for (const item of resolved) {
|
|
32366
|
+
if (!item.task)
|
|
32367
|
+
continue;
|
|
32368
|
+
if (seen.has(item.task.id)) {
|
|
32369
|
+
throw new ResourceConflictError("BULK_DELETE_DUPLICATE_TASK", `Multiple references resolve to task ${item.task.id}`);
|
|
32370
|
+
}
|
|
32371
|
+
seen.add(item.task.id);
|
|
32372
|
+
}
|
|
32373
|
+
const childState = new Map;
|
|
32374
|
+
for (const item of resolved) {
|
|
32375
|
+
if (!item.task)
|
|
32376
|
+
continue;
|
|
32377
|
+
const children = await scoped.listTasks({ parent_id: item.task.id, include_subtasks: true, limit: 1 });
|
|
32378
|
+
childState.set(item.task.id, children.length > 0);
|
|
32379
|
+
}
|
|
32380
|
+
const planned = resolved.filter((item) => item.task && (force || !childState.get(item.task.id)));
|
|
32381
|
+
const plannedIds = new Set(planned.map((item) => item.task.id));
|
|
32382
|
+
const roots = [];
|
|
32383
|
+
for (const item of planned) {
|
|
32384
|
+
if (!force) {
|
|
32385
|
+
roots.push(item);
|
|
32386
|
+
continue;
|
|
32387
|
+
}
|
|
32388
|
+
let parentId = item.task.parent_id;
|
|
32389
|
+
let covered = false;
|
|
32390
|
+
while (parentId) {
|
|
32391
|
+
if (plannedIds.has(parentId)) {
|
|
32392
|
+
covered = true;
|
|
32393
|
+
break;
|
|
32394
|
+
}
|
|
32395
|
+
parentId = (await scoped.get("tasks", parentId))?.parent_id ?? null;
|
|
32396
|
+
}
|
|
32397
|
+
if (!covered)
|
|
32398
|
+
roots.push(item);
|
|
32399
|
+
}
|
|
32400
|
+
for (const item of roots) {
|
|
32401
|
+
if (!await scoped.deleteTaskHierarchy(item.task.id, context)) {
|
|
32402
|
+
throw new Error(`Atomic bulk delete lost task ${item.task.id}`);
|
|
32403
|
+
}
|
|
32404
|
+
}
|
|
32405
|
+
return {
|
|
32406
|
+
schema_version: 1,
|
|
32407
|
+
atomic: true,
|
|
32408
|
+
force,
|
|
32409
|
+
results: resolved.map((item) => {
|
|
32410
|
+
if (!item.task)
|
|
32411
|
+
return { requested_id: item.reference, task_id: null, outcome: "missing", reason: "not_found" };
|
|
32412
|
+
if (!force && childState.get(item.task.id)) {
|
|
32413
|
+
return { requested_id: item.reference, task_id: item.task.id, outcome: "skipped", reason: "has_children" };
|
|
32414
|
+
}
|
|
32415
|
+
return { requested_id: item.reference, task_id: item.task.id, outcome: "deleted", reason: null };
|
|
32416
|
+
})
|
|
32417
|
+
};
|
|
32418
|
+
});
|
|
32419
|
+
}
|
|
31987
32420
|
async function createTask2(input, store, context) {
|
|
31988
32421
|
const timestamp3 = new Date().toISOString();
|
|
31989
32422
|
const taskId = randomUUID4();
|
|
@@ -32074,12 +32507,16 @@ async function updateTask2(id, input, store, context) {
|
|
|
32074
32507
|
}
|
|
32075
32508
|
}
|
|
32076
32509
|
}
|
|
32510
|
+
const patchTouchesPlacement = input.project_id !== undefined || input.task_list_id !== undefined;
|
|
32511
|
+
const effectiveProjectIdForShortId = input.project_id !== undefined ? input.project_id : existing.project_id;
|
|
32512
|
+
const backfilledShortId = existing.short_id === null && patchTouchesPlacement && effectiveProjectIdForShortId ? await nextTaskShortId2(effectiveProjectIdForShortId, store, context) : null;
|
|
32077
32513
|
const reopened = existing.status === "completed" && input.status !== undefined && input.status !== "completed" && input.completed_at === undefined;
|
|
32078
32514
|
const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
|
|
32079
32515
|
const task2 = {
|
|
32080
32516
|
...existing,
|
|
32081
32517
|
...definedPatch(input),
|
|
32082
32518
|
...terminalNow ? { locked_by: null, locked_at: null } : {},
|
|
32519
|
+
short_id: backfilledShortId ?? existing.short_id,
|
|
32083
32520
|
version: existing.version + 1,
|
|
32084
32521
|
updated_at: new Date().toISOString(),
|
|
32085
32522
|
tags: input.tags ?? existing.tags,
|
|
@@ -34730,15 +35167,16 @@ function assertTodosPriorRegistrationAdoptionValidationEnvelope(value, input) {
|
|
|
34730
35167
|
// src/sdk/resolve.ts
|
|
34731
35168
|
init_local_opt_in();
|
|
34732
35169
|
var TODOS_LOCAL_SERVE_URL = "http://localhost:19427";
|
|
35170
|
+
var TODOS_CREDENTIAL_MISSING_MESSAGE = "TODOS_CREDENTIAL_MISSING: no Hasna Todos credential resolved. Looked at " + "HASNA_TODOS_API_KEY_OVERRIDE / HASNA_PROFILE / HASNA_TODOS_API_KEY_REF, the Keychain item " + "hasna.credentials.todos.api-key, ~/.hasna/todos/config/credentials, then HASNA_TODOS_API_KEY. " + "There is no local fallback: the on-box todos-serve is opt-in only (HASNA_TODOS_LOCAL=1, alias " + "TODOS_LOCAL=1) and is disabled by default \u2014 failing closed.";
|
|
34733
35171
|
var localNoticePrinted = false;
|
|
34734
35172
|
function stripV1(baseUrl) {
|
|
34735
35173
|
return baseUrl.replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
34736
35174
|
}
|
|
34737
|
-
function announceLocal(notice
|
|
35175
|
+
function announceLocal(notice) {
|
|
34738
35176
|
if (localNoticePrinted)
|
|
34739
35177
|
return;
|
|
34740
35178
|
localNoticePrinted = true;
|
|
34741
|
-
const line = `todos: LOCAL mode \u2014
|
|
35179
|
+
const line = `todos: LOCAL mode \u2014 HASNA_TODOS_LOCAL is set and nothing configures a Todos authority; reading and ` + `writing the local todos-serve at ${TODOS_LOCAL_SERVE_URL}, not the hosted fleet. Unset it, and ` + `provide a credential via the Keychain item hasna.credentials.todos.api-key, ` + `~/.hasna/todos/config/credentials, or HASNA_TODOS_API_KEY, to work against https://api.hasna.com/todos.`;
|
|
34742
35180
|
if (notice)
|
|
34743
35181
|
notice(line);
|
|
34744
35182
|
else if (typeof process !== "undefined")
|
|
@@ -34762,7 +35200,7 @@ function resolveTodosSdkTransport(options = {}) {
|
|
|
34762
35200
|
};
|
|
34763
35201
|
}
|
|
34764
35202
|
if (selectsTodosLocalStore(env2)) {
|
|
34765
|
-
announceLocal(options.notice
|
|
35203
|
+
announceLocal(options.notice);
|
|
34766
35204
|
return {
|
|
34767
35205
|
mode: "local-serve",
|
|
34768
35206
|
baseUrl: TODOS_LOCAL_SERVE_URL,
|
|
@@ -34780,14 +35218,7 @@ function resolveTodosSdkTransport(options = {}) {
|
|
|
34780
35218
|
resolution = resolveClientTransport("todos", env2, chainOptions);
|
|
34781
35219
|
} catch (error) {
|
|
34782
35220
|
if (error instanceof ClientTransportConfigurationError && /is not set and no API key could be resolved/.test(error.message)) {
|
|
34783
|
-
|
|
34784
|
-
return {
|
|
34785
|
-
mode: "local-serve",
|
|
34786
|
-
baseUrl: TODOS_LOCAL_SERVE_URL,
|
|
34787
|
-
apiKey: null,
|
|
34788
|
-
apiKeySource: null,
|
|
34789
|
-
apiUrlSource: "local-serve"
|
|
34790
|
-
};
|
|
35221
|
+
throw new Error(`${TODOS_CREDENTIAL_MISSING_MESSAGE} ${error.message}`, { cause: error });
|
|
34791
35222
|
}
|
|
34792
35223
|
throw error;
|
|
34793
35224
|
}
|
|
@@ -48211,6 +48642,10 @@ function rethrowAuthorityFailure(error) {
|
|
|
48211
48642
|
throw new Error(`REMOTE_API_URL_INVALID: ${message} local SQLite fallback is disabled`, { cause: error });
|
|
48212
48643
|
}
|
|
48213
48644
|
function classifyRemoteRequestError(baseUrl, route, error) {
|
|
48645
|
+
const errorName = error instanceof Error ? error.name : "";
|
|
48646
|
+
if (errorName === "CredentialResolutionError" || errorName === "CredentialFileUnsafeError") {
|
|
48647
|
+
rethrowAuthorityFailure(error);
|
|
48648
|
+
}
|
|
48214
48649
|
const status3 = error && typeof error === "object" ? error.status : undefined;
|
|
48215
48650
|
if (status3 === 401) {
|
|
48216
48651
|
throw new Error(`REMOTE_API_UNAUTHORIZED: configured Todos authority ${baseUrl} rejected HASNA_TODOS_API_KEY for ${route}; ` + "local SQLite fallback is disabled", { cause: error });
|
|
@@ -48359,6 +48794,10 @@ function toListQuery(filter = {}) {
|
|
|
48359
48794
|
query["parent_id"] = filter.parent_id ?? "";
|
|
48360
48795
|
if (filter.include_subtasks !== undefined)
|
|
48361
48796
|
query["include_subtasks"] = filter.include_subtasks ? "true" : "false";
|
|
48797
|
+
if (filter.include_archived !== undefined)
|
|
48798
|
+
query["include_archived"] = filter.include_archived ? "true" : "false";
|
|
48799
|
+
if (filter.archived_only !== undefined)
|
|
48800
|
+
query["archived_only"] = filter.archived_only ? "true" : "false";
|
|
48362
48801
|
if (filter.plan_id)
|
|
48363
48802
|
query["plan_id"] = filter.plan_id;
|
|
48364
48803
|
if (filter.task_list_id)
|
|
@@ -54871,7 +55310,7 @@ function formatTraceabilityReport(report) {
|
|
|
54871
55310
|
}
|
|
54872
55311
|
// src/lib/mention-resolver.ts
|
|
54873
55312
|
import { existsSync as existsSync17, readdirSync as readdirSync3, readFileSync as readFileSync13, statSync as statSync6 } from "fs";
|
|
54874
|
-
import { basename as basename4, isAbsolute, join as join15, relative as relative4, resolve as resolve17, sep as sep2 } from "path";
|
|
55313
|
+
import { basename as basename4, isAbsolute as isAbsolute2, join as join15, relative as relative4, resolve as resolve17, sep as sep2 } from "path";
|
|
54875
55314
|
init_database();
|
|
54876
55315
|
init_plans();
|
|
54877
55316
|
init_task_runs();
|
|
@@ -54954,7 +55393,7 @@ function normalizeWorkspace(workspace) {
|
|
|
54954
55393
|
}
|
|
54955
55394
|
function isInside(root, absolutePath) {
|
|
54956
55395
|
const rel = relative4(root, absolutePath);
|
|
54957
|
-
return rel === "" || !rel.startsWith("..") && !rel.includes(`..${sep2}`) && !
|
|
55396
|
+
return rel === "" || !rel.startsWith("..") && !rel.includes(`..${sep2}`) && !isAbsolute2(rel);
|
|
54958
55397
|
}
|
|
54959
55398
|
function normalizeRelativePath(value) {
|
|
54960
55399
|
const normalized2 = value.trim().replace(/^\.?\//, "").replace(/\\/g, "/");
|
|
@@ -64038,7 +64477,6 @@ var ALL_MCP_TOOLS = [
|
|
|
64038
64477
|
"export_todos_md",
|
|
64039
64478
|
"export_verification_evidence",
|
|
64040
64479
|
"extend_task",
|
|
64041
|
-
"extract_todos",
|
|
64042
64480
|
"fail_agent_run",
|
|
64043
64481
|
"fail_task",
|
|
64044
64482
|
"find_duplicate_tasks",
|