@hasna/todos 0.15.19 → 0.15.20
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 +20 -1
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +851 -47
- package/dist/cli/stage-a.d.ts +21 -10
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +228 -16
- package/dist/db/audit.d.ts +8 -0
- package/dist/db/audit.d.ts.map +1 -1
- package/dist/db/task-lifecycle.d.ts +7 -1
- package/dist/db/task-lifecycle.d.ts.map +1 -1
- package/dist/db/tasks.d.ts +1 -1
- package/dist/db/tasks.d.ts.map +1 -1
- package/dist/index.js +479 -20
- package/dist/lib/cli-help.d.ts +3 -2
- package/dist/lib/cli-help.d.ts.map +1 -1
- package/dist/lib/stale-lock-handoff.d.ts +25 -0
- package/dist/lib/stale-lock-handoff.d.ts.map +1 -0
- package/dist/mcp/index.js +702 -36
- package/dist/mcp.js +3 -1
- package/dist/project-registration.js +1147 -686
- package/dist/registry.js +228 -16
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/index.js +7 -0
- package/dist/sdk/v1.generated.d.ts +27 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +1035 -369
- package/dist/server/openapi.d.ts +182 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/audit-history-import.d.ts +14 -0
- package/dist/storage/audit-history-import.d.ts.map +1 -0
- package/dist/storage/interfaces.d.ts +12 -1
- 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/sqlite-snapshot.d.ts.map +1 -1
- package/dist/storage.js +476 -19
- package/dist/task-manifest.js +11 -1
- package/dist/types/index.d.ts +43 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +3 -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.20",
|
|
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",
|
|
@@ -2194,6 +2194,8 @@ var init_package = __esm(() => {
|
|
|
2194
2194
|
"dev:mcp": "bun run src/mcp/index.ts",
|
|
2195
2195
|
"dev:serve": "bun run src/server/index.ts",
|
|
2196
2196
|
"verify:release": "bun run scripts/verify-public-release.ts --mode=review",
|
|
2197
|
+
"verify:release-review": "bun run scripts/verify-npm-release-agent-review.ts",
|
|
2198
|
+
"issue:release-review": "bun run scripts/issue-npm-release-agent-review.ts",
|
|
2197
2199
|
prepublishOnly: "bun run scripts/verify-public-release.ts --mode=publish",
|
|
2198
2200
|
postinstall: "mkdir -p $HOME/.hasna/todos $HOME/.hasna/todos/training 2>/dev/null || true"
|
|
2199
2201
|
},
|
|
@@ -2269,7 +2271,7 @@ function isBlockingDependencyStatus(status) {
|
|
|
2269
2271
|
function isTerminalStatus(status) {
|
|
2270
2272
|
return status === "completed" || status === "failed" || status === "cancelled";
|
|
2271
2273
|
}
|
|
2272
|
-
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
|
|
2274
|
+
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, StaleLockHandoffError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
|
|
2273
2275
|
var init_types = __esm(() => {
|
|
2274
2276
|
TASK_STATUSES = [
|
|
2275
2277
|
"pending",
|
|
@@ -2392,6 +2394,16 @@ var init_types = __esm(() => {
|
|
|
2392
2394
|
this.name = "LockError";
|
|
2393
2395
|
}
|
|
2394
2396
|
};
|
|
2397
|
+
StaleLockHandoffError = class StaleLockHandoffError extends Error {
|
|
2398
|
+
code;
|
|
2399
|
+
details;
|
|
2400
|
+
constructor(code, message, details = {}) {
|
|
2401
|
+
super(message);
|
|
2402
|
+
this.code = code;
|
|
2403
|
+
this.details = details;
|
|
2404
|
+
this.name = "StaleLockHandoffError";
|
|
2405
|
+
}
|
|
2406
|
+
};
|
|
2395
2407
|
AgentNotFoundError = class AgentNotFoundError extends Error {
|
|
2396
2408
|
agentId;
|
|
2397
2409
|
static code = "AGENT_NOT_FOUND";
|
|
@@ -5480,6 +5492,7 @@ var init_http_client = __esm(() => {
|
|
|
5480
5492
|
// src/cli/cloud-router.ts
|
|
5481
5493
|
import { resolveStorageClient } from "@hasna/contracts/client/storage";
|
|
5482
5494
|
import { normalizeStorageMode } from "@hasna/contracts/mode";
|
|
5495
|
+
import { randomUUID } from "crypto";
|
|
5483
5496
|
import { resolve as resolvePath } from "path";
|
|
5484
5497
|
function cleanMode(value) {
|
|
5485
5498
|
const normalized = value?.trim().toLowerCase();
|
|
@@ -5832,6 +5845,45 @@ async function requestRawCloudTaskPage(client, filter) {
|
|
|
5832
5845
|
total: parseCloudTaskTotal(res.raw)
|
|
5833
5846
|
};
|
|
5834
5847
|
}
|
|
5848
|
+
function cloudPlanTaskListError(planId, detail) {
|
|
5849
|
+
return new Error(`REMOTE_PLAN_TASK_LIST_INCOMPLETE: hosted authority returned an incomplete task set for plan ${planId}; ` + `${detail}; refusing an incomplete plan result`);
|
|
5850
|
+
}
|
|
5851
|
+
function appendExactPlanTaskPage(target, seen, page, planId) {
|
|
5852
|
+
for (const task of page) {
|
|
5853
|
+
if (task.plan_id !== planId) {
|
|
5854
|
+
throw cloudPlanTaskListError(planId, `response includes task ${task.id} from plan ${task.plan_id ?? "null"}`);
|
|
5855
|
+
}
|
|
5856
|
+
if (seen.has(task.id)) {
|
|
5857
|
+
throw cloudPlanTaskListError(planId, `pagination repeats task id ${task.id}`);
|
|
5858
|
+
}
|
|
5859
|
+
seen.add(task.id);
|
|
5860
|
+
target.push(task);
|
|
5861
|
+
}
|
|
5862
|
+
}
|
|
5863
|
+
async function cloudListPlanTasks(client, planId) {
|
|
5864
|
+
const filter = { plan_id: planId, include_subtasks: true };
|
|
5865
|
+
const firstPage = await requestRawCloudTaskPage(client, filter);
|
|
5866
|
+
const tasks = [];
|
|
5867
|
+
const seen = new Set;
|
|
5868
|
+
appendExactPlanTaskPage(tasks, seen, firstPage.tasks, planId);
|
|
5869
|
+
if (firstPage.total === undefined)
|
|
5870
|
+
return tasks;
|
|
5871
|
+
const total = firstPage.total;
|
|
5872
|
+
if (tasks.length > total) {
|
|
5873
|
+
throw cloudPlanTaskListError(planId, `first page contains ${tasks.length} rows but reports total ${total}`);
|
|
5874
|
+
}
|
|
5875
|
+
while (tasks.length < total) {
|
|
5876
|
+
const page = await requestRawCloudTaskPage(client, { ...filter, offset: tasks.length });
|
|
5877
|
+
if (page.total !== total) {
|
|
5878
|
+
throw cloudPlanTaskListError(planId, `pagination total changed from ${total} to ${String(page.total)}`);
|
|
5879
|
+
}
|
|
5880
|
+
if (page.tasks.length === 0 || tasks.length + page.tasks.length > total) {
|
|
5881
|
+
throw cloudPlanTaskListError(planId, "pagination did not make bounded progress toward the reported total");
|
|
5882
|
+
}
|
|
5883
|
+
appendExactPlanTaskPage(tasks, seen, page.tasks, planId);
|
|
5884
|
+
}
|
|
5885
|
+
return tasks;
|
|
5886
|
+
}
|
|
5835
5887
|
function cloudTaskListFilterError(code, taskListId, detail) {
|
|
5836
5888
|
return new Error(`${code}: hosted authority returned rows outside requested task_list_id ${taskListId}; ${detail}; refusing an incomplete exact-list result`);
|
|
5837
5889
|
}
|
|
@@ -5948,7 +6000,10 @@ async function cloudGetTask(client, id) {
|
|
|
5948
6000
|
}
|
|
5949
6001
|
async function cloudCreateTask(client, input) {
|
|
5950
6002
|
const expectedParentId = typeof input["parent_id"] === "string" ? input["parent_id"] : null;
|
|
5951
|
-
const created = unwrapTask(await requiredRemoteRoute(client, "/v1/tasks", () => client.
|
|
6003
|
+
const created = unwrapTask(await requiredRemoteRoute(client, "/v1/tasks", () => client.transport.post("/tasks", input, {
|
|
6004
|
+
idempotencyKey: randomUUID(),
|
|
6005
|
+
retry: false
|
|
6006
|
+
}), ["PARENT_TASK_NOT_FOUND"]));
|
|
5952
6007
|
if (!created || typeof created.id !== "string" || !created.id.trim()) {
|
|
5953
6008
|
throw new Error(`REMOTE_API_INCOMPATIBLE: configured Todos authority ${remoteAuthorityBase(client)} returned a task create ` + "response without a stored task id; no success row or local SQLite fallback is permitted");
|
|
5954
6009
|
}
|
|
@@ -5959,7 +6014,7 @@ async function cloudCreateTask(client, input) {
|
|
|
5959
6014
|
return persisted;
|
|
5960
6015
|
}
|
|
5961
6016
|
async function cloudUpdateTask(client, id, patch) {
|
|
5962
|
-
return unwrapTask(await client.
|
|
6017
|
+
return unwrapTask(await client.transport.patch(`/tasks/${encodeURIComponent(id)}`, patch));
|
|
5963
6018
|
}
|
|
5964
6019
|
async function cloudDeleteTask(client, id) {
|
|
5965
6020
|
try {
|
|
@@ -6710,6 +6765,19 @@ async function cloudUnlockTask(client, id, agentId, force = false) {
|
|
|
6710
6765
|
}
|
|
6711
6766
|
return true;
|
|
6712
6767
|
}
|
|
6768
|
+
async function cloudHandoffStaleTaskLock(client, input) {
|
|
6769
|
+
const raw = await client.transport.post(`/tasks/${encodeURIComponent(input.task_id)}/stale-lock-handoff`, {
|
|
6770
|
+
expected_holder: input.expected_holder,
|
|
6771
|
+
expected_lock_version: input.expected_lock_version,
|
|
6772
|
+
stale_after_seconds: input.stale_after_seconds,
|
|
6773
|
+
new_holder: input.new_holder,
|
|
6774
|
+
reason: input.reason
|
|
6775
|
+
});
|
|
6776
|
+
if (raw && typeof raw === "object" && "receipt" in raw) {
|
|
6777
|
+
return raw.receipt;
|
|
6778
|
+
}
|
|
6779
|
+
throw new Error("STALE_LOCK_HANDOFF_RECEIPT_MISSING: remote response did not include receipt");
|
|
6780
|
+
}
|
|
6713
6781
|
async function cloudGetDependencies(client, id) {
|
|
6714
6782
|
const raw = await client.transport.get(`/tasks/${encodeURIComponent(id)}/dependencies`);
|
|
6715
6783
|
const env = raw ?? {};
|
|
@@ -7190,6 +7258,12 @@ var init_cloud_router = __esm(() => {
|
|
|
7190
7258
|
});
|
|
7191
7259
|
|
|
7192
7260
|
// src/cli/stage-a.ts
|
|
7261
|
+
function applyTodosCliAuthorityEnvironment(authority, env = process.env) {
|
|
7262
|
+
if (authority.route !== "local" || authority.selected_by !== "local-only-command")
|
|
7263
|
+
return;
|
|
7264
|
+
env.HASNA_TODOS_STORAGE_MODE = "sqlite";
|
|
7265
|
+
env.TODOS_STORAGE_MODE = "sqlite";
|
|
7266
|
+
}
|
|
7193
7267
|
function isTodosCliCommandVisibleForRoute(command, route) {
|
|
7194
7268
|
if (route === "local")
|
|
7195
7269
|
return true;
|
|
@@ -7399,7 +7473,7 @@ function nearestCommands(command, limit = 3) {
|
|
|
7399
7473
|
const threshold = command.length <= 4 ? 1 : command.length <= 8 ? 2 : 3;
|
|
7400
7474
|
return [...COMMAND_CAPABILITY_MATRIX.entries()].filter(([, owner]) => owner !== "local-only").map(([candidate]) => candidate).map((candidate) => ({ candidate, distance: editDistance(command, candidate) })).filter(({ distance }) => distance <= threshold).sort((left, right) => left.distance - right.distance || left.candidate.localeCompare(right.candidate)).slice(0, limit).map(({ candidate }) => candidate);
|
|
7401
7475
|
}
|
|
7402
|
-
function
|
|
7476
|
+
function assertInvocationRoutable(invocation) {
|
|
7403
7477
|
if (invocation.invalidGlobalOption) {
|
|
7404
7478
|
throw new Error(`REMOTE_COMMAND_UNSUPPORTED: the global option ${invocation.invalidGlobalOption} was given without a value; ` + `pass one as \`${invocation.invalidGlobalOption} <value>\``);
|
|
7405
7479
|
}
|
|
@@ -7413,8 +7487,12 @@ function assertRemoteCommandSupported(invocation) {
|
|
|
7413
7487
|
const didYouMean = suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}?` : "";
|
|
7414
7488
|
throw new Error(`UNKNOWN_COMMAND: \`${command}\` is not a built-in todos command on the /v1 route.${didYouMean} ` + "Run `todos --help` for the commands available here; verbs contributed by optional packages are local-only. " + "(This is not a connectivity, storage-mode or credential problem.)");
|
|
7415
7489
|
}
|
|
7490
|
+
return owner;
|
|
7491
|
+
}
|
|
7492
|
+
function assertRemoteCommandSupported(invocation, owner) {
|
|
7493
|
+
const command = invocation.command;
|
|
7416
7494
|
if (command && owner === "local-only") {
|
|
7417
|
-
throw new Error(`
|
|
7495
|
+
throw new Error(`LOCAL_COMMAND_ROUTING_INVARIANT: \`${command}\` must select the local command route before remote authority validation`);
|
|
7418
7496
|
}
|
|
7419
7497
|
if (!command || !commandSupportsRemote(invocation)) {
|
|
7420
7498
|
const blame = command ? disqualifyingArgument(invocation) : null;
|
|
@@ -7432,7 +7510,11 @@ function initializeTodosCliAuthority(args = process.argv.slice(2), env = process
|
|
|
7432
7510
|
const status = getTodosRemoteAuthorityConfigStatus(env);
|
|
7433
7511
|
return { route: "remote-diagnostic", v1_base_url: status.v1_base_url };
|
|
7434
7512
|
}
|
|
7435
|
-
|
|
7513
|
+
const owner = assertInvocationRoutable(invocation);
|
|
7514
|
+
if (owner === "local-only") {
|
|
7515
|
+
return { route: "local", v1_base_url: null, selected_by: "local-only-command" };
|
|
7516
|
+
}
|
|
7517
|
+
assertRemoteCommandSupported(invocation, owner);
|
|
7436
7518
|
const client = getTodosCloudClient(env);
|
|
7437
7519
|
if (!client) {
|
|
7438
7520
|
throw new Error("REMOTE_API_UNAVAILABLE: remote mode did not resolve an HTTP client; local SQLite fallback is disabled");
|
|
@@ -7566,6 +7648,7 @@ var init_stage_a = __esm(() => {
|
|
|
7566
7648
|
"sprint",
|
|
7567
7649
|
"stale",
|
|
7568
7650
|
"standup",
|
|
7651
|
+
"stale-lock-handoff",
|
|
7569
7652
|
"start",
|
|
7570
7653
|
"status",
|
|
7571
7654
|
"steal",
|
|
@@ -7698,6 +7781,7 @@ var init_stage_a = __esm(() => {
|
|
|
7698
7781
|
"tag",
|
|
7699
7782
|
"task",
|
|
7700
7783
|
"task-lists",
|
|
7784
|
+
"stale-lock-handoff",
|
|
7701
7785
|
"template-export",
|
|
7702
7786
|
"template-import",
|
|
7703
7787
|
"template-preview",
|
|
@@ -7705,6 +7789,7 @@ var init_stage_a = __esm(() => {
|
|
|
7705
7789
|
"timeline",
|
|
7706
7790
|
"tl",
|
|
7707
7791
|
"unlock",
|
|
7792
|
+
"unassign",
|
|
7708
7793
|
"untag",
|
|
7709
7794
|
"update"
|
|
7710
7795
|
]);
|
|
@@ -13590,7 +13675,7 @@ __export(exports_event_hooks, {
|
|
|
13590
13675
|
emitLocalEventHooks: () => emitLocalEventHooks,
|
|
13591
13676
|
LOCAL_EVENT_TYPES: () => LOCAL_EVENT_TYPES
|
|
13592
13677
|
});
|
|
13593
|
-
import { createHash as createHash4, randomUUID } from "crypto";
|
|
13678
|
+
import { createHash as createHash4, randomUUID as randomUUID2 } from "crypto";
|
|
13594
13679
|
import { appendFileSync, mkdirSync as mkdirSync4 } from "fs";
|
|
13595
13680
|
import { dirname as dirname4, resolve as resolve7 } from "path";
|
|
13596
13681
|
import { createConnection } from "net";
|
|
@@ -13651,7 +13736,7 @@ function canonicalEvent(input) {
|
|
|
13651
13736
|
}
|
|
13652
13737
|
function buildEnvelope(type, payload, timestamp2 = new Date().toISOString()) {
|
|
13653
13738
|
const base = {
|
|
13654
|
-
id:
|
|
13739
|
+
id: randomUUID2(),
|
|
13655
13740
|
type,
|
|
13656
13741
|
timestamp: timestamp2,
|
|
13657
13742
|
payload: redactValue(payload ?? {}),
|
|
@@ -13854,7 +13939,7 @@ import { existsSync as existsSync6 } from "fs";
|
|
|
13854
13939
|
import { homedir as homedir2 } from "os";
|
|
13855
13940
|
import { join as join5 } from "path";
|
|
13856
13941
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
13857
|
-
import { randomUUID as
|
|
13942
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
13858
13943
|
import { spawn } from "child_process";
|
|
13859
13944
|
import { randomUUID as randomUUID22 } from "crypto";
|
|
13860
13945
|
function getPathValue(input, path) {
|
|
@@ -14212,7 +14297,7 @@ async function dispatchChannel(event, channel, options = {}) {
|
|
|
14212
14297
|
function createDeliveryResult(event, channel, attempts) {
|
|
14213
14298
|
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
14214
14299
|
return {
|
|
14215
|
-
id:
|
|
14300
|
+
id: randomUUID3(),
|
|
14216
14301
|
eventId: event.id,
|
|
14217
14302
|
channelId: channel.id,
|
|
14218
14303
|
transport: channel.transport,
|
|
@@ -15214,6 +15299,7 @@ var init_activity_audit = __esm(() => {
|
|
|
15214
15299
|
var exports_audit = {};
|
|
15215
15300
|
__export(exports_audit, {
|
|
15216
15301
|
logTaskChange: () => logTaskChange,
|
|
15302
|
+
insertTaskHistory: () => insertTaskHistory,
|
|
15217
15303
|
getTaskHistory: () => getTaskHistory,
|
|
15218
15304
|
getRecentActivity: () => getRecentActivity,
|
|
15219
15305
|
getRecap: () => getRecap
|
|
@@ -15221,28 +15307,55 @@ __export(exports_audit, {
|
|
|
15221
15307
|
function sanitizeHistoryValue(value, context) {
|
|
15222
15308
|
return value === undefined || value === null ? null : sanitizePreWriteText(String(value), context);
|
|
15223
15309
|
}
|
|
15224
|
-
function
|
|
15310
|
+
function insertTaskHistory(entry, db) {
|
|
15225
15311
|
const d = db || getDatabase();
|
|
15226
|
-
const
|
|
15227
|
-
|
|
15228
|
-
|
|
15229
|
-
|
|
15230
|
-
|
|
15312
|
+
const safeEntry = {
|
|
15313
|
+
...entry,
|
|
15314
|
+
field: entry.field || null,
|
|
15315
|
+
old_value: sanitizeHistoryValue(entry.old_value, "task_history.old_value"),
|
|
15316
|
+
new_value: sanitizeHistoryValue(entry.new_value, "task_history.new_value"),
|
|
15317
|
+
agent_id: entry.agent_id || null,
|
|
15318
|
+
machine_id: entry.machine_id ?? currentStorageMachineId(d)
|
|
15319
|
+
};
|
|
15231
15320
|
d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at, machine_id)
|
|
15232
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
15321
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
15322
|
+
safeEntry.id,
|
|
15323
|
+
safeEntry.task_id,
|
|
15324
|
+
safeEntry.action,
|
|
15325
|
+
safeEntry.field,
|
|
15326
|
+
safeEntry.old_value,
|
|
15327
|
+
safeEntry.new_value,
|
|
15328
|
+
safeEntry.agent_id,
|
|
15329
|
+
safeEntry.created_at,
|
|
15330
|
+
safeEntry.machine_id ?? null
|
|
15331
|
+
]);
|
|
15233
15332
|
try {
|
|
15234
15333
|
const { logActivity: logActivity2 } = (init_activity_audit(), __toCommonJS(exports_activity_audit));
|
|
15235
15334
|
logActivity2({
|
|
15236
15335
|
entity_type: "task",
|
|
15237
|
-
entity_id:
|
|
15238
|
-
action,
|
|
15239
|
-
field,
|
|
15240
|
-
old_value:
|
|
15241
|
-
new_value:
|
|
15242
|
-
actor_id:
|
|
15336
|
+
entity_id: safeEntry.task_id,
|
|
15337
|
+
action: safeEntry.action,
|
|
15338
|
+
field: safeEntry.field ?? undefined,
|
|
15339
|
+
old_value: safeEntry.old_value,
|
|
15340
|
+
new_value: safeEntry.new_value,
|
|
15341
|
+
actor_id: safeEntry.agent_id ?? undefined
|
|
15243
15342
|
}, d);
|
|
15244
15343
|
} catch {}
|
|
15245
|
-
return
|
|
15344
|
+
return safeEntry;
|
|
15345
|
+
}
|
|
15346
|
+
function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
|
|
15347
|
+
const d = db || getDatabase();
|
|
15348
|
+
return insertTaskHistory({
|
|
15349
|
+
id: uuid(),
|
|
15350
|
+
task_id: taskId,
|
|
15351
|
+
action,
|
|
15352
|
+
field: field || null,
|
|
15353
|
+
old_value: oldValue ?? null,
|
|
15354
|
+
new_value: newValue ?? null,
|
|
15355
|
+
agent_id: agentId || null,
|
|
15356
|
+
created_at: now(),
|
|
15357
|
+
machine_id: currentStorageMachineId(d)
|
|
15358
|
+
}, d);
|
|
15246
15359
|
}
|
|
15247
15360
|
function getTaskHistory(taskId, db) {
|
|
15248
15361
|
const d = db || getDatabase();
|
|
@@ -16303,6 +16416,145 @@ var init_task_graph = __esm(() => {
|
|
|
16303
16416
|
init_task_crud();
|
|
16304
16417
|
});
|
|
16305
16418
|
|
|
16419
|
+
// src/lib/stale-lock-handoff.ts
|
|
16420
|
+
function normalizeExactTaskId(value) {
|
|
16421
|
+
if (typeof value !== "string" || !EXACT_TASK_UUID_RE.test(value.trim())) {
|
|
16422
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_TASK_ID", "stale-lock handoff requires one exact full task UUID", { task_id: typeof value === "string" ? value : null });
|
|
16423
|
+
}
|
|
16424
|
+
return value.trim().toLowerCase();
|
|
16425
|
+
}
|
|
16426
|
+
function requireNonEmptyString(value, field) {
|
|
16427
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
16428
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", `${field} must be a non-empty string`, { field });
|
|
16429
|
+
}
|
|
16430
|
+
const trimmed = value.trim();
|
|
16431
|
+
if (field === "reason" && trimmed.length > MAX_REASON_LENGTH) {
|
|
16432
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", `reason must be at most ${MAX_REASON_LENGTH} characters`, { field, max_length: MAX_REASON_LENGTH });
|
|
16433
|
+
}
|
|
16434
|
+
return trimmed;
|
|
16435
|
+
}
|
|
16436
|
+
function requireCanonicalLockVersion(value) {
|
|
16437
|
+
if (typeof value !== "string" || !CANONICAL_LOCK_VERSION_RE.test(value)) {
|
|
16438
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "expected_lock_version must be the exact canonical locked_at timestamp (YYYY-MM-DDTHH:mm:ss.sssZ)", { field: "expected_lock_version" });
|
|
16439
|
+
}
|
|
16440
|
+
const parsed = Date.parse(value);
|
|
16441
|
+
if (Number.isNaN(parsed) || new Date(parsed).toISOString() !== value) {
|
|
16442
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "expected_lock_version must name a real canonical UTC instant", { field: "expected_lock_version" });
|
|
16443
|
+
}
|
|
16444
|
+
return value;
|
|
16445
|
+
}
|
|
16446
|
+
function requireStaleThreshold(value) {
|
|
16447
|
+
if (!Number.isSafeInteger(value) || Number(value) <= 0) {
|
|
16448
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "stale_after_seconds must be a positive safe integer", { field: "stale_after_seconds" });
|
|
16449
|
+
}
|
|
16450
|
+
return Number(value);
|
|
16451
|
+
}
|
|
16452
|
+
function prepareStaleLockHandoff(input, options = {}) {
|
|
16453
|
+
const taskId = normalizeExactTaskId(input.task_id);
|
|
16454
|
+
const actor = requireNonEmptyString(input.actor, "actor");
|
|
16455
|
+
const expectedHolder = requireNonEmptyString(input.expected_holder, "expected_holder");
|
|
16456
|
+
const newHolder = requireNonEmptyString(input.new_holder, "new_holder");
|
|
16457
|
+
const expectedLockVersion = requireCanonicalLockVersion(input.expected_lock_version);
|
|
16458
|
+
const staleAfterSeconds = requireStaleThreshold(input.stale_after_seconds);
|
|
16459
|
+
const reason = sanitizePreWriteText(requireNonEmptyString(input.reason, "reason"), "stale_lock_handoff.reason").trim();
|
|
16460
|
+
if (!reason) {
|
|
16461
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "reason must remain non-empty after safety filtering", { field: "reason" });
|
|
16462
|
+
}
|
|
16463
|
+
if (canonicalAgentRef(actor) !== canonicalAgentRef(newHolder)) {
|
|
16464
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_ACTOR_MISMATCH", "new_holder must match the authenticated actor", { actor, new_holder: newHolder });
|
|
16465
|
+
}
|
|
16466
|
+
if (canonicalAgentRef(expectedHolder) === canonicalAgentRef(newHolder)) {
|
|
16467
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "new_holder must differ from expected_holder", { field: "new_holder" });
|
|
16468
|
+
}
|
|
16469
|
+
const operationTimestamp = options.now ?? new Date().toISOString();
|
|
16470
|
+
if (!CANONICAL_LOCK_VERSION_RE.test(operationTimestamp) || new Date(Date.parse(operationTimestamp)).toISOString() !== operationTimestamp) {
|
|
16471
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "operation timestamp must be a canonical UTC instant");
|
|
16472
|
+
}
|
|
16473
|
+
const staleCutoff = new Date(Date.parse(operationTimestamp) - staleAfterSeconds * 1000).toISOString();
|
|
16474
|
+
return {
|
|
16475
|
+
task_id: taskId,
|
|
16476
|
+
actor,
|
|
16477
|
+
expected_holder: expectedHolder,
|
|
16478
|
+
expected_lock_version: expectedLockVersion,
|
|
16479
|
+
stale_after_seconds: staleAfterSeconds,
|
|
16480
|
+
new_holder: newHolder,
|
|
16481
|
+
reason,
|
|
16482
|
+
operation_timestamp: operationTimestamp,
|
|
16483
|
+
stale_cutoff: staleCutoff,
|
|
16484
|
+
receipt_id: options.receiptId ?? crypto.randomUUID()
|
|
16485
|
+
};
|
|
16486
|
+
}
|
|
16487
|
+
function buildStaleLockHandoffReceipt(input) {
|
|
16488
|
+
return {
|
|
16489
|
+
schema_version: STALE_LOCK_HANDOFF_SCHEMA_VERSION,
|
|
16490
|
+
receipt_id: input.receipt_id,
|
|
16491
|
+
task_id: input.task_id,
|
|
16492
|
+
actor: input.actor,
|
|
16493
|
+
previous_holder: input.expected_holder,
|
|
16494
|
+
previous_lock_version: input.expected_lock_version,
|
|
16495
|
+
new_holder: input.new_holder,
|
|
16496
|
+
new_lock_version: input.operation_timestamp,
|
|
16497
|
+
stale_after_seconds: input.stale_after_seconds,
|
|
16498
|
+
stale_cutoff: input.stale_cutoff,
|
|
16499
|
+
reason: input.reason,
|
|
16500
|
+
created_at: input.operation_timestamp
|
|
16501
|
+
};
|
|
16502
|
+
}
|
|
16503
|
+
function staleLockHandoffHistory(receipt, machineId) {
|
|
16504
|
+
return {
|
|
16505
|
+
id: receipt.receipt_id,
|
|
16506
|
+
task_id: receipt.task_id,
|
|
16507
|
+
action: STALE_LOCK_HANDOFF_ACTION,
|
|
16508
|
+
field: STALE_LOCK_HANDOFF_FIELD,
|
|
16509
|
+
old_value: JSON.stringify({
|
|
16510
|
+
holder: receipt.previous_holder,
|
|
16511
|
+
lock_version: receipt.previous_lock_version
|
|
16512
|
+
}),
|
|
16513
|
+
new_value: JSON.stringify(receipt),
|
|
16514
|
+
agent_id: receipt.actor,
|
|
16515
|
+
created_at: receipt.created_at,
|
|
16516
|
+
machine_id: machineId
|
|
16517
|
+
};
|
|
16518
|
+
}
|
|
16519
|
+
function throwStaleLockHandoffConflict(task, input) {
|
|
16520
|
+
if (!task.locked_by || !task.locked_at) {
|
|
16521
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_NOT_LOCKED", `Task ${input.task_id} does not have a complete lock to hand off`, { task_id: input.task_id });
|
|
16522
|
+
}
|
|
16523
|
+
if (task.locked_at !== input.expected_lock_version) {
|
|
16524
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_VERSION_MISMATCH", `Task ${input.task_id} lock version changed`, {
|
|
16525
|
+
task_id: input.task_id,
|
|
16526
|
+
expected_lock_version: input.expected_lock_version,
|
|
16527
|
+
current_lock_version: task.locked_at
|
|
16528
|
+
});
|
|
16529
|
+
}
|
|
16530
|
+
if (task.locked_by !== input.expected_holder) {
|
|
16531
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_HOLDER_MISMATCH", `Task ${input.task_id} lock holder changed`, {
|
|
16532
|
+
task_id: input.task_id,
|
|
16533
|
+
expected_holder: input.expected_holder,
|
|
16534
|
+
current_holder: task.locked_by
|
|
16535
|
+
});
|
|
16536
|
+
}
|
|
16537
|
+
if (isTerminalStatus(task.status)) {
|
|
16538
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_TERMINAL", `Task ${input.task_id} is ${task.status} and cannot transfer a lock`, { task_id: input.task_id, status: task.status });
|
|
16539
|
+
}
|
|
16540
|
+
if (task.locked_at >= input.stale_cutoff) {
|
|
16541
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_NOT_STALE", `Task ${input.task_id} lock is not older than the supplied stale threshold`, {
|
|
16542
|
+
task_id: input.task_id,
|
|
16543
|
+
current_lock_version: task.locked_at,
|
|
16544
|
+
stale_cutoff: input.stale_cutoff
|
|
16545
|
+
});
|
|
16546
|
+
}
|
|
16547
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_CONFLICT", `Task ${input.task_id} changed during stale-lock handoff`, { task_id: input.task_id });
|
|
16548
|
+
}
|
|
16549
|
+
var STALE_LOCK_HANDOFF_SCHEMA_VERSION = "todos.stale-lock-handoff.v1", STALE_LOCK_HANDOFF_ACTION = "stale_lock_handoff", STALE_LOCK_HANDOFF_FIELD = "lock", EXACT_TASK_UUID_RE, CANONICAL_LOCK_VERSION_RE, MAX_REASON_LENGTH = 4096;
|
|
16550
|
+
var init_stale_lock_handoff = __esm(() => {
|
|
16551
|
+
init_types();
|
|
16552
|
+
init_creator_identity();
|
|
16553
|
+
init_prewrite_secrets();
|
|
16554
|
+
EXACT_TASK_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
16555
|
+
CANONICAL_LOCK_VERSION_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
16556
|
+
});
|
|
16557
|
+
|
|
16306
16558
|
// src/db/task-lifecycle.ts
|
|
16307
16559
|
var exports_task_lifecycle = {};
|
|
16308
16560
|
__export(exports_task_lifecycle, {
|
|
@@ -16311,6 +16563,7 @@ __export(exports_task_lifecycle, {
|
|
|
16311
16563
|
startTask: () => startTask,
|
|
16312
16564
|
spawnNextRecurrence: () => spawnNextRecurrence,
|
|
16313
16565
|
lockTask: () => lockTask,
|
|
16566
|
+
handoffStaleTaskLock: () => handoffStaleTaskLock,
|
|
16314
16567
|
getTasksChangedSince: () => getTasksChangedSince,
|
|
16315
16568
|
getTaskLockStatus: () => getTaskLockStatus,
|
|
16316
16569
|
getStaleTasks: () => getStaleTasks,
|
|
@@ -16579,6 +16832,38 @@ function unlockTask(id, agentId, db) {
|
|
|
16579
16832
|
WHERE id = ?`, [timestamp2, id]);
|
|
16580
16833
|
return true;
|
|
16581
16834
|
}
|
|
16835
|
+
function handoffStaleTaskLock(input, db) {
|
|
16836
|
+
const d = db || getDatabase();
|
|
16837
|
+
const prepared = prepareStaleLockHandoff(input);
|
|
16838
|
+
const receipt = buildStaleLockHandoffReceipt(prepared);
|
|
16839
|
+
const history = staleLockHandoffHistory(receipt, null);
|
|
16840
|
+
const transfer = d.transaction(() => {
|
|
16841
|
+
const result = d.run(`UPDATE tasks
|
|
16842
|
+
SET locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
|
|
16843
|
+
WHERE id = ?
|
|
16844
|
+
AND locked_by = ?
|
|
16845
|
+
AND locked_at = ?
|
|
16846
|
+
AND julianday(locked_at) < julianday(?)
|
|
16847
|
+
AND status NOT IN ('completed', 'failed', 'cancelled')`, [
|
|
16848
|
+
prepared.new_holder,
|
|
16849
|
+
prepared.operation_timestamp,
|
|
16850
|
+
prepared.operation_timestamp,
|
|
16851
|
+
prepared.task_id,
|
|
16852
|
+
prepared.expected_holder,
|
|
16853
|
+
prepared.expected_lock_version,
|
|
16854
|
+
prepared.stale_cutoff
|
|
16855
|
+
]);
|
|
16856
|
+
if (result.changes === 0) {
|
|
16857
|
+
const current = getTask(prepared.task_id, d);
|
|
16858
|
+
if (!current)
|
|
16859
|
+
throw new TaskNotFoundError(prepared.task_id);
|
|
16860
|
+
throwStaleLockHandoffConflict(current, prepared);
|
|
16861
|
+
}
|
|
16862
|
+
insertTaskHistory(history, d);
|
|
16863
|
+
});
|
|
16864
|
+
transfer();
|
|
16865
|
+
return receipt;
|
|
16866
|
+
}
|
|
16582
16867
|
function getTaskLockStatus(id, db) {
|
|
16583
16868
|
const d = db || getDatabase();
|
|
16584
16869
|
const task = getTask(id, d);
|
|
@@ -16879,6 +17164,7 @@ var init_task_lifecycle = __esm(() => {
|
|
|
16879
17164
|
init_task_crud();
|
|
16880
17165
|
init_task_graph();
|
|
16881
17166
|
init_prewrite_secrets();
|
|
17167
|
+
init_stale_lock_handoff();
|
|
16882
17168
|
});
|
|
16883
17169
|
|
|
16884
17170
|
// src/db/task-crud.ts
|
|
@@ -20488,6 +20774,7 @@ __export(exports_tasks, {
|
|
|
20488
20774
|
insertTaskTags: () => insertTaskTags,
|
|
20489
20775
|
importTaskBoardBundle: () => importTaskBoardBundle,
|
|
20490
20776
|
importCalendarIcs: () => importCalendarIcs,
|
|
20777
|
+
handoffStaleTaskLock: () => handoffStaleTaskLock,
|
|
20491
20778
|
getTimeReport: () => getTimeReport,
|
|
20492
20779
|
getTimeLogs: () => getTimeLogs,
|
|
20493
20780
|
getTasksChangedSince: () => getTasksChangedSince,
|
|
@@ -22130,7 +22417,7 @@ function registerTaskCommands(program2) {
|
|
|
22130
22417
|
}
|
|
22131
22418
|
});
|
|
22132
22419
|
const task = program2.command("task").description("Task subcommands for deterministic automation");
|
|
22133
|
-
task.command("upsert").description("Create or update a task by stable metadata fingerprint").requiredOption("--fingerprint <key>", "Stable dedupe fingerprint").requiredOption("--title <text>", "Task title").option("-d, --description <text>", "Task description").option("-p, --priority <level>", "Priority: low, medium, high, critical").option("-s, --status <status>", "Task status").option("--list <id>", "Task list ID").option("--task-list <id>", "Task list ID (alias for --list)").option("-t, --tags <tags>", "Comma-separated tags").option("--tag <tags>", "Comma-separated tags (alias for --tags)").option("--metadata-json <json>", "JSON object merged into task metadata").option("--working-dir <path>", "Working directory to store on create/update").option("--project <id>", "Assign to project by ID, slug, or path").option("--assign <agent>", "Assign to agent").option("--assign-seat", "Allow --assign to name a durable seat (a seat queue has no session watching it)").option("--expectation-id <id>", "Expectation metadata ID").option("--expectation-fingerprint <key>", "Expectation metadata fingerprint").option("--evidence-paths <paths>", "Comma-separated evidence paths").option("--origin-loop-id <id>", "Origin loop ID").option("--origin-run-id <id>", "Origin run ID").option("--expected <json-or-text>", "Expected value metadata").option("--observed <json-or-text>", "Observed value metadata").option("--acceptance <json-or-text>", "Acceptance metadata").action(async (opts) => {
|
|
22420
|
+
task.command("upsert").description("Create or update a task by stable metadata fingerprint").requiredOption("--fingerprint <key>", "Stable dedupe fingerprint").requiredOption("--title <text>", "Task title").option("-d, --description <text>", "Task description").option("-p, --priority <level>", "Priority: low, medium, high, critical").option("-s, --status <status>", "Task status").option("--list <id>", "Task list ID").option("--task-list <id>", "Task list ID (alias for --list)").option("--plan <id>", "Assign to a plan").option("-t, --tags <tags>", "Comma-separated tags").option("--tag <tags>", "Comma-separated tags (alias for --tags)").option("--metadata-json <json>", "JSON object merged into task metadata").option("--working-dir <path>", "Working directory to store on create/update").option("--project <id>", "Assign to project by ID, slug, or path").option("--assign <agent>", "Assign to agent").option("--assign-seat", "Allow --assign to name a durable seat (a seat queue has no session watching it)").option("--expectation-id <id>", "Expectation metadata ID").option("--expectation-fingerprint <key>", "Expectation metadata fingerprint").option("--evidence-paths <paths>", "Comma-separated evidence paths").option("--origin-loop-id <id>", "Origin loop ID").option("--origin-run-id <id>", "Origin run ID").option("--expected <json-or-text>", "Expected value metadata").option("--observed <json-or-text>", "Observed value metadata").option("--acceptance <json-or-text>", "Acceptance metadata").action(async (opts) => {
|
|
22134
22421
|
const globalOpts = program2.opts();
|
|
22135
22422
|
opts.tags = opts.tags || opts.tag;
|
|
22136
22423
|
opts.list = opts.list || opts.taskList;
|
|
@@ -22144,6 +22431,10 @@ function registerTaskCommands(program2) {
|
|
|
22144
22431
|
try {
|
|
22145
22432
|
const projectId2 = explicitProject ? await cloudResolveProjectRef(cloud, explicitProject) : undefined;
|
|
22146
22433
|
const taskListId2 = opts.list ? await cloudResolveTaskListRef(cloud, opts.list, projectId2) : undefined;
|
|
22434
|
+
const plan = opts.plan ? await cloudResolvePlan(cloud, opts.plan, projectId2) : null;
|
|
22435
|
+
if (opts.plan && !plan) {
|
|
22436
|
+
throw new Error(`Could not resolve plan ID or slug: ${opts.plan}`);
|
|
22437
|
+
}
|
|
22147
22438
|
cloudResult = await cloudUpsertTaskByFingerprint(cloud, {
|
|
22148
22439
|
fingerprint: opts.fingerprint,
|
|
22149
22440
|
title: opts.title,
|
|
@@ -22155,7 +22446,8 @@ function registerTaskCommands(program2) {
|
|
|
22155
22446
|
metadata: buildExpectationMetadata(opts),
|
|
22156
22447
|
working_dir: opts.workingDir ? resolve9(opts.workingDir) : process.cwd(),
|
|
22157
22448
|
project_id: projectId2,
|
|
22158
|
-
assigned_to: opts.assign
|
|
22449
|
+
assigned_to: opts.assign,
|
|
22450
|
+
plan_id: plan?.id
|
|
22159
22451
|
});
|
|
22160
22452
|
} catch (e) {
|
|
22161
22453
|
handleError(e);
|
|
@@ -22177,6 +22469,7 @@ function registerTaskCommands(program2) {
|
|
|
22177
22469
|
}
|
|
22178
22470
|
return id;
|
|
22179
22471
|
})() : undefined;
|
|
22472
|
+
const planId = opts.plan ? resolvePlanId(opts.plan) : undefined;
|
|
22180
22473
|
let result;
|
|
22181
22474
|
try {
|
|
22182
22475
|
result = upsertTaskByFingerprint({
|
|
@@ -22191,6 +22484,7 @@ function registerTaskCommands(program2) {
|
|
|
22191
22484
|
working_dir: opts.workingDir ? resolve9(opts.workingDir) : process.cwd(),
|
|
22192
22485
|
project_id: projectId,
|
|
22193
22486
|
assigned_to: opts.assign,
|
|
22487
|
+
plan_id: planId,
|
|
22194
22488
|
agent_id: globalOpts.agent,
|
|
22195
22489
|
session_id: globalOpts.session
|
|
22196
22490
|
});
|
|
@@ -23184,6 +23478,44 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
|
|
|
23184
23478
|
console.log(chalk3.green("Lock released."));
|
|
23185
23479
|
}
|
|
23186
23480
|
});
|
|
23481
|
+
program2.command("stale-lock-handoff <id>").description("Atomically transfer one exact stale lock by holder and locked_at version").requiredOption("--expected-holder <agent>", "Exact current locked_by value").requiredOption("--expected-lock-version <timestamp>", "Exact current locked_at value (canonical UTC)").requiredOption("--stale-after-seconds <seconds>", "Required lock age threshold; no default").requiredOption("--new-holder <agent>", "New holder; must match the authenticated/--agent identity").requiredOption("--reason <text>", "Non-empty audit reason").action(async (id, opts) => {
|
|
23482
|
+
const globalOpts = program2.opts();
|
|
23483
|
+
const taskId = normalizeExactTaskId(id);
|
|
23484
|
+
const actor = resolveClaimIdentity("handoff a stale lock on", globalOpts.agent);
|
|
23485
|
+
const staleAfterSeconds = Number(opts.staleAfterSeconds);
|
|
23486
|
+
const cloud = getTodosCloudClient();
|
|
23487
|
+
let receipt;
|
|
23488
|
+
try {
|
|
23489
|
+
receipt = cloud ? await cloudHandoffStaleTaskLock(cloud, {
|
|
23490
|
+
task_id: taskId,
|
|
23491
|
+
expected_holder: opts.expectedHolder,
|
|
23492
|
+
expected_lock_version: opts.expectedLockVersion,
|
|
23493
|
+
stale_after_seconds: staleAfterSeconds,
|
|
23494
|
+
new_holder: opts.newHolder,
|
|
23495
|
+
reason: opts.reason
|
|
23496
|
+
}) : handoffStaleTaskLock({
|
|
23497
|
+
task_id: taskId,
|
|
23498
|
+
actor,
|
|
23499
|
+
expected_holder: opts.expectedHolder,
|
|
23500
|
+
expected_lock_version: opts.expectedLockVersion,
|
|
23501
|
+
stale_after_seconds: staleAfterSeconds,
|
|
23502
|
+
new_holder: opts.newHolder,
|
|
23503
|
+
reason: opts.reason
|
|
23504
|
+
});
|
|
23505
|
+
} catch (e) {
|
|
23506
|
+
handleError(e);
|
|
23507
|
+
}
|
|
23508
|
+
if (globalOpts.json) {
|
|
23509
|
+
output({ receipt }, true);
|
|
23510
|
+
return;
|
|
23511
|
+
}
|
|
23512
|
+
console.log(chalk3.green(`Stale lock transferred on task ${escapeTerminalControls(receipt.task_id)}.`));
|
|
23513
|
+
console.log(` ${escapeTerminalControls(receipt.previous_holder)} @ ${escapeTerminalControls(receipt.previous_lock_version)}`);
|
|
23514
|
+
console.log(` -> ${escapeTerminalControls(receipt.new_holder)} @ ${escapeTerminalControls(receipt.new_lock_version)}`);
|
|
23515
|
+
console.log(` stale after ${receipt.stale_after_seconds}s (cutoff ${escapeTerminalControls(receipt.stale_cutoff)})`);
|
|
23516
|
+
console.log(` receipt ${escapeTerminalControls(receipt.receipt_id)}`);
|
|
23517
|
+
console.log(` reason ${escapeTerminalControls(receipt.reason)}`);
|
|
23518
|
+
});
|
|
23187
23519
|
program2.command("delete <id>").description("Delete a task").action(async (id) => {
|
|
23188
23520
|
const globalOpts = program2.opts();
|
|
23189
23521
|
const cloud = getTodosCloudClient();
|
|
@@ -23368,6 +23700,7 @@ var init_task_commands = __esm(() => {
|
|
|
23368
23700
|
init_agents();
|
|
23369
23701
|
init_helpers();
|
|
23370
23702
|
init_output_redaction();
|
|
23703
|
+
init_stale_lock_handoff();
|
|
23371
23704
|
});
|
|
23372
23705
|
|
|
23373
23706
|
// src/lib/plan-artifacts.ts
|
|
@@ -24355,6 +24688,58 @@ var init_plan_project_links = __esm(() => {
|
|
|
24355
24688
|
init_tasks();
|
|
24356
24689
|
});
|
|
24357
24690
|
|
|
24691
|
+
// src/storage/audit-history-import.ts
|
|
24692
|
+
function auditHistoryRowsAreFieldIdentical(left, right) {
|
|
24693
|
+
return AUDIT_HISTORY_FIELDS.every((field) => {
|
|
24694
|
+
const leftValue = field === "machine_id" ? left[field] ?? null : left[field];
|
|
24695
|
+
const rightValue = field === "machine_id" ? right[field] ?? null : right[field];
|
|
24696
|
+
return leftValue === rightValue;
|
|
24697
|
+
});
|
|
24698
|
+
}
|
|
24699
|
+
function divergentAuditHistoryReplayError(id) {
|
|
24700
|
+
return `${AUDIT_HISTORY_DIVERGENT_REPLAY}: immutable audit_history row ${id} differs from stored row`;
|
|
24701
|
+
}
|
|
24702
|
+
function forbiddenAuditHistoryTombstoneError(id) {
|
|
24703
|
+
return `${AUDIT_HISTORY_TOMBSTONE_FORBIDDEN}: audit_history tombstone ${id} is not allowed`;
|
|
24704
|
+
}
|
|
24705
|
+
function parseAuditHistoryImportFailure(message) {
|
|
24706
|
+
const divergentPrefix = `${AUDIT_HISTORY_DIVERGENT_REPLAY}: immutable audit_history row `;
|
|
24707
|
+
const divergentSuffix = " differs from stored row";
|
|
24708
|
+
if (message.startsWith(divergentPrefix) && message.endsWith(divergentSuffix)) {
|
|
24709
|
+
return {
|
|
24710
|
+
code: AUDIT_HISTORY_DIVERGENT_REPLAY,
|
|
24711
|
+
auditHistoryId: message.slice(divergentPrefix.length, -divergentSuffix.length),
|
|
24712
|
+
conflict: true,
|
|
24713
|
+
status: 409
|
|
24714
|
+
};
|
|
24715
|
+
}
|
|
24716
|
+
const tombstonePrefix = `${AUDIT_HISTORY_TOMBSTONE_FORBIDDEN}: audit_history tombstone `;
|
|
24717
|
+
const tombstoneSuffix = " is not allowed";
|
|
24718
|
+
if (message.startsWith(tombstonePrefix) && message.endsWith(tombstoneSuffix)) {
|
|
24719
|
+
return {
|
|
24720
|
+
code: AUDIT_HISTORY_TOMBSTONE_FORBIDDEN,
|
|
24721
|
+
auditHistoryId: message.slice(tombstonePrefix.length, -tombstoneSuffix.length),
|
|
24722
|
+
conflict: false,
|
|
24723
|
+
status: 400
|
|
24724
|
+
};
|
|
24725
|
+
}
|
|
24726
|
+
return null;
|
|
24727
|
+
}
|
|
24728
|
+
var AUDIT_HISTORY_DIVERGENT_REPLAY = "AUDIT_HISTORY_DIVERGENT_REPLAY", AUDIT_HISTORY_TOMBSTONE_FORBIDDEN = "AUDIT_HISTORY_TOMBSTONE_FORBIDDEN", AUDIT_HISTORY_FIELDS;
|
|
24729
|
+
var init_audit_history_import = __esm(() => {
|
|
24730
|
+
AUDIT_HISTORY_FIELDS = [
|
|
24731
|
+
"id",
|
|
24732
|
+
"task_id",
|
|
24733
|
+
"action",
|
|
24734
|
+
"field",
|
|
24735
|
+
"old_value",
|
|
24736
|
+
"new_value",
|
|
24737
|
+
"agent_id",
|
|
24738
|
+
"created_at",
|
|
24739
|
+
"machine_id"
|
|
24740
|
+
];
|
|
24741
|
+
});
|
|
24742
|
+
|
|
24358
24743
|
// src/storage/sqlite-snapshot.ts
|
|
24359
24744
|
function exportSqliteTodosStorageSnapshot(db) {
|
|
24360
24745
|
const d = db ?? getDatabase();
|
|
@@ -24388,8 +24773,11 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
24388
24773
|
const existingTaskLists = d.query("SELECT id, project_id, slug FROM task_lists").all();
|
|
24389
24774
|
result.errors.push(...validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists));
|
|
24390
24775
|
}
|
|
24776
|
+
const auditImport = preflightAuditHistoryImport(d, snapshot.auditHistory, snapshot.tombstones ?? []);
|
|
24777
|
+
result.errors.push(...auditImport.errors);
|
|
24391
24778
|
if (result.errors.length > 0)
|
|
24392
24779
|
return result;
|
|
24780
|
+
result.skipped += auditImport.identicalReplayCount;
|
|
24393
24781
|
const applyRows = (objectType, table, columns, rows, updateClockColumn, acceptEqualClock = true, afterUpsert) => {
|
|
24394
24782
|
for (const row of rows) {
|
|
24395
24783
|
try {
|
|
@@ -24424,10 +24812,72 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
24424
24812
|
replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
|
|
24425
24813
|
}
|
|
24426
24814
|
});
|
|
24427
|
-
|
|
24815
|
+
insertAuditHistoryRows(d, auditImport.rowsToInsert, result);
|
|
24428
24816
|
applyTombstones(d, snapshot.tombstones ?? [], result);
|
|
24429
24817
|
return result;
|
|
24430
24818
|
}
|
|
24819
|
+
function preflightAuditHistoryImport(db, rows, tombstones) {
|
|
24820
|
+
const errors = tombstones.filter((tombstone) => tombstone.object_type === "audit_history").map((tombstone) => forbiddenAuditHistoryTombstoneError(tombstone.object_id));
|
|
24821
|
+
const rowsToInsert = [];
|
|
24822
|
+
const seen = new Map;
|
|
24823
|
+
let identicalReplayCount = 0;
|
|
24824
|
+
for (const rawRow of rows) {
|
|
24825
|
+
try {
|
|
24826
|
+
const row = asRecord(rawRow);
|
|
24827
|
+
if (typeof row.id !== "string" || !row.id) {
|
|
24828
|
+
throw new Error("task_history row is missing id");
|
|
24829
|
+
}
|
|
24830
|
+
const prior = seen.get(row.id);
|
|
24831
|
+
if (prior) {
|
|
24832
|
+
if (auditHistoryRowsAreFieldIdentical(prior, row))
|
|
24833
|
+
identicalReplayCount += 1;
|
|
24834
|
+
else
|
|
24835
|
+
errors.push(divergentAuditHistoryReplayError(row.id));
|
|
24836
|
+
continue;
|
|
24837
|
+
}
|
|
24838
|
+
seen.set(row.id, row);
|
|
24839
|
+
const existing = getAuditHistoryById(db, row.id);
|
|
24840
|
+
if (!existing) {
|
|
24841
|
+
rowsToInsert.push(row);
|
|
24842
|
+
} else if (auditHistoryRowsAreFieldIdentical(existing, row)) {
|
|
24843
|
+
identicalReplayCount += 1;
|
|
24844
|
+
} else {
|
|
24845
|
+
errors.push(divergentAuditHistoryReplayError(row.id));
|
|
24846
|
+
}
|
|
24847
|
+
} catch (error) {
|
|
24848
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
24849
|
+
}
|
|
24850
|
+
}
|
|
24851
|
+
return { rowsToInsert, identicalReplayCount, errors };
|
|
24852
|
+
}
|
|
24853
|
+
function insertAuditHistoryRows(db, rows, result) {
|
|
24854
|
+
for (const rawRow of rows) {
|
|
24855
|
+
try {
|
|
24856
|
+
const row = asRecord(rawRow);
|
|
24857
|
+
const presentColumns = AUDIT_COLUMNS.filter((column) => (column in row));
|
|
24858
|
+
if (!presentColumns.includes("id"))
|
|
24859
|
+
presentColumns.unshift("id");
|
|
24860
|
+
const placeholders = presentColumns.map(() => "?").join(", ");
|
|
24861
|
+
const values = presentColumns.map((column) => valueForColumn(column, row[column]));
|
|
24862
|
+
const changes = db.run(`INSERT OR IGNORE INTO task_history (${presentColumns.join(", ")}) VALUES (${placeholders})`, values).changes;
|
|
24863
|
+
if (changes > 0) {
|
|
24864
|
+
result.inserted += 1;
|
|
24865
|
+
continue;
|
|
24866
|
+
}
|
|
24867
|
+
const existing = getAuditHistoryById(db, String(row["id"]));
|
|
24868
|
+
if (existing && auditHistoryRowsAreFieldIdentical(existing, row)) {
|
|
24869
|
+
result.skipped += 1;
|
|
24870
|
+
} else {
|
|
24871
|
+
result.errors.push(divergentAuditHistoryReplayError(String(row["id"])));
|
|
24872
|
+
}
|
|
24873
|
+
} catch (error) {
|
|
24874
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
24875
|
+
}
|
|
24876
|
+
}
|
|
24877
|
+
}
|
|
24878
|
+
function getAuditHistoryById(db, id) {
|
|
24879
|
+
return db.query(`SELECT ${AUDIT_COLUMNS.join(", ")} FROM task_history WHERE id = ? LIMIT 1`).get(id);
|
|
24880
|
+
}
|
|
24431
24881
|
function upsertById(db, table, columns, row, updateClockColumn, acceptEqualClock = true) {
|
|
24432
24882
|
const id = row["id"];
|
|
24433
24883
|
if (typeof id !== "string" || !id)
|
|
@@ -24525,7 +24975,7 @@ function tableForTombstone(objectType) {
|
|
|
24525
24975
|
return "task_templates";
|
|
24526
24976
|
if (objectType === "template_tasks")
|
|
24527
24977
|
return "template_tasks";
|
|
24528
|
-
|
|
24978
|
+
throw new Error(`unsupported storage tombstone object_type: ${String(objectType)}`);
|
|
24529
24979
|
}
|
|
24530
24980
|
function listRows(db, table, columns) {
|
|
24531
24981
|
return db.query(`SELECT ${columns.join(", ")} FROM ${table} ORDER BY id`).all();
|
|
@@ -24572,6 +25022,7 @@ var init_sqlite_snapshot = __esm(() => {
|
|
|
24572
25022
|
init_tasks();
|
|
24573
25023
|
init_templates();
|
|
24574
25024
|
init_storage_tombstones();
|
|
25025
|
+
init_audit_history_import();
|
|
24575
25026
|
PROJECT_COLUMNS = [
|
|
24576
25027
|
"id",
|
|
24577
25028
|
"name",
|
|
@@ -24836,6 +25287,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
24836
25287
|
unlockTask(id, agentId, database());
|
|
24837
25288
|
return true;
|
|
24838
25289
|
},
|
|
25290
|
+
handoffStaleLock: (input) => handoffStaleTaskLock(input, database()),
|
|
24839
25291
|
delete: (id) => deleteTask(id, database()),
|
|
24840
25292
|
start: (id, agentId) => startTask(id, agentId, database()),
|
|
24841
25293
|
complete: (id, agentId, options2) => completeTask(id, agentId, database(), options2),
|
|
@@ -25592,7 +26044,7 @@ function registerPlanTemplateCommands(program2) {
|
|
|
25592
26044
|
if (!plan2) {
|
|
25593
26045
|
handleError(new Error(`Plan not found: ${opts.show}`));
|
|
25594
26046
|
}
|
|
25595
|
-
const tasks2 = await
|
|
26047
|
+
const tasks2 = await cloudListPlanTasks(cloud, plan2.id);
|
|
25596
26048
|
if (globalOpts.json) {
|
|
25597
26049
|
output({ plan: plan2, tasks: tasks2, artifact: null }, true);
|
|
25598
26050
|
return;
|
|
@@ -34444,7 +34896,7 @@ var init_postgres_sync = __esm(() => {
|
|
|
34444
34896
|
});
|
|
34445
34897
|
|
|
34446
34898
|
// src/storage/postgres-adapter.ts
|
|
34447
|
-
import { randomUUID as
|
|
34899
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
34448
34900
|
function createPostgresTodosStorageAdapter(options) {
|
|
34449
34901
|
const store = new PostgresJsonRecordStore(options);
|
|
34450
34902
|
const adapter = {
|
|
@@ -34473,6 +34925,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
34473
34925
|
getChangedSince: (since, filters) => getChangedSince(since, filters, store),
|
|
34474
34926
|
lock: (id, agentId) => lockTask2(id, agentId, store),
|
|
34475
34927
|
unlock: (id, agentId) => unlockTask2(id, agentId, store),
|
|
34928
|
+
handoffStaleLock: (input, context) => store.handoffStaleLock(input, context),
|
|
34476
34929
|
getByFingerprint: (fingerprint) => store.getTaskByFingerprint(fingerprint)
|
|
34477
34930
|
},
|
|
34478
34931
|
dependencies: {
|
|
@@ -34619,6 +35072,91 @@ class PostgresJsonRecordStore {
|
|
|
34619
35072
|
LIMIT 1`, [this.service, type, id]);
|
|
34620
35073
|
return result.rows[0] ? payloadRecord2(result.rows[0].payload) : null;
|
|
34621
35074
|
}
|
|
35075
|
+
async handoffStaleLock(input, context = {}) {
|
|
35076
|
+
const prepared = prepareStaleLockHandoff(input);
|
|
35077
|
+
const receipt = buildStaleLockHandoffReceipt(prepared);
|
|
35078
|
+
const history = staleLockHandoffHistory(receipt, this.machineId(context));
|
|
35079
|
+
await this.ensureSchema();
|
|
35080
|
+
const result = await this.options.client.query(`/* todos:stale-lock-handoff-atomic */ WITH
|
|
35081
|
+
target AS MATERIALIZED (
|
|
35082
|
+
SELECT payload
|
|
35083
|
+
FROM ${this.tableName}
|
|
35084
|
+
WHERE service = $1
|
|
35085
|
+
AND object_type = 'tasks'
|
|
35086
|
+
AND object_id = $2
|
|
35087
|
+
AND deleted_at IS NULL
|
|
35088
|
+
FOR UPDATE
|
|
35089
|
+
),
|
|
35090
|
+
updated AS (
|
|
35091
|
+
UPDATE ${this.tableName} AS task_record
|
|
35092
|
+
SET payload = jsonb_set(
|
|
35093
|
+
jsonb_set(
|
|
35094
|
+
jsonb_set(
|
|
35095
|
+
jsonb_set(
|
|
35096
|
+
task_record.payload,
|
|
35097
|
+
'{locked_by}',
|
|
35098
|
+
to_jsonb($6::text),
|
|
35099
|
+
true
|
|
35100
|
+
),
|
|
35101
|
+
'{locked_at}',
|
|
35102
|
+
to_jsonb($7::text),
|
|
35103
|
+
true
|
|
35104
|
+
),
|
|
35105
|
+
'{updated_at}',
|
|
35106
|
+
to_jsonb($7::text),
|
|
35107
|
+
true
|
|
35108
|
+
),
|
|
35109
|
+
'{version}',
|
|
35110
|
+
to_jsonb(COALESCE((task_record.payload->>'version')::integer, 0) + 1),
|
|
35111
|
+
true
|
|
35112
|
+
),
|
|
35113
|
+
updated_at = $7::timestamptz,
|
|
35114
|
+
source_machine_id = $10,
|
|
35115
|
+
version = COALESCE(task_record.version, 0) + 1
|
|
35116
|
+
FROM target
|
|
35117
|
+
WHERE task_record.service = $1
|
|
35118
|
+
AND task_record.object_type = 'tasks'
|
|
35119
|
+
AND task_record.object_id = $2
|
|
35120
|
+
AND task_record.deleted_at IS NULL
|
|
35121
|
+
AND target.payload->>'locked_by' = $3
|
|
35122
|
+
AND target.payload->>'locked_at' = $4
|
|
35123
|
+
AND todos_try_timestamptz(target.payload->>'locked_at') < $5::timestamptz
|
|
35124
|
+
AND COALESCE(target.payload->>'status', '') NOT IN ('completed', 'failed', 'cancelled')
|
|
35125
|
+
RETURNING task_record.payload
|
|
35126
|
+
),
|
|
35127
|
+
audit AS (
|
|
35128
|
+
INSERT INTO ${this.tableName} (
|
|
35129
|
+
service, object_type, object_id, payload, updated_at,
|
|
35130
|
+
deleted_at, source_machine_id, version
|
|
35131
|
+
)
|
|
35132
|
+
SELECT $1, 'audit_history', $8, $9::jsonb, $7::timestamptz,
|
|
35133
|
+
NULL, $10, NULL
|
|
35134
|
+
FROM updated
|
|
35135
|
+
RETURNING payload
|
|
35136
|
+
)
|
|
35137
|
+
SELECT
|
|
35138
|
+
(SELECT payload FROM target) AS current_payload,
|
|
35139
|
+
(SELECT payload FROM updated) AS updated_payload,
|
|
35140
|
+
(SELECT payload FROM audit) AS audit_payload`, [
|
|
35141
|
+
this.service,
|
|
35142
|
+
prepared.task_id,
|
|
35143
|
+
prepared.expected_holder,
|
|
35144
|
+
prepared.expected_lock_version,
|
|
35145
|
+
prepared.stale_cutoff,
|
|
35146
|
+
prepared.new_holder,
|
|
35147
|
+
prepared.operation_timestamp,
|
|
35148
|
+
receipt.receipt_id,
|
|
35149
|
+
jsonbParam(history),
|
|
35150
|
+
this.machineId(context)
|
|
35151
|
+
]);
|
|
35152
|
+
const row = result.rows[0];
|
|
35153
|
+
if (!row?.current_payload)
|
|
35154
|
+
throw new TaskNotFoundError(prepared.task_id);
|
|
35155
|
+
if (!row.updated_payload || !row.audit_payload) {
|
|
35156
|
+
throwStaleLockHandoffConflict(payloadRecord2(row.current_payload), prepared);
|
|
35157
|
+
}
|
|
35158
|
+
return receipt;
|
|
35159
|
+
}
|
|
34622
35160
|
async list(type) {
|
|
34623
35161
|
return (await this.listRecords(type)).map((record) => record.payload);
|
|
34624
35162
|
}
|
|
@@ -34888,6 +35426,28 @@ class PostgresJsonRecordStore {
|
|
|
34888
35426
|
}
|
|
34889
35427
|
return value;
|
|
34890
35428
|
}
|
|
35429
|
+
async insertImmutableAuditHistory(value, context = {}) {
|
|
35430
|
+
await this.ensureSchema();
|
|
35431
|
+
const inserted = await this.options.client.query(`INSERT INTO ${this.tableName} (
|
|
35432
|
+
service, object_type, object_id, payload, updated_at,
|
|
35433
|
+
deleted_at, source_machine_id, version
|
|
35434
|
+
) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, NULL, $6, NULL)
|
|
35435
|
+
ON CONFLICT (service, object_type, object_id) DO NOTHING
|
|
35436
|
+
RETURNING object_id`, [
|
|
35437
|
+
this.service,
|
|
35438
|
+
"audit_history",
|
|
35439
|
+
value.id,
|
|
35440
|
+
jsonbParam(value),
|
|
35441
|
+
value.created_at,
|
|
35442
|
+
context.requestId ?? this.sourceMachineId ?? null
|
|
35443
|
+
]);
|
|
35444
|
+
if (inserted.rows.length > 0)
|
|
35445
|
+
return "inserted";
|
|
35446
|
+
const existing = await this.get("audit_history", value.id);
|
|
35447
|
+
if (existing && auditHistoryRowsAreFieldIdentical(existing, value))
|
|
35448
|
+
return "identical";
|
|
35449
|
+
throw new Error(divergentAuditHistoryReplayError(value.id));
|
|
35450
|
+
}
|
|
34891
35451
|
async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}) {
|
|
34892
35452
|
const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
|
|
34893
35453
|
if (planIds.length === 0)
|
|
@@ -35714,7 +36274,7 @@ async function createTask2(input, store, context) {
|
|
|
35714
36274
|
const effectiveProjectId = linkedPlan?.project_id ?? requestedProjectId;
|
|
35715
36275
|
const shortId = effectiveProjectId ? await nextTaskShortId2(effectiveProjectId, store, context) : null;
|
|
35716
36276
|
const task = {
|
|
35717
|
-
id:
|
|
36277
|
+
id: randomUUID4(),
|
|
35718
36278
|
short_id: shortId,
|
|
35719
36279
|
project_id: effectiveProjectId,
|
|
35720
36280
|
parent_id: input.parent_id ?? null,
|
|
@@ -35960,7 +36520,7 @@ async function addVerification(input, store, context) {
|
|
|
35960
36520
|
throw new Error(`Task not found: ${input.task_id}`);
|
|
35961
36521
|
const timestamp2 = new Date().toISOString();
|
|
35962
36522
|
const verification = {
|
|
35963
|
-
id:
|
|
36523
|
+
id: randomUUID4(),
|
|
35964
36524
|
task_id: input.task_id,
|
|
35965
36525
|
command: input.command,
|
|
35966
36526
|
status: input.status ?? "unknown",
|
|
@@ -35981,7 +36541,7 @@ async function addCommit(input, store, context) {
|
|
|
35981
36541
|
throw new Error(`Task not found: ${input.task_id}`);
|
|
35982
36542
|
const timestamp2 = new Date().toISOString();
|
|
35983
36543
|
const commit = {
|
|
35984
|
-
id:
|
|
36544
|
+
id: randomUUID4(),
|
|
35985
36545
|
task_id: input.task_id,
|
|
35986
36546
|
sha: input.sha,
|
|
35987
36547
|
message: input.message ?? null,
|
|
@@ -36004,7 +36564,7 @@ async function addGitRef(input, store, context) {
|
|
|
36004
36564
|
throw new Error(`Task not found: ${input.task_id}`);
|
|
36005
36565
|
const timestamp2 = new Date().toISOString();
|
|
36006
36566
|
const gitRef = {
|
|
36007
|
-
id:
|
|
36567
|
+
id: randomUUID4(),
|
|
36008
36568
|
task_id: input.task_id,
|
|
36009
36569
|
ref_type: input.ref_type,
|
|
36010
36570
|
name: input.name,
|
|
@@ -36074,7 +36634,7 @@ async function createProject2(input, store, context) {
|
|
|
36074
36634
|
if (!derivedSlug || !taskListId)
|
|
36075
36635
|
throw new Error("Project name and task-list slug must be non-empty");
|
|
36076
36636
|
const project = {
|
|
36077
|
-
id:
|
|
36637
|
+
id: randomUUID4(),
|
|
36078
36638
|
name: input.name,
|
|
36079
36639
|
path: input.path,
|
|
36080
36640
|
description: input.description ?? null,
|
|
@@ -36106,7 +36666,7 @@ async function createPlan2(input, store, context) {
|
|
|
36106
36666
|
store
|
|
36107
36667
|
});
|
|
36108
36668
|
return store.upsert("plans", {
|
|
36109
|
-
id:
|
|
36669
|
+
id: randomUUID4(),
|
|
36110
36670
|
slug,
|
|
36111
36671
|
project_id: projectId,
|
|
36112
36672
|
task_list_id: input.task_list_id ?? context?.taskListId ?? null,
|
|
@@ -36158,7 +36718,7 @@ async function registerAgent2(input, store, context) {
|
|
|
36158
36718
|
}
|
|
36159
36719
|
const timestamp2 = new Date().toISOString();
|
|
36160
36720
|
const agent = {
|
|
36161
|
-
id: existing?.id ??
|
|
36721
|
+
id: existing?.id ?? randomUUID4().slice(0, 8),
|
|
36162
36722
|
name: canonicalName,
|
|
36163
36723
|
description: input.description ?? existing?.description ?? null,
|
|
36164
36724
|
role: input.role ?? existing?.role ?? null,
|
|
@@ -36231,7 +36791,7 @@ async function createTaskList2(input, store, context) {
|
|
|
36231
36791
|
if (!slug)
|
|
36232
36792
|
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
36233
36793
|
return store.upsert("task_lists", {
|
|
36234
|
-
id:
|
|
36794
|
+
id: randomUUID4(),
|
|
36235
36795
|
project_id: input.project_id ?? context?.projectId ?? null,
|
|
36236
36796
|
slug,
|
|
36237
36797
|
name: input.name,
|
|
@@ -36266,7 +36826,7 @@ async function updateTaskList2(id, input, store) {
|
|
|
36266
36826
|
async function createTemplate2(input, store, context) {
|
|
36267
36827
|
const timestamp2 = new Date().toISOString();
|
|
36268
36828
|
const template = {
|
|
36269
|
-
id:
|
|
36829
|
+
id: randomUUID4(),
|
|
36270
36830
|
name: input.name,
|
|
36271
36831
|
title_pattern: input.title_pattern,
|
|
36272
36832
|
description: input.description ?? null,
|
|
@@ -36287,7 +36847,7 @@ async function createTemplate2(input, store, context) {
|
|
|
36287
36847
|
}
|
|
36288
36848
|
function buildTemplateTasks(templateId, inputs, timestamp2) {
|
|
36289
36849
|
return inputs.map((input, position) => ({
|
|
36290
|
-
id:
|
|
36850
|
+
id: randomUUID4(),
|
|
36291
36851
|
template_id: templateId,
|
|
36292
36852
|
position,
|
|
36293
36853
|
title_pattern: input.title_pattern,
|
|
@@ -36320,7 +36880,7 @@ async function updateTemplate2(id, input, store) {
|
|
|
36320
36880
|
}
|
|
36321
36881
|
async function logTaskChange2(taskId, action, field, oldValue, newValue, agentId, store, context) {
|
|
36322
36882
|
const entry2 = {
|
|
36323
|
-
id:
|
|
36883
|
+
id: randomUUID4(),
|
|
36324
36884
|
task_id: taskId,
|
|
36325
36885
|
action,
|
|
36326
36886
|
field: field ?? null,
|
|
@@ -36334,7 +36894,7 @@ async function logTaskChange2(taskId, action, field, oldValue, newValue, agentId
|
|
|
36334
36894
|
}
|
|
36335
36895
|
async function addComment2(input, store, context) {
|
|
36336
36896
|
const comment = {
|
|
36337
|
-
id:
|
|
36897
|
+
id: randomUUID4(),
|
|
36338
36898
|
task_id: input.task_id,
|
|
36339
36899
|
agent_id: input.agent_id ?? context?.agentId ?? null,
|
|
36340
36900
|
session_id: input.session_id ?? context?.sessionId ?? null,
|
|
@@ -36376,6 +36936,11 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
36376
36936
|
result.errors.push(...validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists));
|
|
36377
36937
|
if (result.errors.length > 0)
|
|
36378
36938
|
return result;
|
|
36939
|
+
const auditHistory = await preflightAuditHistoryImport2(snapshot.auditHistory, snapshot.tombstones ?? [], store);
|
|
36940
|
+
result.errors.push(...auditHistory.errors);
|
|
36941
|
+
if (result.errors.length > 0)
|
|
36942
|
+
return result;
|
|
36943
|
+
result.skipped += auditHistory.identical;
|
|
36379
36944
|
const entries = [
|
|
36380
36945
|
...snapshot.tasks.map((row) => ["tasks", row]),
|
|
36381
36946
|
...snapshot.projects.map((row) => ["projects", row]),
|
|
@@ -36384,9 +36949,20 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
36384
36949
|
...snapshot.agents.map((row) => ["agents", row]),
|
|
36385
36950
|
...snapshot.taskLists.map((row) => ["task_lists", row]),
|
|
36386
36951
|
...snapshot.templates.map((row) => ["templates", row]),
|
|
36387
|
-
...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row])
|
|
36388
|
-
...snapshot.auditHistory.map((row) => ["audit_history", row])
|
|
36952
|
+
...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row])
|
|
36389
36953
|
];
|
|
36954
|
+
for (const row of auditHistory.rowsToInsert) {
|
|
36955
|
+
try {
|
|
36956
|
+
const outcome = await store.insertImmutableAuditHistory(row, context);
|
|
36957
|
+
if (outcome === "inserted")
|
|
36958
|
+
result.inserted += 1;
|
|
36959
|
+
else
|
|
36960
|
+
result.skipped += 1;
|
|
36961
|
+
} catch (error) {
|
|
36962
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
36963
|
+
return result;
|
|
36964
|
+
}
|
|
36965
|
+
}
|
|
36390
36966
|
for (const [type, row] of entries) {
|
|
36391
36967
|
try {
|
|
36392
36968
|
const existing = await store.get(type, row.id);
|
|
@@ -36420,6 +36996,32 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
36420
36996
|
}
|
|
36421
36997
|
return result;
|
|
36422
36998
|
}
|
|
36999
|
+
async function preflightAuditHistoryImport2(rows, tombstones, store) {
|
|
37000
|
+
const errors = tombstones.filter((tombstone) => tombstone.object_type === "audit_history").map((tombstone) => forbiddenAuditHistoryTombstoneError(tombstone.object_id));
|
|
37001
|
+
const rowsToInsert = [];
|
|
37002
|
+
const seen = new Map;
|
|
37003
|
+
let identical = 0;
|
|
37004
|
+
for (const row of rows) {
|
|
37005
|
+
const prior = seen.get(row.id);
|
|
37006
|
+
if (prior) {
|
|
37007
|
+
if (auditHistoryRowsAreFieldIdentical(prior, row))
|
|
37008
|
+
identical += 1;
|
|
37009
|
+
else
|
|
37010
|
+
errors.push(divergentAuditHistoryReplayError(row.id));
|
|
37011
|
+
continue;
|
|
37012
|
+
}
|
|
37013
|
+
seen.set(row.id, row);
|
|
37014
|
+
const existing = await store.get("audit_history", row.id);
|
|
37015
|
+
if (!existing) {
|
|
37016
|
+
rowsToInsert.push(row);
|
|
37017
|
+
} else if (auditHistoryRowsAreFieldIdentical(existing, row)) {
|
|
37018
|
+
identical += 1;
|
|
37019
|
+
} else {
|
|
37020
|
+
errors.push(divergentAuditHistoryReplayError(row.id));
|
|
37021
|
+
}
|
|
37022
|
+
}
|
|
37023
|
+
return { rowsToInsert, identical, errors };
|
|
37024
|
+
}
|
|
36423
37025
|
async function requireRecord(type, id, store) {
|
|
36424
37026
|
const record = await store.get(type, id);
|
|
36425
37027
|
if (!record)
|
|
@@ -36526,9 +37128,11 @@ var init_postgres_adapter = __esm(() => {
|
|
|
36526
37128
|
init_types();
|
|
36527
37129
|
init_creator_identity();
|
|
36528
37130
|
init_plan_project_link_contract();
|
|
37131
|
+
init_stale_lock_handoff();
|
|
36529
37132
|
init_postgres_sync();
|
|
36530
37133
|
init_integrity();
|
|
36531
37134
|
init_redaction();
|
|
37135
|
+
init_audit_history_import();
|
|
36532
37136
|
TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
|
|
36533
37137
|
});
|
|
36534
37138
|
|
|
@@ -46532,6 +47136,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
46532
47136
|
ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
|
|
46533
47137
|
ProjectTaskListRollbackResult: projectTaskListRollbackResultSchema,
|
|
46534
47138
|
TaskComment: taskCommentSchema,
|
|
47139
|
+
StaleLockHandoffInput: staleLockHandoffInputSchema,
|
|
47140
|
+
StaleLockHandoffReceipt: staleLockHandoffReceiptSchema,
|
|
46535
47141
|
TaskGitRef: taskGitRefSchema,
|
|
46536
47142
|
Plan: planSchema,
|
|
46537
47143
|
PlanProjectLinkReceipt: planProjectLinkReceiptSchema,
|
|
@@ -47739,6 +48345,51 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
47739
48345
|
}
|
|
47740
48346
|
}
|
|
47741
48347
|
},
|
|
48348
|
+
"/v1/tasks/{id}/stale-lock-handoff": {
|
|
48349
|
+
post: {
|
|
48350
|
+
operationId: "handoffStaleTaskLock",
|
|
48351
|
+
summary: "Atomically transfer one exact stale task lock",
|
|
48352
|
+
description: "Compares one full task UUID, current holder, and exact locked_at version, verifies the lock is strictly older than the supplied threshold, then transfers it directly and writes an immutable task-history receipt in the same backend transaction.",
|
|
48353
|
+
parameters: [
|
|
48354
|
+
{
|
|
48355
|
+
name: "id",
|
|
48356
|
+
in: "path",
|
|
48357
|
+
required: true,
|
|
48358
|
+
schema: { type: "string", format: "uuid" },
|
|
48359
|
+
description: "Exact full task UUID. Short ids and prefixes are rejected."
|
|
48360
|
+
}
|
|
48361
|
+
],
|
|
48362
|
+
requestBody: {
|
|
48363
|
+
required: true,
|
|
48364
|
+
content: {
|
|
48365
|
+
"application/json": {
|
|
48366
|
+
schema: { $ref: "#/components/schemas/StaleLockHandoffInput" }
|
|
48367
|
+
}
|
|
48368
|
+
}
|
|
48369
|
+
},
|
|
48370
|
+
responses: {
|
|
48371
|
+
"200": {
|
|
48372
|
+
content: {
|
|
48373
|
+
"application/json": {
|
|
48374
|
+
schema: {
|
|
48375
|
+
type: "object",
|
|
48376
|
+
additionalProperties: false,
|
|
48377
|
+
required: ["receipt"],
|
|
48378
|
+
properties: {
|
|
48379
|
+
receipt: { $ref: "#/components/schemas/StaleLockHandoffReceipt" }
|
|
48380
|
+
}
|
|
48381
|
+
}
|
|
48382
|
+
}
|
|
48383
|
+
}
|
|
48384
|
+
},
|
|
48385
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
48386
|
+
"403": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
48387
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
48388
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
48389
|
+
"501": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
48390
|
+
}
|
|
48391
|
+
}
|
|
48392
|
+
},
|
|
47742
48393
|
"/v1/tasks/{id}/refs": {
|
|
47743
48394
|
get: {
|
|
47744
48395
|
operationId: "listTaskGitRefs",
|
|
@@ -48335,7 +48986,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
48335
48986
|
}
|
|
48336
48987
|
};
|
|
48337
48988
|
}
|
|
48338
|
-
var taskSchema, projectSchema, taskManifestBindingLookupRequestSchema, taskManifestBindingLookupResultSchema, taskManifestBindingLookupResponseSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
|
|
48989
|
+
var taskSchema, projectSchema, taskManifestBindingLookupRequestSchema, taskManifestBindingLookupResultSchema, taskManifestBindingLookupResponseSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, staleLockHandoffInputSchema, staleLockHandoffReceiptSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
|
|
48339
48990
|
var init_openapi = __esm(() => {
|
|
48340
48991
|
init_package_version();
|
|
48341
48992
|
init_types();
|
|
@@ -48354,6 +49005,8 @@ var init_openapi = __esm(() => {
|
|
|
48354
49005
|
reason: { type: "string", nullable: true },
|
|
48355
49006
|
tags: { type: "array", items: { type: "string" } },
|
|
48356
49007
|
version: { type: "number" },
|
|
49008
|
+
locked_by: { type: "string", nullable: true },
|
|
49009
|
+
locked_at: { type: "string", format: "date-time", nullable: true },
|
|
48357
49010
|
created_at: { type: "string" },
|
|
48358
49011
|
updated_at: { type: "string" }
|
|
48359
49012
|
}
|
|
@@ -48518,6 +49171,69 @@ var init_openapi = __esm(() => {
|
|
|
48518
49171
|
created_at: { type: "string", format: "date-time" }
|
|
48519
49172
|
}
|
|
48520
49173
|
};
|
|
49174
|
+
staleLockHandoffInputSchema = {
|
|
49175
|
+
type: "object",
|
|
49176
|
+
additionalProperties: false,
|
|
49177
|
+
required: [
|
|
49178
|
+
"expected_holder",
|
|
49179
|
+
"expected_lock_version",
|
|
49180
|
+
"stale_after_seconds",
|
|
49181
|
+
"new_holder",
|
|
49182
|
+
"reason"
|
|
49183
|
+
],
|
|
49184
|
+
properties: {
|
|
49185
|
+
expected_holder: { type: "string", minLength: 1 },
|
|
49186
|
+
expected_lock_version: {
|
|
49187
|
+
type: "string",
|
|
49188
|
+
format: "date-time",
|
|
49189
|
+
pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$",
|
|
49190
|
+
description: "Exact authoritative locked_at token read from the task; no default or normalization is applied."
|
|
49191
|
+
},
|
|
49192
|
+
stale_after_seconds: {
|
|
49193
|
+
type: "integer",
|
|
49194
|
+
minimum: 1,
|
|
49195
|
+
description: "Lock age threshold supplied by the caller. The lock must be strictly older at the CAS instant."
|
|
49196
|
+
},
|
|
49197
|
+
new_holder: {
|
|
49198
|
+
type: "string",
|
|
49199
|
+
minLength: 1,
|
|
49200
|
+
description: "Must match the agent bound to the authenticated API key."
|
|
49201
|
+
},
|
|
49202
|
+
reason: { type: "string", minLength: 1, maxLength: 4096 }
|
|
49203
|
+
}
|
|
49204
|
+
};
|
|
49205
|
+
staleLockHandoffReceiptSchema = {
|
|
49206
|
+
type: "object",
|
|
49207
|
+
additionalProperties: false,
|
|
49208
|
+
required: [
|
|
49209
|
+
"schema_version",
|
|
49210
|
+
"receipt_id",
|
|
49211
|
+
"task_id",
|
|
49212
|
+
"actor",
|
|
49213
|
+
"previous_holder",
|
|
49214
|
+
"previous_lock_version",
|
|
49215
|
+
"new_holder",
|
|
49216
|
+
"new_lock_version",
|
|
49217
|
+
"stale_after_seconds",
|
|
49218
|
+
"stale_cutoff",
|
|
49219
|
+
"reason",
|
|
49220
|
+
"created_at"
|
|
49221
|
+
],
|
|
49222
|
+
properties: {
|
|
49223
|
+
schema_version: { type: "string", enum: ["todos.stale-lock-handoff.v1"] },
|
|
49224
|
+
receipt_id: { type: "string", format: "uuid" },
|
|
49225
|
+
task_id: { type: "string", format: "uuid" },
|
|
49226
|
+
actor: { type: "string" },
|
|
49227
|
+
previous_holder: { type: "string" },
|
|
49228
|
+
previous_lock_version: { type: "string", format: "date-time" },
|
|
49229
|
+
new_holder: { type: "string" },
|
|
49230
|
+
new_lock_version: { type: "string", format: "date-time" },
|
|
49231
|
+
stale_after_seconds: { type: "integer", minimum: 1 },
|
|
49232
|
+
stale_cutoff: { type: "string", format: "date-time" },
|
|
49233
|
+
reason: { type: "string" },
|
|
49234
|
+
created_at: { type: "string", format: "date-time" }
|
|
49235
|
+
}
|
|
49236
|
+
};
|
|
48521
49237
|
taskGitRefSchema = {
|
|
48522
49238
|
type: "object",
|
|
48523
49239
|
additionalProperties: false,
|
|
@@ -49412,6 +50128,45 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
49412
50128
|
return error(405, `method ${method} not allowed on /v1/tasks`);
|
|
49413
50129
|
}
|
|
49414
50130
|
if (action) {
|
|
50131
|
+
if (action === "stale-lock-handoff") {
|
|
50132
|
+
if (method !== "POST") {
|
|
50133
|
+
return error(405, "method must be POST on /v1/tasks/:id/stale-lock-handoff");
|
|
50134
|
+
}
|
|
50135
|
+
const exactId = normalizeExactTaskId(id);
|
|
50136
|
+
if (!principal.agent) {
|
|
50137
|
+
return error(403, "stale-lock handoff requires an authenticated agent-bound key", {
|
|
50138
|
+
code: "STALE_LOCK_HANDOFF_ACTOR_MISMATCH"
|
|
50139
|
+
});
|
|
50140
|
+
}
|
|
50141
|
+
if (typeof store.tasks.handoffStaleLock !== "function") {
|
|
50142
|
+
return error(501, "stale-lock handoff is not supported by this storage backend");
|
|
50143
|
+
}
|
|
50144
|
+
const body3 = await readJson3(req) ?? {};
|
|
50145
|
+
const allowed = new Set([
|
|
50146
|
+
"expected_holder",
|
|
50147
|
+
"expected_lock_version",
|
|
50148
|
+
"stale_after_seconds",
|
|
50149
|
+
"new_holder",
|
|
50150
|
+
"reason"
|
|
50151
|
+
]);
|
|
50152
|
+
const unknown = Object.keys(body3).find((key2) => !allowed.has(key2));
|
|
50153
|
+
if (unknown) {
|
|
50154
|
+
return error(400, `unknown stale-lock handoff field: ${unknown}`, {
|
|
50155
|
+
code: "STALE_LOCK_HANDOFF_INVALID_INPUT",
|
|
50156
|
+
field: unknown
|
|
50157
|
+
});
|
|
50158
|
+
}
|
|
50159
|
+
const receipt = await store.tasks.handoffStaleLock({
|
|
50160
|
+
task_id: exactId,
|
|
50161
|
+
actor: principal.agent,
|
|
50162
|
+
expected_holder: body3.expected_holder,
|
|
50163
|
+
expected_lock_version: body3.expected_lock_version,
|
|
50164
|
+
stale_after_seconds: body3.stale_after_seconds,
|
|
50165
|
+
new_holder: body3.new_holder,
|
|
50166
|
+
reason: body3.reason
|
|
50167
|
+
}, contextFromPrincipal(principal));
|
|
50168
|
+
return json5({ receipt });
|
|
50169
|
+
}
|
|
49415
50170
|
if (action === "comments") {
|
|
49416
50171
|
if (method === "GET") {
|
|
49417
50172
|
if (!await store.tasks.get(id))
|
|
@@ -50200,7 +50955,24 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
50200
50955
|
if (received === 0) {
|
|
50201
50956
|
return error(400, "empty snapshot: provide at least one record array (tasks/projects/plans/...)");
|
|
50202
50957
|
}
|
|
50958
|
+
const forbiddenAuditTombstone = (snapshot.tombstones ?? []).find((tombstone) => tombstone.object_type === "audit_history");
|
|
50959
|
+
if (forbiddenAuditTombstone) {
|
|
50960
|
+
return error(400, forbiddenAuditHistoryTombstoneError(forbiddenAuditTombstone.object_id), {
|
|
50961
|
+
code: AUDIT_HISTORY_TOMBSTONE_FORBIDDEN,
|
|
50962
|
+
conflict: false,
|
|
50963
|
+
audit_history_id: forbiddenAuditTombstone.object_id
|
|
50964
|
+
});
|
|
50965
|
+
}
|
|
50203
50966
|
const result = await store.sync.importSnapshot(snapshot, contextFromPrincipal(principal));
|
|
50967
|
+
const auditFailureMessage = result.errors.find((message) => parseAuditHistoryImportFailure(message) !== null);
|
|
50968
|
+
if (auditFailureMessage) {
|
|
50969
|
+
const failure = parseAuditHistoryImportFailure(auditFailureMessage);
|
|
50970
|
+
return error(failure.status, auditFailureMessage, {
|
|
50971
|
+
code: failure.code,
|
|
50972
|
+
conflict: failure.conflict,
|
|
50973
|
+
audit_history_id: failure.auditHistoryId
|
|
50974
|
+
});
|
|
50975
|
+
}
|
|
50204
50976
|
return json5({ result, received });
|
|
50205
50977
|
}
|
|
50206
50978
|
return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
|
|
@@ -50223,6 +50995,14 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
50223
50995
|
if (e instanceof TaskNotFoundError) {
|
|
50224
50996
|
return error(404, e.message, { code: TaskNotFoundError.code });
|
|
50225
50997
|
}
|
|
50998
|
+
if (e instanceof StaleLockHandoffError) {
|
|
50999
|
+
const status2 = e.code === "STALE_LOCK_HANDOFF_INVALID_TASK_ID" || e.code === "STALE_LOCK_HANDOFF_INVALID_INPUT" ? 400 : e.code === "STALE_LOCK_HANDOFF_ACTOR_MISMATCH" ? 403 : 409;
|
|
51000
|
+
return error(status2, e.message, {
|
|
51001
|
+
code: e.code,
|
|
51002
|
+
conflict: status2 === 409,
|
|
51003
|
+
...e.details
|
|
51004
|
+
});
|
|
51005
|
+
}
|
|
50226
51006
|
if (e instanceof PlanNotFoundError) {
|
|
50227
51007
|
return error(404, e.message, { code: PlanNotFoundError.code });
|
|
50228
51008
|
}
|
|
@@ -50257,6 +51037,8 @@ var init_v1 = __esm(() => {
|
|
|
50257
51037
|
init_redaction();
|
|
50258
51038
|
init_project_task_list_ensure();
|
|
50259
51039
|
init_plan_project_link();
|
|
51040
|
+
init_stale_lock_handoff();
|
|
51041
|
+
init_audit_history_import();
|
|
50260
51042
|
JSON_HEADERS4 = { "Content-Type": "application/json" };
|
|
50261
51043
|
RFC3339_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/;
|
|
50262
51044
|
});
|
|
@@ -75778,8 +76560,29 @@ No task claimed (nothing available).`));
|
|
|
75778
76560
|
handleError(new Error("Failed to assign"));
|
|
75779
76561
|
}
|
|
75780
76562
|
});
|
|
75781
|
-
program2.command("unassign <id>").description("Remove task assignment").option("-j, --json", "Output as JSON").action((id, opts) => {
|
|
76563
|
+
program2.command("unassign <id>").description("Remove task assignment").option("-j, --json", "Output as JSON").action(async (id, opts) => {
|
|
75782
76564
|
const globalOpts = program2.opts();
|
|
76565
|
+
const cloud = getTodosCloudClient();
|
|
76566
|
+
if (cloud) {
|
|
76567
|
+
try {
|
|
76568
|
+
const resolvedId2 = await resolveTaskIdForCommand(id, cloud);
|
|
76569
|
+
const task4 = await cloudGetTask(cloud, resolvedId2);
|
|
76570
|
+
if (!task4) {
|
|
76571
|
+
throw new Error(`Task not found: ${id}`);
|
|
76572
|
+
}
|
|
76573
|
+
const updated = await cloudUpdateTask(cloud, resolvedId2, {
|
|
76574
|
+
assigned_to: null
|
|
76575
|
+
});
|
|
76576
|
+
if (opts.json || globalOpts.json) {
|
|
76577
|
+
console.log(JSON.stringify(updated));
|
|
76578
|
+
return;
|
|
76579
|
+
}
|
|
76580
|
+
console.log(chalk9.green(`Unassigned: ${formatTaskLine(updated)}`));
|
|
76581
|
+
} catch (error2) {
|
|
76582
|
+
handleError(error2);
|
|
76583
|
+
}
|
|
76584
|
+
return;
|
|
76585
|
+
}
|
|
75783
76586
|
const resolvedId = resolveTaskId(id);
|
|
75784
76587
|
const db = getDatabase();
|
|
75785
76588
|
const task3 = getTask(resolvedId, db);
|
|
@@ -88241,6 +89044,7 @@ program2.name("todos").description("Universal task management for AI coding agen
|
|
|
88241
89044
|
var authority;
|
|
88242
89045
|
try {
|
|
88243
89046
|
authority = initializeTodosCliAuthority();
|
|
89047
|
+
applyTodosCliAuthorityEnvironment(authority);
|
|
88244
89048
|
} catch (error2) {
|
|
88245
89049
|
console.error(error2 instanceof Error ? error2.message : String(error2));
|
|
88246
89050
|
process.exit(1);
|