@hasna/todos 0.15.18 → 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/project-commands.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 +1306 -122
- package/dist/cli/stage-a.d.ts +21 -10
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +283 -16
- package/dist/db/audit.d.ts +8 -0
- package/dist/db/audit.d.ts.map +1 -1
- package/dist/db/plans.d.ts +4 -0
- package/dist/db/plans.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 +599 -26
- 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 +954 -46
- package/dist/mcp.js +3 -1
- package/dist/project-registration.js +4250 -3684
- package/dist/registry.js +283 -16
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/index.js +7 -0
- package/dist/sdk/v1.generated.d.ts +40 -1
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +1287 -379
- package/dist/server/openapi.d.ts +232 -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 +22 -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/shadow.d.ts.map +1 -1
- package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
- package/dist/storage.js +596 -25
- package/dist/task-manifest.js +24 -1
- package/dist/types/index.d.ts +50 -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, 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",
|
|
@@ -2357,6 +2359,19 @@ var init_types = __esm(() => {
|
|
|
2357
2359
|
this.name = "ResourceConflictError";
|
|
2358
2360
|
}
|
|
2359
2361
|
};
|
|
2362
|
+
PlanRevisionConflictError = class PlanRevisionConflictError extends Error {
|
|
2363
|
+
planId;
|
|
2364
|
+
expectedUpdatedAt;
|
|
2365
|
+
currentUpdatedAt;
|
|
2366
|
+
static code = "PLAN_REVISION_CONFLICT";
|
|
2367
|
+
constructor(planId, expectedUpdatedAt, currentUpdatedAt) {
|
|
2368
|
+
super(`Plan revision conflict for ${planId}: expected ${expectedUpdatedAt}, current ${currentUpdatedAt}`);
|
|
2369
|
+
this.planId = planId;
|
|
2370
|
+
this.expectedUpdatedAt = expectedUpdatedAt;
|
|
2371
|
+
this.currentUpdatedAt = currentUpdatedAt;
|
|
2372
|
+
this.name = "PlanRevisionConflictError";
|
|
2373
|
+
}
|
|
2374
|
+
};
|
|
2360
2375
|
PlanNotFoundError = class PlanNotFoundError extends Error {
|
|
2361
2376
|
planId;
|
|
2362
2377
|
static code = "PLAN_NOT_FOUND";
|
|
@@ -2379,6 +2394,16 @@ var init_types = __esm(() => {
|
|
|
2379
2394
|
this.name = "LockError";
|
|
2380
2395
|
}
|
|
2381
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
|
+
};
|
|
2382
2407
|
AgentNotFoundError = class AgentNotFoundError extends Error {
|
|
2383
2408
|
agentId;
|
|
2384
2409
|
static code = "AGENT_NOT_FOUND";
|
|
@@ -5467,6 +5492,7 @@ var init_http_client = __esm(() => {
|
|
|
5467
5492
|
// src/cli/cloud-router.ts
|
|
5468
5493
|
import { resolveStorageClient } from "@hasna/contracts/client/storage";
|
|
5469
5494
|
import { normalizeStorageMode } from "@hasna/contracts/mode";
|
|
5495
|
+
import { randomUUID } from "crypto";
|
|
5470
5496
|
import { resolve as resolvePath } from "path";
|
|
5471
5497
|
function cleanMode(value) {
|
|
5472
5498
|
const normalized = value?.trim().toLowerCase();
|
|
@@ -5819,6 +5845,45 @@ async function requestRawCloudTaskPage(client, filter) {
|
|
|
5819
5845
|
total: parseCloudTaskTotal(res.raw)
|
|
5820
5846
|
};
|
|
5821
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
|
+
}
|
|
5822
5887
|
function cloudTaskListFilterError(code, taskListId, detail) {
|
|
5823
5888
|
return new Error(`${code}: hosted authority returned rows outside requested task_list_id ${taskListId}; ${detail}; refusing an incomplete exact-list result`);
|
|
5824
5889
|
}
|
|
@@ -5935,7 +6000,10 @@ async function cloudGetTask(client, id) {
|
|
|
5935
6000
|
}
|
|
5936
6001
|
async function cloudCreateTask(client, input) {
|
|
5937
6002
|
const expectedParentId = typeof input["parent_id"] === "string" ? input["parent_id"] : null;
|
|
5938
|
-
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"]));
|
|
5939
6007
|
if (!created || typeof created.id !== "string" || !created.id.trim()) {
|
|
5940
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");
|
|
5941
6009
|
}
|
|
@@ -5946,7 +6014,7 @@ async function cloudCreateTask(client, input) {
|
|
|
5946
6014
|
return persisted;
|
|
5947
6015
|
}
|
|
5948
6016
|
async function cloudUpdateTask(client, id, patch) {
|
|
5949
|
-
return unwrapTask(await client.
|
|
6017
|
+
return unwrapTask(await client.transport.patch(`/tasks/${encodeURIComponent(id)}`, patch));
|
|
5950
6018
|
}
|
|
5951
6019
|
async function cloudDeleteTask(client, id) {
|
|
5952
6020
|
try {
|
|
@@ -6004,8 +6072,16 @@ async function cloudTaskAction(client, id, action, body = {}) {
|
|
|
6004
6072
|
const raw = await client.transport.post(`/tasks/${encodeURIComponent(id)}/${action}`, body);
|
|
6005
6073
|
return unwrapTask(raw);
|
|
6006
6074
|
}
|
|
6075
|
+
function isRemoteTaskIdentity(value) {
|
|
6076
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
6077
|
+
return false;
|
|
6078
|
+
const id = value["id"];
|
|
6079
|
+
return typeof id === "string" && id.length > 0;
|
|
6080
|
+
}
|
|
6007
6081
|
async function cloudFailTask(client, id, body = {}) {
|
|
6008
6082
|
const route = `/v1/tasks/${encodeURIComponent(id)}/fail`;
|
|
6083
|
+
if (body.retry === true)
|
|
6084
|
+
await requireRetryCapability(client);
|
|
6009
6085
|
const raw = await requiredRemoteRoute(client, route, () => client.transport.post(`/tasks/${encodeURIComponent(id)}/fail`, body));
|
|
6010
6086
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
6011
6087
|
throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned an invalid failure response envelope`);
|
|
@@ -6016,7 +6092,7 @@ async function cloudFailTask(client, id, body = {}) {
|
|
|
6016
6092
|
}
|
|
6017
6093
|
const task = result["task"];
|
|
6018
6094
|
const retryTask = result["retryTask"];
|
|
6019
|
-
if (!task ||
|
|
6095
|
+
if (!isRemoteTaskIdentity(task) || body.retry === true && !isRemoteTaskIdentity(retryTask) || body.retry !== true && retryTask !== undefined && !isRemoteTaskIdentity(retryTask)) {
|
|
6020
6096
|
throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned an invalid failure result`);
|
|
6021
6097
|
}
|
|
6022
6098
|
return result;
|
|
@@ -6039,6 +6115,36 @@ function resolveOpenApiSchema(document, schema) {
|
|
|
6039
6115
|
}
|
|
6040
6116
|
return current && typeof current === "object" && !Array.isArray(current) ? current : null;
|
|
6041
6117
|
}
|
|
6118
|
+
async function fetchRetryCapability(client) {
|
|
6119
|
+
const document = await requiredRemoteRoute(client, "/v1/openapi.json", () => client.transport.get("/openapi.json"));
|
|
6120
|
+
if (!document || typeof document !== "object" || Array.isArray(document))
|
|
6121
|
+
return false;
|
|
6122
|
+
const doc = document;
|
|
6123
|
+
const paths = doc["paths"];
|
|
6124
|
+
const failPath = paths && typeof paths === "object" && !Array.isArray(paths) ? paths["/v1/tasks/{id}/fail"] : undefined;
|
|
6125
|
+
const post = failPath && typeof failPath === "object" && !Array.isArray(failPath) ? failPath["post"] : undefined;
|
|
6126
|
+
const requestBody = post && typeof post === "object" && !Array.isArray(post) ? post["requestBody"] : undefined;
|
|
6127
|
+
const content = requestBody && typeof requestBody === "object" && !Array.isArray(requestBody) ? requestBody["content"] : undefined;
|
|
6128
|
+
const jsonContent = content && typeof content === "object" && !Array.isArray(content) ? content["application/json"] : undefined;
|
|
6129
|
+
const schema = jsonContent && typeof jsonContent === "object" && !Array.isArray(jsonContent) ? jsonContent["schema"] : undefined;
|
|
6130
|
+
const resolved = resolveOpenApiSchema(document, schema);
|
|
6131
|
+
const properties = resolved?.["properties"];
|
|
6132
|
+
if (!properties || typeof properties !== "object" || Array.isArray(properties))
|
|
6133
|
+
return false;
|
|
6134
|
+
const retry = properties["retry"];
|
|
6135
|
+
return !!retry && typeof retry === "object" && !Array.isArray(retry) && retry["type"] === "boolean";
|
|
6136
|
+
}
|
|
6137
|
+
async function requireRetryCapability(client) {
|
|
6138
|
+
const authority = remoteAuthorityBase(client);
|
|
6139
|
+
let capability = retryCapabilityCache.get(authority);
|
|
6140
|
+
if (!capability) {
|
|
6141
|
+
capability = fetchRetryCapability(client);
|
|
6142
|
+
retryCapabilityCache.set(authority, capability);
|
|
6143
|
+
}
|
|
6144
|
+
if (!await capability) {
|
|
6145
|
+
throw new Error(`REMOTE_RETRY_UNSUPPORTED: configured Todos authority ${authority} does not advertise retry in the ` + "application/json request schema for POST /v1/tasks/{id}/fail; no failure mutation was sent; " + "deploy a compatible @hasna/todos /v1 server before retrying; local SQLite fallback is disabled");
|
|
6146
|
+
}
|
|
6147
|
+
}
|
|
6042
6148
|
async function fetchCompletionCapabilities(client) {
|
|
6043
6149
|
const document = await requiredRemoteRoute(client, "/v1/openapi.json", () => client.transport.get("/openapi.json"));
|
|
6044
6150
|
if (!document || typeof document !== "object" || Array.isArray(document))
|
|
@@ -6296,11 +6402,62 @@ function unwrapPlan(raw) {
|
|
|
6296
6402
|
}
|
|
6297
6403
|
return raw;
|
|
6298
6404
|
}
|
|
6405
|
+
async function cloudGetPlanById(client, id) {
|
|
6406
|
+
const raw = await client.get("plans", id);
|
|
6407
|
+
return raw ? unwrapPlan(raw) : null;
|
|
6408
|
+
}
|
|
6409
|
+
function isUnavailablePlanPatchRoute(error) {
|
|
6410
|
+
if (!error || typeof error !== "object" || error.status !== 404)
|
|
6411
|
+
return false;
|
|
6412
|
+
const body = error.body;
|
|
6413
|
+
const remoteMessage = body && typeof body === "object" && !Array.isArray(body) ? body.error : undefined;
|
|
6414
|
+
return remoteMessage === "unknown /v1 resource: plans";
|
|
6415
|
+
}
|
|
6416
|
+
function isExactPlanCompletionPatch(patch) {
|
|
6417
|
+
const entries = Object.entries(patch).filter(([, value]) => value !== undefined);
|
|
6418
|
+
return entries.length === 1 && entries[0]?.[0] === "status" && entries[0]?.[1] === "completed";
|
|
6419
|
+
}
|
|
6299
6420
|
async function cloudCreatePlan(client, input) {
|
|
6300
6421
|
return unwrapPlan(await requiredRemoteRoute(client, "/v1/plans", () => client.create("plans", input)));
|
|
6301
6422
|
}
|
|
6302
6423
|
async function cloudUpdatePlan(client, id, patch) {
|
|
6303
|
-
|
|
6424
|
+
try {
|
|
6425
|
+
return unwrapPlan(await client.update("plans", id, patch));
|
|
6426
|
+
} catch (error) {
|
|
6427
|
+
if (!isExactPlanCompletionPatch(patch) || !isUnavailablePlanPatchRoute(error))
|
|
6428
|
+
throw error;
|
|
6429
|
+
const existing = await cloudGetPlanById(client, id);
|
|
6430
|
+
if (!existing)
|
|
6431
|
+
throw error;
|
|
6432
|
+
if (existing.status === "completed")
|
|
6433
|
+
return existing;
|
|
6434
|
+
const imported = await requiredRemoteRoute(client, "/v1/import", () => client.transport.post("/import", {
|
|
6435
|
+
source: "postgres",
|
|
6436
|
+
planCompletions: [{
|
|
6437
|
+
id,
|
|
6438
|
+
expected_updated_at: existing.updated_at,
|
|
6439
|
+
status: "completed"
|
|
6440
|
+
}]
|
|
6441
|
+
}));
|
|
6442
|
+
const envelope = imported && typeof imported === "object" && !Array.isArray(imported) ? imported : null;
|
|
6443
|
+
const completion = envelope?.planCompletions?.[0];
|
|
6444
|
+
if (envelope?.received !== 1 || !envelope.result || !Array.isArray(envelope.result.errors) || envelope.result.errors.length > 0 || envelope.planCompletions?.length !== 1 || completion?.id !== id || completion.status !== "completed" || completion.expected_updated_at !== existing.updated_at || typeof completion.result_updated_at !== "string" || typeof completion.applied !== "boolean") {
|
|
6445
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: /v1/import did not confirm one atomic plan completion; " + "local SQLite fallback is disabled");
|
|
6446
|
+
}
|
|
6447
|
+
const persisted = await cloudGetPlanById(client, id);
|
|
6448
|
+
if (!persisted) {
|
|
6449
|
+
throw new Error(`REMOTE_API_INCOMPATIBLE: /v1/import acknowledged plan completion ${id} but authoritative readback returned no plan; ` + "local SQLite fallback is disabled");
|
|
6450
|
+
}
|
|
6451
|
+
if (persisted.status !== "completed" || persisted.updated_at !== completion.result_updated_at) {
|
|
6452
|
+
throw new Error(`REMOTE_API_INCOMPATIBLE: /v1/import completion readback did not match its receipt for plan ${id}; ` + "local SQLite fallback is disabled");
|
|
6453
|
+
}
|
|
6454
|
+
for (const key of PLAN_COMPLETION_PROTECTED_FIELDS) {
|
|
6455
|
+
if (persisted[key] !== existing[key]) {
|
|
6456
|
+
throw new Error(`REMOTE_PLAN_COMPLETION_CONFLICT: /v1/import completion changed protected plan field ${key}; ` + "local SQLite fallback is disabled");
|
|
6457
|
+
}
|
|
6458
|
+
}
|
|
6459
|
+
return persisted;
|
|
6460
|
+
}
|
|
6304
6461
|
}
|
|
6305
6462
|
async function cloudDeletePlan(client, id) {
|
|
6306
6463
|
try {
|
|
@@ -6608,6 +6765,19 @@ async function cloudUnlockTask(client, id, agentId, force = false) {
|
|
|
6608
6765
|
}
|
|
6609
6766
|
return true;
|
|
6610
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
|
+
}
|
|
6611
6781
|
async function cloudGetDependencies(client, id) {
|
|
6612
6782
|
const raw = await client.transport.get(`/tasks/${encodeURIComponent(id)}/dependencies`);
|
|
6613
6783
|
const env = raw ?? {};
|
|
@@ -7043,7 +7213,7 @@ async function cloudTimeline(client, options = {}) {
|
|
|
7043
7213
|
const limit = options.limit ?? 50;
|
|
7044
7214
|
return { entries: entries.slice(offset, offset + limit), total, limit, offset };
|
|
7045
7215
|
}
|
|
7046
|
-
var UUID_RE, TRANSPORT_TOKENS, COMPLETION_EVIDENCE_FIELDS, completionCapabilityCache, gitRefCapabilityCache, SERVER_MODE_CANDIDATES, cachedServerMode = null, PRIORITY_RANK, listTagsCapabilityCache, RELATION_HYDRATION_CONCURRENCY = 6;
|
|
7216
|
+
var UUID_RE, TRANSPORT_TOKENS, COMPLETION_EVIDENCE_FIELDS, completionCapabilityCache, retryCapabilityCache, gitRefCapabilityCache, SERVER_MODE_CANDIDATES, cachedServerMode = null, PRIORITY_RANK, listTagsCapabilityCache, PLAN_COMPLETION_PROTECTED_FIELDS, RELATION_HYDRATION_CONCURRENCY = 6;
|
|
7047
7217
|
var init_cloud_router = __esm(() => {
|
|
7048
7218
|
init_types();
|
|
7049
7219
|
init_redaction();
|
|
@@ -7068,13 +7238,32 @@ var init_cloud_router = __esm(() => {
|
|
|
7068
7238
|
"confidence"
|
|
7069
7239
|
];
|
|
7070
7240
|
completionCapabilityCache = new Map;
|
|
7241
|
+
retryCapabilityCache = new Map;
|
|
7071
7242
|
gitRefCapabilityCache = new Map;
|
|
7072
7243
|
SERVER_MODE_CANDIDATES = ["postgres", "cloud", "self_hosted"];
|
|
7073
7244
|
PRIORITY_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
7074
7245
|
listTagsCapabilityCache = new Map;
|
|
7246
|
+
PLAN_COMPLETION_PROTECTED_FIELDS = [
|
|
7247
|
+
"id",
|
|
7248
|
+
"slug",
|
|
7249
|
+
"project_id",
|
|
7250
|
+
"task_list_id",
|
|
7251
|
+
"agent_id",
|
|
7252
|
+
"name",
|
|
7253
|
+
"description",
|
|
7254
|
+
"created_at",
|
|
7255
|
+
"machine_id",
|
|
7256
|
+
"synced_at"
|
|
7257
|
+
];
|
|
7075
7258
|
});
|
|
7076
7259
|
|
|
7077
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
|
+
}
|
|
7078
7267
|
function isTodosCliCommandVisibleForRoute(command, route) {
|
|
7079
7268
|
if (route === "local")
|
|
7080
7269
|
return true;
|
|
@@ -7284,7 +7473,7 @@ function nearestCommands(command, limit = 3) {
|
|
|
7284
7473
|
const threshold = command.length <= 4 ? 1 : command.length <= 8 ? 2 : 3;
|
|
7285
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);
|
|
7286
7475
|
}
|
|
7287
|
-
function
|
|
7476
|
+
function assertInvocationRoutable(invocation) {
|
|
7288
7477
|
if (invocation.invalidGlobalOption) {
|
|
7289
7478
|
throw new Error(`REMOTE_COMMAND_UNSUPPORTED: the global option ${invocation.invalidGlobalOption} was given without a value; ` + `pass one as \`${invocation.invalidGlobalOption} <value>\``);
|
|
7290
7479
|
}
|
|
@@ -7298,8 +7487,12 @@ function assertRemoteCommandSupported(invocation) {
|
|
|
7298
7487
|
const didYouMean = suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}?` : "";
|
|
7299
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.)");
|
|
7300
7489
|
}
|
|
7490
|
+
return owner;
|
|
7491
|
+
}
|
|
7492
|
+
function assertRemoteCommandSupported(invocation, owner) {
|
|
7493
|
+
const command = invocation.command;
|
|
7301
7494
|
if (command && owner === "local-only") {
|
|
7302
|
-
throw new Error(`
|
|
7495
|
+
throw new Error(`LOCAL_COMMAND_ROUTING_INVARIANT: \`${command}\` must select the local command route before remote authority validation`);
|
|
7303
7496
|
}
|
|
7304
7497
|
if (!command || !commandSupportsRemote(invocation)) {
|
|
7305
7498
|
const blame = command ? disqualifyingArgument(invocation) : null;
|
|
@@ -7317,7 +7510,11 @@ function initializeTodosCliAuthority(args = process.argv.slice(2), env = process
|
|
|
7317
7510
|
const status = getTodosRemoteAuthorityConfigStatus(env);
|
|
7318
7511
|
return { route: "remote-diagnostic", v1_base_url: status.v1_base_url };
|
|
7319
7512
|
}
|
|
7320
|
-
|
|
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);
|
|
7321
7518
|
const client = getTodosCloudClient(env);
|
|
7322
7519
|
if (!client) {
|
|
7323
7520
|
throw new Error("REMOTE_API_UNAVAILABLE: remote mode did not resolve an HTTP client; local SQLite fallback is disabled");
|
|
@@ -7451,6 +7648,7 @@ var init_stage_a = __esm(() => {
|
|
|
7451
7648
|
"sprint",
|
|
7452
7649
|
"stale",
|
|
7453
7650
|
"standup",
|
|
7651
|
+
"stale-lock-handoff",
|
|
7454
7652
|
"start",
|
|
7455
7653
|
"status",
|
|
7456
7654
|
"steal",
|
|
@@ -7583,6 +7781,7 @@ var init_stage_a = __esm(() => {
|
|
|
7583
7781
|
"tag",
|
|
7584
7782
|
"task",
|
|
7585
7783
|
"task-lists",
|
|
7784
|
+
"stale-lock-handoff",
|
|
7586
7785
|
"template-export",
|
|
7587
7786
|
"template-import",
|
|
7588
7787
|
"template-preview",
|
|
@@ -7590,6 +7789,7 @@ var init_stage_a = __esm(() => {
|
|
|
7590
7789
|
"timeline",
|
|
7591
7790
|
"tl",
|
|
7592
7791
|
"unlock",
|
|
7792
|
+
"unassign",
|
|
7593
7793
|
"untag",
|
|
7594
7794
|
"update"
|
|
7595
7795
|
]);
|
|
@@ -13475,7 +13675,7 @@ __export(exports_event_hooks, {
|
|
|
13475
13675
|
emitLocalEventHooks: () => emitLocalEventHooks,
|
|
13476
13676
|
LOCAL_EVENT_TYPES: () => LOCAL_EVENT_TYPES
|
|
13477
13677
|
});
|
|
13478
|
-
import { createHash as createHash4, randomUUID } from "crypto";
|
|
13678
|
+
import { createHash as createHash4, randomUUID as randomUUID2 } from "crypto";
|
|
13479
13679
|
import { appendFileSync, mkdirSync as mkdirSync4 } from "fs";
|
|
13480
13680
|
import { dirname as dirname4, resolve as resolve7 } from "path";
|
|
13481
13681
|
import { createConnection } from "net";
|
|
@@ -13536,7 +13736,7 @@ function canonicalEvent(input) {
|
|
|
13536
13736
|
}
|
|
13537
13737
|
function buildEnvelope(type, payload, timestamp2 = new Date().toISOString()) {
|
|
13538
13738
|
const base = {
|
|
13539
|
-
id:
|
|
13739
|
+
id: randomUUID2(),
|
|
13540
13740
|
type,
|
|
13541
13741
|
timestamp: timestamp2,
|
|
13542
13742
|
payload: redactValue(payload ?? {}),
|
|
@@ -13739,7 +13939,7 @@ import { existsSync as existsSync6 } from "fs";
|
|
|
13739
13939
|
import { homedir as homedir2 } from "os";
|
|
13740
13940
|
import { join as join5 } from "path";
|
|
13741
13941
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
13742
|
-
import { randomUUID as
|
|
13942
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
13743
13943
|
import { spawn } from "child_process";
|
|
13744
13944
|
import { randomUUID as randomUUID22 } from "crypto";
|
|
13745
13945
|
function getPathValue(input, path) {
|
|
@@ -14097,7 +14297,7 @@ async function dispatchChannel(event, channel, options = {}) {
|
|
|
14097
14297
|
function createDeliveryResult(event, channel, attempts) {
|
|
14098
14298
|
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
14099
14299
|
return {
|
|
14100
|
-
id:
|
|
14300
|
+
id: randomUUID3(),
|
|
14101
14301
|
eventId: event.id,
|
|
14102
14302
|
channelId: channel.id,
|
|
14103
14303
|
transport: channel.transport,
|
|
@@ -15099,6 +15299,7 @@ var init_activity_audit = __esm(() => {
|
|
|
15099
15299
|
var exports_audit = {};
|
|
15100
15300
|
__export(exports_audit, {
|
|
15101
15301
|
logTaskChange: () => logTaskChange,
|
|
15302
|
+
insertTaskHistory: () => insertTaskHistory,
|
|
15102
15303
|
getTaskHistory: () => getTaskHistory,
|
|
15103
15304
|
getRecentActivity: () => getRecentActivity,
|
|
15104
15305
|
getRecap: () => getRecap
|
|
@@ -15106,28 +15307,55 @@ __export(exports_audit, {
|
|
|
15106
15307
|
function sanitizeHistoryValue(value, context) {
|
|
15107
15308
|
return value === undefined || value === null ? null : sanitizePreWriteText(String(value), context);
|
|
15108
15309
|
}
|
|
15109
|
-
function
|
|
15310
|
+
function insertTaskHistory(entry, db) {
|
|
15110
15311
|
const d = db || getDatabase();
|
|
15111
|
-
const
|
|
15112
|
-
|
|
15113
|
-
|
|
15114
|
-
|
|
15115
|
-
|
|
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
|
+
};
|
|
15116
15320
|
d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at, machine_id)
|
|
15117
|
-
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
|
+
]);
|
|
15118
15332
|
try {
|
|
15119
15333
|
const { logActivity: logActivity2 } = (init_activity_audit(), __toCommonJS(exports_activity_audit));
|
|
15120
15334
|
logActivity2({
|
|
15121
15335
|
entity_type: "task",
|
|
15122
|
-
entity_id:
|
|
15123
|
-
action,
|
|
15124
|
-
field,
|
|
15125
|
-
old_value:
|
|
15126
|
-
new_value:
|
|
15127
|
-
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
|
|
15128
15342
|
}, d);
|
|
15129
15343
|
} catch {}
|
|
15130
|
-
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);
|
|
15131
15359
|
}
|
|
15132
15360
|
function getTaskHistory(taskId, db) {
|
|
15133
15361
|
const d = db || getDatabase();
|
|
@@ -16188,6 +16416,145 @@ var init_task_graph = __esm(() => {
|
|
|
16188
16416
|
init_task_crud();
|
|
16189
16417
|
});
|
|
16190
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
|
+
|
|
16191
16558
|
// src/db/task-lifecycle.ts
|
|
16192
16559
|
var exports_task_lifecycle = {};
|
|
16193
16560
|
__export(exports_task_lifecycle, {
|
|
@@ -16196,6 +16563,7 @@ __export(exports_task_lifecycle, {
|
|
|
16196
16563
|
startTask: () => startTask,
|
|
16197
16564
|
spawnNextRecurrence: () => spawnNextRecurrence,
|
|
16198
16565
|
lockTask: () => lockTask,
|
|
16566
|
+
handoffStaleTaskLock: () => handoffStaleTaskLock,
|
|
16199
16567
|
getTasksChangedSince: () => getTasksChangedSince,
|
|
16200
16568
|
getTaskLockStatus: () => getTaskLockStatus,
|
|
16201
16569
|
getStaleTasks: () => getStaleTasks,
|
|
@@ -16464,6 +16832,38 @@ function unlockTask(id, agentId, db) {
|
|
|
16464
16832
|
WHERE id = ?`, [timestamp2, id]);
|
|
16465
16833
|
return true;
|
|
16466
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
|
+
}
|
|
16467
16867
|
function getTaskLockStatus(id, db) {
|
|
16468
16868
|
const d = db || getDatabase();
|
|
16469
16869
|
const task = getTask(id, d);
|
|
@@ -16764,6 +17164,7 @@ var init_task_lifecycle = __esm(() => {
|
|
|
16764
17164
|
init_task_crud();
|
|
16765
17165
|
init_task_graph();
|
|
16766
17166
|
init_prewrite_secrets();
|
|
17167
|
+
init_stale_lock_handoff();
|
|
16767
17168
|
});
|
|
16768
17169
|
|
|
16769
17170
|
// src/db/task-crud.ts
|
|
@@ -18333,6 +18734,48 @@ function updatePlan(id, input, db) {
|
|
|
18333
18734
|
return updatePlanStored(id, input, d);
|
|
18334
18735
|
})();
|
|
18335
18736
|
}
|
|
18737
|
+
function nextPlanCompletionTimestamp(expectedUpdatedAt) {
|
|
18738
|
+
const expected = Date.parse(expectedUpdatedAt);
|
|
18739
|
+
const minimum = Number.isNaN(expected) ? Date.now() : expected + 2;
|
|
18740
|
+
return new Date(Math.max(Date.now(), minimum)).toISOString();
|
|
18741
|
+
}
|
|
18742
|
+
function completePlanAtRevision(id, expectedUpdatedAt, db) {
|
|
18743
|
+
const d = db || getDatabase();
|
|
18744
|
+
return d.transaction(() => {
|
|
18745
|
+
guardPlanRowsSqlite([id], d);
|
|
18746
|
+
const plan = getPlan(id, d);
|
|
18747
|
+
if (!plan)
|
|
18748
|
+
throw new PlanNotFoundError(id);
|
|
18749
|
+
if (plan.updated_at !== expectedUpdatedAt) {
|
|
18750
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, plan.updated_at);
|
|
18751
|
+
}
|
|
18752
|
+
if (plan.status === "completed")
|
|
18753
|
+
return { plan, applied: false };
|
|
18754
|
+
const updatedAt = nextPlanCompletionTimestamp(expectedUpdatedAt);
|
|
18755
|
+
const result = d.run(`UPDATE plans
|
|
18756
|
+
SET status = 'completed', updated_at = ?
|
|
18757
|
+
WHERE id = ? AND updated_at = ? AND status <> 'completed'`, [updatedAt, id, expectedUpdatedAt]);
|
|
18758
|
+
if (result.changes !== 1) {
|
|
18759
|
+
const current = getPlan(id, d);
|
|
18760
|
+
if (!current)
|
|
18761
|
+
throw new PlanNotFoundError(id);
|
|
18762
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
|
|
18763
|
+
}
|
|
18764
|
+
const completed = getPlan(id, d);
|
|
18765
|
+
emitLocalEventHooksQuiet({
|
|
18766
|
+
type: "plan.updated",
|
|
18767
|
+
payload: {
|
|
18768
|
+
id,
|
|
18769
|
+
old_status: plan.status,
|
|
18770
|
+
new_status: completed.status,
|
|
18771
|
+
name: completed.name,
|
|
18772
|
+
project_id: completed.project_id
|
|
18773
|
+
},
|
|
18774
|
+
databasePath: databasePathFromDatabase(d)
|
|
18775
|
+
});
|
|
18776
|
+
return { plan: completed, applied: true };
|
|
18777
|
+
})();
|
|
18778
|
+
}
|
|
18336
18779
|
function deletePlan(id, db) {
|
|
18337
18780
|
const d = db || getDatabase();
|
|
18338
18781
|
const plan = getPlan(id, d);
|
|
@@ -20331,6 +20774,7 @@ __export(exports_tasks, {
|
|
|
20331
20774
|
insertTaskTags: () => insertTaskTags,
|
|
20332
20775
|
importTaskBoardBundle: () => importTaskBoardBundle,
|
|
20333
20776
|
importCalendarIcs: () => importCalendarIcs,
|
|
20777
|
+
handoffStaleTaskLock: () => handoffStaleTaskLock,
|
|
20334
20778
|
getTimeReport: () => getTimeReport,
|
|
20335
20779
|
getTimeLogs: () => getTimeLogs,
|
|
20336
20780
|
getTasksChangedSince: () => getTasksChangedSince,
|
|
@@ -21973,7 +22417,7 @@ function registerTaskCommands(program2) {
|
|
|
21973
22417
|
}
|
|
21974
22418
|
});
|
|
21975
22419
|
const task = program2.command("task").description("Task subcommands for deterministic automation");
|
|
21976
|
-
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) => {
|
|
21977
22421
|
const globalOpts = program2.opts();
|
|
21978
22422
|
opts.tags = opts.tags || opts.tag;
|
|
21979
22423
|
opts.list = opts.list || opts.taskList;
|
|
@@ -21987,6 +22431,10 @@ function registerTaskCommands(program2) {
|
|
|
21987
22431
|
try {
|
|
21988
22432
|
const projectId2 = explicitProject ? await cloudResolveProjectRef(cloud, explicitProject) : undefined;
|
|
21989
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
|
+
}
|
|
21990
22438
|
cloudResult = await cloudUpsertTaskByFingerprint(cloud, {
|
|
21991
22439
|
fingerprint: opts.fingerprint,
|
|
21992
22440
|
title: opts.title,
|
|
@@ -21998,7 +22446,8 @@ function registerTaskCommands(program2) {
|
|
|
21998
22446
|
metadata: buildExpectationMetadata(opts),
|
|
21999
22447
|
working_dir: opts.workingDir ? resolve9(opts.workingDir) : process.cwd(),
|
|
22000
22448
|
project_id: projectId2,
|
|
22001
|
-
assigned_to: opts.assign
|
|
22449
|
+
assigned_to: opts.assign,
|
|
22450
|
+
plan_id: plan?.id
|
|
22002
22451
|
});
|
|
22003
22452
|
} catch (e) {
|
|
22004
22453
|
handleError(e);
|
|
@@ -22020,6 +22469,7 @@ function registerTaskCommands(program2) {
|
|
|
22020
22469
|
}
|
|
22021
22470
|
return id;
|
|
22022
22471
|
})() : undefined;
|
|
22472
|
+
const planId = opts.plan ? resolvePlanId(opts.plan) : undefined;
|
|
22023
22473
|
let result;
|
|
22024
22474
|
try {
|
|
22025
22475
|
result = upsertTaskByFingerprint({
|
|
@@ -22034,6 +22484,7 @@ function registerTaskCommands(program2) {
|
|
|
22034
22484
|
working_dir: opts.workingDir ? resolve9(opts.workingDir) : process.cwd(),
|
|
22035
22485
|
project_id: projectId,
|
|
22036
22486
|
assigned_to: opts.assign,
|
|
22487
|
+
plan_id: planId,
|
|
22037
22488
|
agent_id: globalOpts.agent,
|
|
22038
22489
|
session_id: globalOpts.session
|
|
22039
22490
|
});
|
|
@@ -23027,6 +23478,44 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
|
|
|
23027
23478
|
console.log(chalk3.green("Lock released."));
|
|
23028
23479
|
}
|
|
23029
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
|
+
});
|
|
23030
23519
|
program2.command("delete <id>").description("Delete a task").action(async (id) => {
|
|
23031
23520
|
const globalOpts = program2.opts();
|
|
23032
23521
|
const cloud = getTodosCloudClient();
|
|
@@ -23211,6 +23700,7 @@ var init_task_commands = __esm(() => {
|
|
|
23211
23700
|
init_agents();
|
|
23212
23701
|
init_helpers();
|
|
23213
23702
|
init_output_redaction();
|
|
23703
|
+
init_stale_lock_handoff();
|
|
23214
23704
|
});
|
|
23215
23705
|
|
|
23216
23706
|
// src/lib/plan-artifacts.ts
|
|
@@ -24198,6 +24688,58 @@ var init_plan_project_links = __esm(() => {
|
|
|
24198
24688
|
init_tasks();
|
|
24199
24689
|
});
|
|
24200
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
|
+
|
|
24201
24743
|
// src/storage/sqlite-snapshot.ts
|
|
24202
24744
|
function exportSqliteTodosStorageSnapshot(db) {
|
|
24203
24745
|
const d = db ?? getDatabase();
|
|
@@ -24231,9 +24773,12 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
24231
24773
|
const existingTaskLists = d.query("SELECT id, project_id, slug FROM task_lists").all();
|
|
24232
24774
|
result.errors.push(...validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists));
|
|
24233
24775
|
}
|
|
24776
|
+
const auditImport = preflightAuditHistoryImport(d, snapshot.auditHistory, snapshot.tombstones ?? []);
|
|
24777
|
+
result.errors.push(...auditImport.errors);
|
|
24234
24778
|
if (result.errors.length > 0)
|
|
24235
24779
|
return result;
|
|
24236
|
-
|
|
24780
|
+
result.skipped += auditImport.identicalReplayCount;
|
|
24781
|
+
const applyRows = (objectType, table, columns, rows, updateClockColumn, acceptEqualClock = true, afterUpsert) => {
|
|
24237
24782
|
for (const row of rows) {
|
|
24238
24783
|
try {
|
|
24239
24784
|
const record = asRecord(row);
|
|
@@ -24242,7 +24787,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
24242
24787
|
result.skipped += 1;
|
|
24243
24788
|
continue;
|
|
24244
24789
|
}
|
|
24245
|
-
const state = upsertById(d, table, columns, record, updateClockColumn);
|
|
24790
|
+
const state = upsertById(d, table, columns, record, updateClockColumn, acceptEqualClock);
|
|
24246
24791
|
if (state === "inserted")
|
|
24247
24792
|
result.inserted += 1;
|
|
24248
24793
|
else if (state === "updated")
|
|
@@ -24259,19 +24804,81 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
24259
24804
|
applyRows("project_machine_paths", "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS, snapshot.projectMachinePaths ?? [], "updated_at");
|
|
24260
24805
|
applyRows("agents", "agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
|
|
24261
24806
|
applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
|
|
24262
|
-
applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
|
|
24807
|
+
applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at", false);
|
|
24263
24808
|
applyRows("templates", "task_templates", TEMPLATE_COLUMNS, snapshot.templates);
|
|
24264
24809
|
applyRows("template_tasks", "template_tasks", TEMPLATE_TASK_COLUMNS, snapshot.templateTasks ?? []);
|
|
24265
|
-
applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks(snapshot.tasks), "updated_at", (row, changed) => {
|
|
24810
|
+
applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks(snapshot.tasks), "updated_at", true, (row, changed) => {
|
|
24266
24811
|
if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
|
|
24267
24812
|
replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
|
|
24268
24813
|
}
|
|
24269
24814
|
});
|
|
24270
|
-
|
|
24815
|
+
insertAuditHistoryRows(d, auditImport.rowsToInsert, result);
|
|
24271
24816
|
applyTombstones(d, snapshot.tombstones ?? [], result);
|
|
24272
24817
|
return result;
|
|
24273
24818
|
}
|
|
24274
|
-
function
|
|
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
|
+
}
|
|
24881
|
+
function upsertById(db, table, columns, row, updateClockColumn, acceptEqualClock = true) {
|
|
24275
24882
|
const id = row["id"];
|
|
24276
24883
|
if (typeof id !== "string" || !id)
|
|
24277
24884
|
throw new Error(`${table} row is missing id`);
|
|
@@ -24283,7 +24890,7 @@ function upsertById(db, table, columns, row, updateClockColumn) {
|
|
|
24283
24890
|
const values = presentColumns.map((column) => valueForColumn(column, row[column]));
|
|
24284
24891
|
const updateColumns = presentColumns.filter((column) => column !== "id");
|
|
24285
24892
|
const updateSet = updateColumns.map((column) => column === "version" ? `version = MAX(COALESCE(${table}.version, 0), excluded.version)` : `${column} = excluded.${column}`).join(", ");
|
|
24286
|
-
const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} <= excluded.${updateClockColumn}` : "";
|
|
24893
|
+
const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} ${acceptEqualClock ? "<=" : "<"} excluded.${updateClockColumn}` : "";
|
|
24287
24894
|
const sql = updateSet ? `INSERT INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})
|
|
24288
24895
|
ON CONFLICT(id) DO UPDATE SET ${updateSet}${clockGuard}` : `INSERT OR IGNORE INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})`;
|
|
24289
24896
|
const changes = db.run(sql, values).changes;
|
|
@@ -24368,7 +24975,7 @@ function tableForTombstone(objectType) {
|
|
|
24368
24975
|
return "task_templates";
|
|
24369
24976
|
if (objectType === "template_tasks")
|
|
24370
24977
|
return "template_tasks";
|
|
24371
|
-
|
|
24978
|
+
throw new Error(`unsupported storage tombstone object_type: ${String(objectType)}`);
|
|
24372
24979
|
}
|
|
24373
24980
|
function listRows(db, table, columns) {
|
|
24374
24981
|
return db.query(`SELECT ${columns.join(", ")} FROM ${table} ORDER BY id`).all();
|
|
@@ -24415,6 +25022,7 @@ var init_sqlite_snapshot = __esm(() => {
|
|
|
24415
25022
|
init_tasks();
|
|
24416
25023
|
init_templates();
|
|
24417
25024
|
init_storage_tombstones();
|
|
25025
|
+
init_audit_history_import();
|
|
24418
25026
|
PROJECT_COLUMNS = [
|
|
24419
25027
|
"id",
|
|
24420
25028
|
"name",
|
|
@@ -24679,6 +25287,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
24679
25287
|
unlockTask(id, agentId, database());
|
|
24680
25288
|
return true;
|
|
24681
25289
|
},
|
|
25290
|
+
handoffStaleLock: (input) => handoffStaleTaskLock(input, database()),
|
|
24682
25291
|
delete: (id) => deleteTask(id, database()),
|
|
24683
25292
|
start: (id, agentId) => startTask(id, agentId, database()),
|
|
24684
25293
|
complete: (id, agentId, options2) => completeTask(id, agentId, database(), options2),
|
|
@@ -24702,6 +25311,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
24702
25311
|
get: (id) => getPlan(id, database()),
|
|
24703
25312
|
list: (projectId) => listPlans(projectId, database()),
|
|
24704
25313
|
update: (id, input) => updatePlan(id, input, database()),
|
|
25314
|
+
completeAtRevision: (id, expectedUpdatedAt) => completePlanAtRevision(id, expectedUpdatedAt, database()),
|
|
24705
25315
|
delete: (id) => deletePlan(id, database())
|
|
24706
25316
|
},
|
|
24707
25317
|
planProjectLinks: {
|
|
@@ -25434,7 +26044,7 @@ function registerPlanTemplateCommands(program2) {
|
|
|
25434
26044
|
if (!plan2) {
|
|
25435
26045
|
handleError(new Error(`Plan not found: ${opts.show}`));
|
|
25436
26046
|
}
|
|
25437
|
-
const tasks2 = await
|
|
26047
|
+
const tasks2 = await cloudListPlanTasks(cloud, plan2.id);
|
|
25438
26048
|
if (globalOpts.json) {
|
|
25439
26049
|
output({ plan: plan2, tasks: tasks2, artifact: null }, true);
|
|
25440
26050
|
return;
|
|
@@ -29234,6 +29844,7 @@ __export(exports_project_commands, {
|
|
|
29234
29844
|
registerProjectCommands: () => registerProjectCommands
|
|
29235
29845
|
});
|
|
29236
29846
|
import chalk5 from "chalk";
|
|
29847
|
+
import { readFileSync as readFileSync9, statSync as statSync6 } from "fs";
|
|
29237
29848
|
import { basename as basename5, resolve as resolve13, sep as sep3 } from "path";
|
|
29238
29849
|
function collectOption(value, previous = []) {
|
|
29239
29850
|
return [...previous, value];
|
|
@@ -29427,18 +30038,45 @@ function registerProjectCommands(program2) {
|
|
|
29427
30038
|
handleError(e);
|
|
29428
30039
|
}
|
|
29429
30040
|
});
|
|
29430
|
-
program2.command("comment <id>
|
|
30041
|
+
program2.command("comment <id> [text]").alias("log-progress").description("Add a comment to a task (alias: log-progress, for recording intermediate progress)").option("--file <path>", "Read comment text from a UTF-8 file").option("--pct <percent>", "Progress percentage (0-100) to record alongside the note").action(async (id, text, opts) => {
|
|
30042
|
+
const hasPositionalContent = text !== undefined;
|
|
30043
|
+
const hasFileContent = opts.file !== undefined;
|
|
30044
|
+
if (hasPositionalContent === hasFileContent) {
|
|
30045
|
+
handleError(new Error("Provide exactly one comment content source: positional text or --file <path>, not both."));
|
|
30046
|
+
}
|
|
30047
|
+
let content;
|
|
30048
|
+
if (opts.file !== undefined) {
|
|
30049
|
+
const commentFilePath = resolve13(opts.file);
|
|
30050
|
+
let isRegularFile = false;
|
|
30051
|
+
try {
|
|
30052
|
+
isRegularFile = statSync6(commentFilePath).isFile();
|
|
30053
|
+
} catch (error) {
|
|
30054
|
+
handleError(new Error(`Unable to read comment file "${opts.file}".`, { cause: error }));
|
|
30055
|
+
}
|
|
30056
|
+
if (!isRegularFile) {
|
|
30057
|
+
handleError(new Error(`Comment file "${opts.file}" must be a regular file.`));
|
|
30058
|
+
}
|
|
30059
|
+
try {
|
|
30060
|
+
content = readFileSync9(commentFilePath, "utf8");
|
|
30061
|
+
} catch (error) {
|
|
30062
|
+
handleError(new Error(`Unable to read comment file "${opts.file}".`, { cause: error }));
|
|
30063
|
+
}
|
|
30064
|
+
} else {
|
|
30065
|
+
content = text;
|
|
30066
|
+
}
|
|
30067
|
+
if (!content.trim()) {
|
|
30068
|
+
handleError(new Error("Comment content must not be empty."));
|
|
30069
|
+
}
|
|
29431
30070
|
const globalOpts = program2.opts();
|
|
29432
30071
|
const cloud = getTodosCloudClient();
|
|
29433
30072
|
const resolvedId = await resolveTaskIdForCommand(id, cloud);
|
|
29434
|
-
let content = text;
|
|
29435
30073
|
let progressPct;
|
|
29436
30074
|
if (opts.pct !== undefined) {
|
|
29437
30075
|
const pct = parseInt(opts.pct, 10);
|
|
29438
30076
|
if (!Number.isFinite(pct) || pct < 0 || pct > 100) {
|
|
29439
30077
|
handleError(new Error("--pct must be a number between 0 and 100"));
|
|
29440
30078
|
}
|
|
29441
|
-
content = `[progress ${pct}%] ${
|
|
30079
|
+
content = `[progress ${pct}%] ${content}`;
|
|
29442
30080
|
progressPct = pct;
|
|
29443
30081
|
}
|
|
29444
30082
|
const router = resolveWritableIdentity(globalOpts.agent);
|
|
@@ -30198,10 +30836,10 @@ Indexed ${result.index.files.length} file(s), ${result.index.total_symbols} symb
|
|
|
30198
30836
|
program2.command("bridge-import <file>").description("Dry-run or apply a local hasna/todos bridge export bundle").option("--apply", "Apply the import. Defaults to dry-run.").option("--decrypt", "Decrypt an encrypted bridge export before importing").option("--resolve-conflicts", "Safely merge existing local tasks by filling blank fields, unioning tags, and recording unresolved divergences").action(async (file, opts) => {
|
|
30199
30837
|
const globalOpts = program2.opts();
|
|
30200
30838
|
try {
|
|
30201
|
-
const { readFileSync:
|
|
30839
|
+
const { readFileSync: readFileSync10 } = await import("fs");
|
|
30202
30840
|
const { importLocalBridgeBundle: importLocalBridgeBundle2 } = await Promise.resolve().then(() => (init_local_bridge(), exports_local_bridge));
|
|
30203
30841
|
const { decryptBridgeBundle: decryptBridgeBundle2, isEncryptedBridgeBundle: isEncryptedBridgeBundle2 } = await Promise.resolve().then(() => (init_local_encryption(), exports_local_encryption));
|
|
30204
|
-
const parsed = JSON.parse(
|
|
30842
|
+
const parsed = JSON.parse(readFileSync10(resolve13(file), "utf-8"));
|
|
30205
30843
|
const bundle = isEncryptedBridgeBundle2(parsed) ? opts.decrypt ? decryptBridgeBundle2(parsed) : (() => {
|
|
30206
30844
|
throw new Error("Bridge bundle is encrypted. Re-run with --decrypt and the configured key environment variable set.");
|
|
30207
30845
|
})() : parsed;
|
|
@@ -30235,9 +30873,9 @@ Indexed ${result.index.files.length} file(s), ${result.index.total_symbols} symb
|
|
|
30235
30873
|
program2.command("todos-md-import <file>").alias("markdown-import").alias("import-md").description("Dry-run or apply a local todos.md Markdown import").option("--apply", "Apply the import. Defaults to dry-run.").option("--resolve-conflicts", "Safely merge embedded bridge task conflicts while preserving local divergent fields").action(async (file, opts) => {
|
|
30236
30874
|
const globalOpts = program2.opts();
|
|
30237
30875
|
try {
|
|
30238
|
-
const { readFileSync:
|
|
30876
|
+
const { readFileSync: readFileSync10 } = await import("fs");
|
|
30239
30877
|
const { importTodosMarkdown: importTodosMarkdown2 } = await Promise.resolve().then(() => (init_todos_md(), exports_todos_md));
|
|
30240
|
-
const result = importTodosMarkdown2(
|
|
30878
|
+
const result = importTodosMarkdown2(readFileSync10(resolve13(file), "utf-8"), { dryRun: !opts.apply, conflictStrategy: opts.resolveConflicts ? "safe_merge" : "skip" });
|
|
30241
30879
|
const { emitLocalEventHooksQuiet: emitLocalEventHooksQuiet2 } = await Promise.resolve().then(() => (init_event_hooks(), exports_event_hooks));
|
|
30242
30880
|
emitLocalEventHooksQuiet2({ type: "import.finished", payload: { file: resolve13(file), format: "todos.md", dry_run: result.dry_run, ok: result.ok, inserted: result.inserted, skipped: result.skipped, issues: result.issues.length } });
|
|
30243
30881
|
if (globalOpts.json) {
|
|
@@ -30884,7 +31522,7 @@ async function findFreePort(start) {
|
|
|
30884
31522
|
var DEFAULT_PORT = 19427;
|
|
30885
31523
|
|
|
30886
31524
|
// src/lib/db-backup.ts
|
|
30887
|
-
import { existsSync as existsSync15, copyFileSync, mkdirSync as mkdirSync8, readFileSync as
|
|
31525
|
+
import { existsSync as existsSync15, copyFileSync, mkdirSync as mkdirSync8, readFileSync as readFileSync10, renameSync, statSync as statSync7, writeFileSync as writeFileSync7, unlinkSync } from "fs";
|
|
30888
31526
|
import { dirname as dirname7, join as join14, resolve as resolve14 } from "path";
|
|
30889
31527
|
import { Database as Database2 } from "bun:sqlite";
|
|
30890
31528
|
function resolveDbPath(dbPath) {
|
|
@@ -30913,7 +31551,7 @@ function backupDatabase(outputPath, sourcePath) {
|
|
|
30913
31551
|
src.close();
|
|
30914
31552
|
writeFileSync7(outputPath, image);
|
|
30915
31553
|
const method = "file_copy";
|
|
30916
|
-
const bytes =
|
|
31554
|
+
const bytes = statSync7(outputPath).size;
|
|
30917
31555
|
return {
|
|
30918
31556
|
schema_version: DB_BACKUP_SCHEMA,
|
|
30919
31557
|
source_path: source,
|
|
@@ -32250,7 +32888,7 @@ __export(exports_local_extensions, {
|
|
|
32250
32888
|
discoverLocalExtensions: () => discoverLocalExtensions
|
|
32251
32889
|
});
|
|
32252
32890
|
import { createHash as createHash9, createVerify } from "crypto";
|
|
32253
|
-
import { existsSync as existsSync17, readdirSync as readdirSync3, readFileSync as
|
|
32891
|
+
import { existsSync as existsSync17, readdirSync as readdirSync3, readFileSync as readFileSync11, statSync as statSync8 } from "fs";
|
|
32254
32892
|
import { basename as basename6, join as join16, resolve as resolve15 } from "path";
|
|
32255
32893
|
function isObject(value) {
|
|
32256
32894
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
@@ -32332,7 +32970,7 @@ function normalizeManifest(input) {
|
|
|
32332
32970
|
};
|
|
32333
32971
|
}
|
|
32334
32972
|
function parseJson(path) {
|
|
32335
|
-
return JSON.parse(
|
|
32973
|
+
return JSON.parse(readFileSync11(path, "utf8"));
|
|
32336
32974
|
}
|
|
32337
32975
|
function sha2564(bytes) {
|
|
32338
32976
|
return `sha256:${createHash9("sha256").update(bytes).digest("hex")}`;
|
|
@@ -32512,11 +33150,11 @@ function inspectExtensionSource(source2) {
|
|
|
32512
33150
|
const resolved = resolve15(source2);
|
|
32513
33151
|
if (!existsSync17(resolved))
|
|
32514
33152
|
throw new Error(`extension source not found: ${source2}`);
|
|
32515
|
-
const stat =
|
|
33153
|
+
const stat = statSync8(resolved);
|
|
32516
33154
|
const manifestPath = stat.isDirectory() ? [join16(resolved, "todos.extension.json"), join16(resolved, "extension.json")].find(existsSync17) : resolved;
|
|
32517
33155
|
if (!manifestPath)
|
|
32518
33156
|
throw new Error(`extension directory ${source2} is missing todos.extension.json`);
|
|
32519
|
-
const raw =
|
|
33157
|
+
const raw = readFileSync11(manifestPath);
|
|
32520
33158
|
const parsed = parseJson(manifestPath);
|
|
32521
33159
|
const bundle = isObject(parsed) && isObject(parsed["manifest"]);
|
|
32522
33160
|
const manifest = normalizeManifest(bundle ? parsed["manifest"] : parsed);
|
|
@@ -32618,7 +33256,7 @@ function projectExtensionSources(projectPath) {
|
|
|
32618
33256
|
if (entry.startsWith("."))
|
|
32619
33257
|
continue;
|
|
32620
33258
|
const full = join16(extensionDir, entry);
|
|
32621
|
-
if (
|
|
33259
|
+
if (statSync8(full).isDirectory() || entry.endsWith(".json"))
|
|
32622
33260
|
candidates.push(full);
|
|
32623
33261
|
}
|
|
32624
33262
|
}
|
|
@@ -34258,7 +34896,7 @@ var init_postgres_sync = __esm(() => {
|
|
|
34258
34896
|
});
|
|
34259
34897
|
|
|
34260
34898
|
// src/storage/postgres-adapter.ts
|
|
34261
|
-
import { randomUUID as
|
|
34899
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
34262
34900
|
function createPostgresTodosStorageAdapter(options) {
|
|
34263
34901
|
const store = new PostgresJsonRecordStore(options);
|
|
34264
34902
|
const adapter = {
|
|
@@ -34287,6 +34925,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
34287
34925
|
getChangedSince: (since, filters) => getChangedSince(since, filters, store),
|
|
34288
34926
|
lock: (id, agentId) => lockTask2(id, agentId, store),
|
|
34289
34927
|
unlock: (id, agentId) => unlockTask2(id, agentId, store),
|
|
34928
|
+
handoffStaleLock: (input, context) => store.handoffStaleLock(input, context),
|
|
34290
34929
|
getByFingerprint: (fingerprint) => store.getTaskByFingerprint(fingerprint)
|
|
34291
34930
|
},
|
|
34292
34931
|
dependencies: {
|
|
@@ -34323,6 +34962,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
34323
34962
|
get: (id) => store.get("plans", id),
|
|
34324
34963
|
list: async (projectId) => (await store.list("plans")).filter((plan) => projectId === undefined || plan.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
|
|
34325
34964
|
update: (id, input) => updatePlan2(id, input, store),
|
|
34965
|
+
completeAtRevision: (id, expectedUpdatedAt, context) => store.completePlanAtRevision(id, expectedUpdatedAt, context),
|
|
34326
34966
|
delete: (id, context) => store.deletePlan(id, context)
|
|
34327
34967
|
},
|
|
34328
34968
|
planProjectLinks: {
|
|
@@ -34432,6 +35072,91 @@ class PostgresJsonRecordStore {
|
|
|
34432
35072
|
LIMIT 1`, [this.service, type, id]);
|
|
34433
35073
|
return result.rows[0] ? payloadRecord2(result.rows[0].payload) : null;
|
|
34434
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
|
+
}
|
|
34435
35160
|
async list(type) {
|
|
34436
35161
|
return (await this.listRecords(type)).map((record) => record.payload);
|
|
34437
35162
|
}
|
|
@@ -34701,6 +35426,28 @@ class PostgresJsonRecordStore {
|
|
|
34701
35426
|
}
|
|
34702
35427
|
return value;
|
|
34703
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
|
+
}
|
|
34704
35451
|
async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}) {
|
|
34705
35452
|
const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
|
|
34706
35453
|
if (planIds.length === 0)
|
|
@@ -34810,6 +35557,54 @@ class PostgresJsonRecordStore {
|
|
|
34810
35557
|
throw new PlanNotFoundError(value.id);
|
|
34811
35558
|
return payloadRecord2(row.payload);
|
|
34812
35559
|
}
|
|
35560
|
+
async completePlanAtRevision(id, expectedUpdatedAt, context = {}) {
|
|
35561
|
+
await this.ensureSchema();
|
|
35562
|
+
const result = await this.options.client.query(`/* todos:complete-plan-revision-cas */ WITH next_clock AS (
|
|
35563
|
+
SELECT date_trunc(
|
|
35564
|
+
'milliseconds',
|
|
35565
|
+
GREATEST(clock_timestamp(), ($3::text)::timestamptz + interval '2 milliseconds')
|
|
35566
|
+
) AS completed_at
|
|
35567
|
+
), stored AS (
|
|
35568
|
+
UPDATE ${this.tableName} AS record SET
|
|
35569
|
+
payload = record.payload || jsonb_build_object(
|
|
35570
|
+
'status', 'completed',
|
|
35571
|
+
'updated_at', to_char(
|
|
35572
|
+
next_clock.completed_at AT TIME ZONE 'UTC',
|
|
35573
|
+
'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
|
|
35574
|
+
)
|
|
35575
|
+
),
|
|
35576
|
+
updated_at = next_clock.completed_at,
|
|
35577
|
+
deleted_at = NULL,
|
|
35578
|
+
source_machine_id = COALESCE($4, record.source_machine_id),
|
|
35579
|
+
version = COALESCE(record.version, 0) + 1
|
|
35580
|
+
FROM next_clock
|
|
35581
|
+
WHERE record.service = $1
|
|
35582
|
+
AND record.object_type = 'plans'
|
|
35583
|
+
AND record.object_id = $2
|
|
35584
|
+
AND record.deleted_at IS NULL
|
|
35585
|
+
AND record.payload->>'updated_at' = $3::text
|
|
35586
|
+
AND record.payload->>'status' IS DISTINCT FROM 'completed'
|
|
35587
|
+
RETURNING record.payload
|
|
35588
|
+
)
|
|
35589
|
+
SELECT payload FROM stored`, [
|
|
35590
|
+
this.service,
|
|
35591
|
+
id,
|
|
35592
|
+
expectedUpdatedAt,
|
|
35593
|
+
context.requestId ?? this.sourceMachineId ?? null
|
|
35594
|
+
]);
|
|
35595
|
+
const payload = result.rows[0]?.payload;
|
|
35596
|
+
if (payload)
|
|
35597
|
+
return { plan: payloadRecord2(payload), applied: true };
|
|
35598
|
+
const current = await this.get("plans", id);
|
|
35599
|
+
if (!current)
|
|
35600
|
+
throw new PlanNotFoundError(id);
|
|
35601
|
+
if (current.updated_at !== expectedUpdatedAt) {
|
|
35602
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
|
|
35603
|
+
}
|
|
35604
|
+
if (current.status === "completed")
|
|
35605
|
+
return { plan: current, applied: false };
|
|
35606
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
|
|
35607
|
+
}
|
|
34813
35608
|
async createTemplateWithTasks(template, tasks, context = {}) {
|
|
34814
35609
|
await this.ensureSchema();
|
|
34815
35610
|
const records = [
|
|
@@ -35479,7 +36274,7 @@ async function createTask2(input, store, context) {
|
|
|
35479
36274
|
const effectiveProjectId = linkedPlan?.project_id ?? requestedProjectId;
|
|
35480
36275
|
const shortId = effectiveProjectId ? await nextTaskShortId2(effectiveProjectId, store, context) : null;
|
|
35481
36276
|
const task = {
|
|
35482
|
-
id:
|
|
36277
|
+
id: randomUUID4(),
|
|
35483
36278
|
short_id: shortId,
|
|
35484
36279
|
project_id: effectiveProjectId,
|
|
35485
36280
|
parent_id: input.parent_id ?? null,
|
|
@@ -35725,7 +36520,7 @@ async function addVerification(input, store, context) {
|
|
|
35725
36520
|
throw new Error(`Task not found: ${input.task_id}`);
|
|
35726
36521
|
const timestamp2 = new Date().toISOString();
|
|
35727
36522
|
const verification = {
|
|
35728
|
-
id:
|
|
36523
|
+
id: randomUUID4(),
|
|
35729
36524
|
task_id: input.task_id,
|
|
35730
36525
|
command: input.command,
|
|
35731
36526
|
status: input.status ?? "unknown",
|
|
@@ -35746,7 +36541,7 @@ async function addCommit(input, store, context) {
|
|
|
35746
36541
|
throw new Error(`Task not found: ${input.task_id}`);
|
|
35747
36542
|
const timestamp2 = new Date().toISOString();
|
|
35748
36543
|
const commit = {
|
|
35749
|
-
id:
|
|
36544
|
+
id: randomUUID4(),
|
|
35750
36545
|
task_id: input.task_id,
|
|
35751
36546
|
sha: input.sha,
|
|
35752
36547
|
message: input.message ?? null,
|
|
@@ -35769,7 +36564,7 @@ async function addGitRef(input, store, context) {
|
|
|
35769
36564
|
throw new Error(`Task not found: ${input.task_id}`);
|
|
35770
36565
|
const timestamp2 = new Date().toISOString();
|
|
35771
36566
|
const gitRef = {
|
|
35772
|
-
id:
|
|
36567
|
+
id: randomUUID4(),
|
|
35773
36568
|
task_id: input.task_id,
|
|
35774
36569
|
ref_type: input.ref_type,
|
|
35775
36570
|
name: input.name,
|
|
@@ -35839,7 +36634,7 @@ async function createProject2(input, store, context) {
|
|
|
35839
36634
|
if (!derivedSlug || !taskListId)
|
|
35840
36635
|
throw new Error("Project name and task-list slug must be non-empty");
|
|
35841
36636
|
const project = {
|
|
35842
|
-
id:
|
|
36637
|
+
id: randomUUID4(),
|
|
35843
36638
|
name: input.name,
|
|
35844
36639
|
path: input.path,
|
|
35845
36640
|
description: input.description ?? null,
|
|
@@ -35871,7 +36666,7 @@ async function createPlan2(input, store, context) {
|
|
|
35871
36666
|
store
|
|
35872
36667
|
});
|
|
35873
36668
|
return store.upsert("plans", {
|
|
35874
|
-
id:
|
|
36669
|
+
id: randomUUID4(),
|
|
35875
36670
|
slug,
|
|
35876
36671
|
project_id: projectId,
|
|
35877
36672
|
task_list_id: input.task_list_id ?? context?.taskListId ?? null,
|
|
@@ -35923,7 +36718,7 @@ async function registerAgent2(input, store, context) {
|
|
|
35923
36718
|
}
|
|
35924
36719
|
const timestamp2 = new Date().toISOString();
|
|
35925
36720
|
const agent = {
|
|
35926
|
-
id: existing?.id ??
|
|
36721
|
+
id: existing?.id ?? randomUUID4().slice(0, 8),
|
|
35927
36722
|
name: canonicalName,
|
|
35928
36723
|
description: input.description ?? existing?.description ?? null,
|
|
35929
36724
|
role: input.role ?? existing?.role ?? null,
|
|
@@ -35996,7 +36791,7 @@ async function createTaskList2(input, store, context) {
|
|
|
35996
36791
|
if (!slug)
|
|
35997
36792
|
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
35998
36793
|
return store.upsert("task_lists", {
|
|
35999
|
-
id:
|
|
36794
|
+
id: randomUUID4(),
|
|
36000
36795
|
project_id: input.project_id ?? context?.projectId ?? null,
|
|
36001
36796
|
slug,
|
|
36002
36797
|
name: input.name,
|
|
@@ -36031,7 +36826,7 @@ async function updateTaskList2(id, input, store) {
|
|
|
36031
36826
|
async function createTemplate2(input, store, context) {
|
|
36032
36827
|
const timestamp2 = new Date().toISOString();
|
|
36033
36828
|
const template = {
|
|
36034
|
-
id:
|
|
36829
|
+
id: randomUUID4(),
|
|
36035
36830
|
name: input.name,
|
|
36036
36831
|
title_pattern: input.title_pattern,
|
|
36037
36832
|
description: input.description ?? null,
|
|
@@ -36052,7 +36847,7 @@ async function createTemplate2(input, store, context) {
|
|
|
36052
36847
|
}
|
|
36053
36848
|
function buildTemplateTasks(templateId, inputs, timestamp2) {
|
|
36054
36849
|
return inputs.map((input, position) => ({
|
|
36055
|
-
id:
|
|
36850
|
+
id: randomUUID4(),
|
|
36056
36851
|
template_id: templateId,
|
|
36057
36852
|
position,
|
|
36058
36853
|
title_pattern: input.title_pattern,
|
|
@@ -36085,7 +36880,7 @@ async function updateTemplate2(id, input, store) {
|
|
|
36085
36880
|
}
|
|
36086
36881
|
async function logTaskChange2(taskId, action, field, oldValue, newValue, agentId, store, context) {
|
|
36087
36882
|
const entry2 = {
|
|
36088
|
-
id:
|
|
36883
|
+
id: randomUUID4(),
|
|
36089
36884
|
task_id: taskId,
|
|
36090
36885
|
action,
|
|
36091
36886
|
field: field ?? null,
|
|
@@ -36099,7 +36894,7 @@ async function logTaskChange2(taskId, action, field, oldValue, newValue, agentId
|
|
|
36099
36894
|
}
|
|
36100
36895
|
async function addComment2(input, store, context) {
|
|
36101
36896
|
const comment = {
|
|
36102
|
-
id:
|
|
36897
|
+
id: randomUUID4(),
|
|
36103
36898
|
task_id: input.task_id,
|
|
36104
36899
|
agent_id: input.agent_id ?? context?.agentId ?? null,
|
|
36105
36900
|
session_id: input.session_id ?? context?.sessionId ?? null,
|
|
@@ -36141,6 +36936,11 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
36141
36936
|
result.errors.push(...validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists));
|
|
36142
36937
|
if (result.errors.length > 0)
|
|
36143
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;
|
|
36144
36944
|
const entries = [
|
|
36145
36945
|
...snapshot.tasks.map((row) => ["tasks", row]),
|
|
36146
36946
|
...snapshot.projects.map((row) => ["projects", row]),
|
|
@@ -36149,9 +36949,20 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
36149
36949
|
...snapshot.agents.map((row) => ["agents", row]),
|
|
36150
36950
|
...snapshot.taskLists.map((row) => ["task_lists", row]),
|
|
36151
36951
|
...snapshot.templates.map((row) => ["templates", row]),
|
|
36152
|
-
...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row])
|
|
36153
|
-
...snapshot.auditHistory.map((row) => ["audit_history", row])
|
|
36952
|
+
...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row])
|
|
36154
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
|
+
}
|
|
36155
36966
|
for (const [type, row] of entries) {
|
|
36156
36967
|
try {
|
|
36157
36968
|
const existing = await store.get(type, row.id);
|
|
@@ -36185,6 +36996,32 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
36185
36996
|
}
|
|
36186
36997
|
return result;
|
|
36187
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
|
+
}
|
|
36188
37025
|
async function requireRecord(type, id, store) {
|
|
36189
37026
|
const record = await store.get(type, id);
|
|
36190
37027
|
if (!record)
|
|
@@ -36291,9 +37128,11 @@ var init_postgres_adapter = __esm(() => {
|
|
|
36291
37128
|
init_types();
|
|
36292
37129
|
init_creator_identity();
|
|
36293
37130
|
init_plan_project_link_contract();
|
|
37131
|
+
init_stale_lock_handoff();
|
|
36294
37132
|
init_postgres_sync();
|
|
36295
37133
|
init_integrity();
|
|
36296
37134
|
init_redaction();
|
|
37135
|
+
init_audit_history_import();
|
|
36297
37136
|
TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
|
|
36298
37137
|
});
|
|
36299
37138
|
|
|
@@ -44645,7 +45484,7 @@ var exports_doctor = {};
|
|
|
44645
45484
|
__export(exports_doctor, {
|
|
44646
45485
|
runTodosDoctor: () => runTodosDoctor
|
|
44647
45486
|
});
|
|
44648
|
-
import { chmodSync as chmodSync2, copyFileSync as copyFileSync2, existsSync as existsSync18, mkdirSync as mkdirSync9, statSync as
|
|
45487
|
+
import { chmodSync as chmodSync2, copyFileSync as copyFileSync2, existsSync as existsSync18, mkdirSync as mkdirSync9, statSync as statSync9 } from "fs";
|
|
44649
45488
|
import { basename as basename7, dirname as dirname8, join as join17 } from "path";
|
|
44650
45489
|
function tableExists3(db, table) {
|
|
44651
45490
|
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
|
@@ -44792,7 +45631,7 @@ function databasePermissionsAreUnsafe(dbPath) {
|
|
|
44792
45631
|
if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
|
|
44793
45632
|
return false;
|
|
44794
45633
|
try {
|
|
44795
|
-
return (
|
|
45634
|
+
return (statSync9(dbPath).mode & 63) !== 0;
|
|
44796
45635
|
} catch {
|
|
44797
45636
|
return false;
|
|
44798
45637
|
}
|
|
@@ -46297,6 +47136,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
46297
47136
|
ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
|
|
46298
47137
|
ProjectTaskListRollbackResult: projectTaskListRollbackResultSchema,
|
|
46299
47138
|
TaskComment: taskCommentSchema,
|
|
47139
|
+
StaleLockHandoffInput: staleLockHandoffInputSchema,
|
|
47140
|
+
StaleLockHandoffReceipt: staleLockHandoffReceiptSchema,
|
|
46300
47141
|
TaskGitRef: taskGitRefSchema,
|
|
46301
47142
|
Plan: planSchema,
|
|
46302
47143
|
PlanProjectLinkReceipt: planProjectLinkReceiptSchema,
|
|
@@ -47504,6 +48345,51 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
47504
48345
|
}
|
|
47505
48346
|
}
|
|
47506
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
|
+
},
|
|
47507
48393
|
"/v1/tasks/{id}/refs": {
|
|
47508
48394
|
get: {
|
|
47509
48395
|
operationId: "listTaskGitRefs",
|
|
@@ -48010,8 +48896,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
48010
48896
|
"/v1/import": {
|
|
48011
48897
|
post: {
|
|
48012
48898
|
operationId: "importSnapshot",
|
|
48013
|
-
summary: "Bulk-ingest a
|
|
48014
|
-
description: "Upserts every record
|
|
48899
|
+
summary: "Bulk-ingest a snapshot or atomically complete one observed plan",
|
|
48900
|
+
description: "Upserts every snapshot record by primary key, or accepts exactly one planCompletions operation that changes only plan status under an expected_updated_at CAS. Snapshot records and planCompletions are mutually exclusive. Requires the todos:write scope.",
|
|
48015
48901
|
requestBody: {
|
|
48016
48902
|
required: true,
|
|
48017
48903
|
content: {
|
|
@@ -48030,7 +48916,22 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
48030
48916
|
templates: { type: "array", items: { type: "object" } },
|
|
48031
48917
|
templateTasks: { type: "array", items: { $ref: "#/components/schemas/TemplateTask" } },
|
|
48032
48918
|
auditHistory: { type: "array", items: { type: "object" } },
|
|
48033
|
-
tombstones: { type: "array", items: { type: "object" } }
|
|
48919
|
+
tombstones: { type: "array", items: { type: "object" } },
|
|
48920
|
+
planCompletions: {
|
|
48921
|
+
type: "array",
|
|
48922
|
+
minItems: 1,
|
|
48923
|
+
maxItems: 1,
|
|
48924
|
+
items: {
|
|
48925
|
+
type: "object",
|
|
48926
|
+
additionalProperties: false,
|
|
48927
|
+
required: ["id", "expected_updated_at", "status"],
|
|
48928
|
+
properties: {
|
|
48929
|
+
id: { type: "string" },
|
|
48930
|
+
expected_updated_at: { type: "string", format: "date-time" },
|
|
48931
|
+
status: { type: "string", enum: ["completed"] }
|
|
48932
|
+
}
|
|
48933
|
+
}
|
|
48934
|
+
}
|
|
48034
48935
|
}
|
|
48035
48936
|
}
|
|
48036
48937
|
}
|
|
@@ -48053,6 +48954,26 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
48053
48954
|
skipped: { type: "number" },
|
|
48054
48955
|
errors: { type: "array", items: { type: "string" } }
|
|
48055
48956
|
}
|
|
48957
|
+
},
|
|
48958
|
+
planCompletions: {
|
|
48959
|
+
type: "array",
|
|
48960
|
+
items: {
|
|
48961
|
+
type: "object",
|
|
48962
|
+
required: [
|
|
48963
|
+
"id",
|
|
48964
|
+
"status",
|
|
48965
|
+
"expected_updated_at",
|
|
48966
|
+
"result_updated_at",
|
|
48967
|
+
"applied"
|
|
48968
|
+
],
|
|
48969
|
+
properties: {
|
|
48970
|
+
id: { type: "string" },
|
|
48971
|
+
status: { type: "string", enum: ["completed"] },
|
|
48972
|
+
expected_updated_at: { type: "string", format: "date-time" },
|
|
48973
|
+
result_updated_at: { type: "string", format: "date-time" },
|
|
48974
|
+
applied: { type: "boolean" }
|
|
48975
|
+
}
|
|
48976
|
+
}
|
|
48056
48977
|
}
|
|
48057
48978
|
}
|
|
48058
48979
|
}
|
|
@@ -48065,7 +48986,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
48065
48986
|
}
|
|
48066
48987
|
};
|
|
48067
48988
|
}
|
|
48068
|
-
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;
|
|
48069
48990
|
var init_openapi = __esm(() => {
|
|
48070
48991
|
init_package_version();
|
|
48071
48992
|
init_types();
|
|
@@ -48084,6 +49005,8 @@ var init_openapi = __esm(() => {
|
|
|
48084
49005
|
reason: { type: "string", nullable: true },
|
|
48085
49006
|
tags: { type: "array", items: { type: "string" } },
|
|
48086
49007
|
version: { type: "number" },
|
|
49008
|
+
locked_by: { type: "string", nullable: true },
|
|
49009
|
+
locked_at: { type: "string", format: "date-time", nullable: true },
|
|
48087
49010
|
created_at: { type: "string" },
|
|
48088
49011
|
updated_at: { type: "string" }
|
|
48089
49012
|
}
|
|
@@ -48248,6 +49171,69 @@ var init_openapi = __esm(() => {
|
|
|
48248
49171
|
created_at: { type: "string", format: "date-time" }
|
|
48249
49172
|
}
|
|
48250
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
|
+
};
|
|
48251
49237
|
taskGitRefSchema = {
|
|
48252
49238
|
type: "object",
|
|
48253
49239
|
additionalProperties: false,
|
|
@@ -48902,6 +49888,65 @@ function normalizeImportSnapshot(raw) {
|
|
|
48902
49888
|
function countSnapshotRecords(s) {
|
|
48903
49889
|
return s.tasks.length + s.projects.length + (s.projectMachinePaths?.length ?? 0) + s.plans.length + s.agents.length + s.taskLists.length + s.templates.length + s.templateTasks.length + s.auditHistory.length + (s.tombstones?.length ?? 0);
|
|
48904
49890
|
}
|
|
49891
|
+
function validatePlanCompletionImports(raw) {
|
|
49892
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
49893
|
+
return { present: false, operations: [] };
|
|
49894
|
+
}
|
|
49895
|
+
const body2 = raw;
|
|
49896
|
+
if (!Object.prototype.hasOwnProperty.call(body2, "planCompletions")) {
|
|
49897
|
+
return { present: false, operations: [] };
|
|
49898
|
+
}
|
|
49899
|
+
if (!Array.isArray(body2["planCompletions"]) || body2["planCompletions"].length !== 1) {
|
|
49900
|
+
return {
|
|
49901
|
+
present: true,
|
|
49902
|
+
operations: [],
|
|
49903
|
+
error: "planCompletions must contain exactly one completion operation"
|
|
49904
|
+
};
|
|
49905
|
+
}
|
|
49906
|
+
const operation = body2["planCompletions"][0];
|
|
49907
|
+
if (!operation || typeof operation !== "object" || Array.isArray(operation)) {
|
|
49908
|
+
return { present: true, operations: [], error: "plan completion must be an object" };
|
|
49909
|
+
}
|
|
49910
|
+
const record = operation;
|
|
49911
|
+
const allowed = new Set(["id", "expected_updated_at", "status"]);
|
|
49912
|
+
const unknown = Object.keys(record).find((key2) => !allowed.has(key2));
|
|
49913
|
+
if (unknown) {
|
|
49914
|
+
return { present: true, operations: [], error: `unknown plan completion field: ${unknown}` };
|
|
49915
|
+
}
|
|
49916
|
+
if (typeof record["id"] !== "string" || !record["id"].trim()) {
|
|
49917
|
+
return { present: true, operations: [], error: "plan completion id must be a non-empty string" };
|
|
49918
|
+
}
|
|
49919
|
+
if (record["status"] !== "completed") {
|
|
49920
|
+
return { present: true, operations: [], error: "plan completion status must be completed" };
|
|
49921
|
+
}
|
|
49922
|
+
const expectedUpdatedAt = typeof record["expected_updated_at"] === "string" ? record["expected_updated_at"] : "";
|
|
49923
|
+
const timestampMatch = RFC3339_DATE_TIME.exec(expectedUpdatedAt);
|
|
49924
|
+
const parsedTimestamp = Date.parse(expectedUpdatedAt);
|
|
49925
|
+
if (!timestampMatch || Number.isNaN(parsedTimestamp)) {
|
|
49926
|
+
return {
|
|
49927
|
+
present: true,
|
|
49928
|
+
operations: [],
|
|
49929
|
+
error: "plan completion expected_updated_at must be an RFC 3339 date-time with an explicit offset"
|
|
49930
|
+
};
|
|
49931
|
+
}
|
|
49932
|
+
const [, year, month, day] = timestampMatch;
|
|
49933
|
+
const calendarProbe = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
|
|
49934
|
+
if (calendarProbe.getUTCFullYear() !== Number(year) || calendarProbe.getUTCMonth() !== Number(month) - 1 || calendarProbe.getUTCDate() !== Number(day)) {
|
|
49935
|
+
return {
|
|
49936
|
+
present: true,
|
|
49937
|
+
operations: [],
|
|
49938
|
+
error: "plan completion expected_updated_at names a date that does not exist"
|
|
49939
|
+
};
|
|
49940
|
+
}
|
|
49941
|
+
return {
|
|
49942
|
+
present: true,
|
|
49943
|
+
operations: [{
|
|
49944
|
+
id: record["id"],
|
|
49945
|
+
expected_updated_at: expectedUpdatedAt,
|
|
49946
|
+
status: "completed"
|
|
49947
|
+
}]
|
|
49948
|
+
};
|
|
49949
|
+
}
|
|
48905
49950
|
async function handleV1Request(req, url, dependencies = {}) {
|
|
48906
49951
|
const path = url.pathname;
|
|
48907
49952
|
if (path !== "/v1" && !path.startsWith("/v1/"))
|
|
@@ -49083,6 +50128,45 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
49083
50128
|
return error(405, `method ${method} not allowed on /v1/tasks`);
|
|
49084
50129
|
}
|
|
49085
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
|
+
}
|
|
49086
50170
|
if (action === "comments") {
|
|
49087
50171
|
if (method === "GET") {
|
|
49088
50172
|
if (!await store.tasks.get(id))
|
|
@@ -49838,10 +50922,57 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
49838
50922
|
return error(400, "invalid JSON body");
|
|
49839
50923
|
const snapshot = normalizeImportSnapshot(raw);
|
|
49840
50924
|
const received = countSnapshotRecords(snapshot);
|
|
50925
|
+
const completionImports = validatePlanCompletionImports(raw);
|
|
50926
|
+
if (completionImports.present) {
|
|
50927
|
+
if (completionImports.error)
|
|
50928
|
+
return error(400, completionImports.error);
|
|
50929
|
+
if (received !== 0) {
|
|
50930
|
+
return error(400, "planCompletions cannot be combined with snapshot record arrays");
|
|
50931
|
+
}
|
|
50932
|
+
if (typeof store.plans.completeAtRevision !== "function") {
|
|
50933
|
+
return error(501, "atomic plan completion is not supported by this storage backend");
|
|
50934
|
+
}
|
|
50935
|
+
const operation = completionImports.operations[0];
|
|
50936
|
+
const completed = await store.plans.completeAtRevision(operation.id, operation.expected_updated_at, contextFromPrincipal(principal));
|
|
50937
|
+
return json5({
|
|
50938
|
+
result: {
|
|
50939
|
+
inserted: 0,
|
|
50940
|
+
updated: completed.applied ? 1 : 0,
|
|
50941
|
+
deleted: 0,
|
|
50942
|
+
skipped: completed.applied ? 0 : 1,
|
|
50943
|
+
errors: []
|
|
50944
|
+
},
|
|
50945
|
+
received: 1,
|
|
50946
|
+
planCompletions: [{
|
|
50947
|
+
id: operation.id,
|
|
50948
|
+
status: "completed",
|
|
50949
|
+
expected_updated_at: operation.expected_updated_at,
|
|
50950
|
+
result_updated_at: completed.plan.updated_at,
|
|
50951
|
+
applied: completed.applied
|
|
50952
|
+
}]
|
|
50953
|
+
});
|
|
50954
|
+
}
|
|
49841
50955
|
if (received === 0) {
|
|
49842
50956
|
return error(400, "empty snapshot: provide at least one record array (tasks/projects/plans/...)");
|
|
49843
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
|
+
}
|
|
49844
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
|
+
}
|
|
49845
50976
|
return json5({ result, received });
|
|
49846
50977
|
}
|
|
49847
50978
|
return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
|
|
@@ -49864,6 +50995,26 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
49864
50995
|
if (e instanceof TaskNotFoundError) {
|
|
49865
50996
|
return error(404, e.message, { code: TaskNotFoundError.code });
|
|
49866
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
|
+
}
|
|
51006
|
+
if (e instanceof PlanNotFoundError) {
|
|
51007
|
+
return error(404, e.message, { code: PlanNotFoundError.code });
|
|
51008
|
+
}
|
|
51009
|
+
if (e instanceof PlanRevisionConflictError) {
|
|
51010
|
+
return error(409, e.message, {
|
|
51011
|
+
code: PlanRevisionConflictError.code,
|
|
51012
|
+
conflict: true,
|
|
51013
|
+
plan_id: e.planId,
|
|
51014
|
+
expected_updated_at: e.expectedUpdatedAt,
|
|
51015
|
+
current_updated_at: e.currentUpdatedAt
|
|
51016
|
+
});
|
|
51017
|
+
}
|
|
49867
51018
|
if (e instanceof LockError)
|
|
49868
51019
|
return error(409, e.message, { code: LockError.code });
|
|
49869
51020
|
if (e instanceof TaskNotStartableError) {
|
|
@@ -49886,6 +51037,8 @@ var init_v1 = __esm(() => {
|
|
|
49886
51037
|
init_redaction();
|
|
49887
51038
|
init_project_task_list_ensure();
|
|
49888
51039
|
init_plan_project_link();
|
|
51040
|
+
init_stale_lock_handoff();
|
|
51041
|
+
init_audit_history_import();
|
|
49889
51042
|
JSON_HEADERS4 = { "Content-Type": "application/json" };
|
|
49890
51043
|
RFC3339_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/;
|
|
49891
51044
|
});
|
|
@@ -51015,7 +52168,7 @@ var exports_mention_resolver = {};
|
|
|
51015
52168
|
__export(exports_mention_resolver, {
|
|
51016
52169
|
resolveMentions: () => resolveMentions
|
|
51017
52170
|
});
|
|
51018
|
-
import { existsSync as existsSync19, readdirSync as readdirSync4, readFileSync as
|
|
52171
|
+
import { existsSync as existsSync19, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync10 } from "fs";
|
|
51019
52172
|
import { basename as basename8, isAbsolute, join as join19, relative as relative5, resolve as resolve18, sep as sep5 } from "path";
|
|
51020
52173
|
function blankResolution(parsed) {
|
|
51021
52174
|
return {
|
|
@@ -51118,13 +52271,13 @@ function resolveFile(parsed, workspace) {
|
|
|
51118
52271
|
resolution.warnings.push("file does not exist in the local workspace");
|
|
51119
52272
|
return resolution;
|
|
51120
52273
|
}
|
|
51121
|
-
const stats =
|
|
52274
|
+
const stats = statSync10(absolutePath);
|
|
51122
52275
|
if (!stats.isFile()) {
|
|
51123
52276
|
resolution.warnings.push("path exists but is not a file");
|
|
51124
52277
|
return resolution;
|
|
51125
52278
|
}
|
|
51126
52279
|
if (parsed.line !== undefined) {
|
|
51127
|
-
const lineCount =
|
|
52280
|
+
const lineCount = readFileSync12(absolutePath, "utf-8").split(/\r?\n/).length;
|
|
51128
52281
|
if (parsed.line < 1 || parsed.line > lineCount) {
|
|
51129
52282
|
resolution.warnings.push(`line ${parsed.line} is outside the file range 1-${lineCount}`);
|
|
51130
52283
|
return resolution;
|
|
@@ -51156,7 +52309,7 @@ function walkSourceFiles(root, current = root, files = []) {
|
|
|
51156
52309
|
if (!entry2.isFile())
|
|
51157
52310
|
continue;
|
|
51158
52311
|
const extension = `.${basename8(entry2.name).split(".").pop() || ""}`;
|
|
51159
|
-
if (SOURCE_EXTENSIONS.has(extension) &&
|
|
52312
|
+
if (SOURCE_EXTENSIONS.has(extension) && statSync10(absolutePath).size <= 512 * 1024) {
|
|
51160
52313
|
files.push(absolutePath);
|
|
51161
52314
|
}
|
|
51162
52315
|
}
|
|
@@ -51177,7 +52330,7 @@ function resolveSymbol(parsed, workspace, maxMatches) {
|
|
|
51177
52330
|
const pattern = symbolPattern(name);
|
|
51178
52331
|
const matches = [];
|
|
51179
52332
|
for (const file of walkSourceFiles(workspace)) {
|
|
51180
|
-
const lines =
|
|
52333
|
+
const lines = readFileSync12(file, "utf-8").split(/\r?\n/);
|
|
51181
52334
|
for (let index = 0;index < lines.length; index += 1) {
|
|
51182
52335
|
const line = lines[index];
|
|
51183
52336
|
const found = pattern.exec(line);
|
|
@@ -53919,7 +55072,7 @@ __export(exports_release_compatibility, {
|
|
|
53919
55072
|
createReleaseCompatibilityReport: () => createReleaseCompatibilityReport,
|
|
53920
55073
|
LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION: () => LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION
|
|
53921
55074
|
});
|
|
53922
|
-
import { readFileSync as
|
|
55075
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
53923
55076
|
import { join as join20, resolve as resolve19 } from "path";
|
|
53924
55077
|
import { Database as Database3 } from "bun:sqlite";
|
|
53925
55078
|
function pass(id, message, details) {
|
|
@@ -53932,7 +55085,7 @@ function warn(id, message, details) {
|
|
|
53932
55085
|
return { id, status: "warning", message, details };
|
|
53933
55086
|
}
|
|
53934
55087
|
function readPackageJson2(root) {
|
|
53935
|
-
return JSON.parse(
|
|
55088
|
+
return JSON.parse(readFileSync13(join20(root, "package.json"), "utf8"));
|
|
53936
55089
|
}
|
|
53937
55090
|
function sortedKeys(value) {
|
|
53938
55091
|
return Object.keys(value ?? {}).sort((left, right) => left.localeCompare(right));
|
|
@@ -59902,7 +61055,7 @@ __export(exports_verification_providers, {
|
|
|
59902
61055
|
getVerificationRecord: () => getVerificationRecord,
|
|
59903
61056
|
discoverVerificationProviderCapabilities: () => discoverVerificationProviderCapabilities
|
|
59904
61057
|
});
|
|
59905
|
-
import { existsSync as existsSync20, readFileSync as
|
|
61058
|
+
import { existsSync as existsSync20, readFileSync as readFileSync14 } from "fs";
|
|
59906
61059
|
function normalizeName6(name) {
|
|
59907
61060
|
const normalized = name.trim().toLowerCase();
|
|
59908
61061
|
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(normalized)) {
|
|
@@ -60054,7 +61207,7 @@ Timed out after ${provider.timeout_ms}ms`);
|
|
|
60054
61207
|
};
|
|
60055
61208
|
}
|
|
60056
61209
|
function runCiLogProvider(input) {
|
|
60057
|
-
const text = input.log_text ?? (input.log_path && existsSync20(input.log_path) ?
|
|
61210
|
+
const text = input.log_text ?? (input.log_path && existsSync20(input.log_path) ? readFileSync14(input.log_path, "utf-8") : "");
|
|
60058
61211
|
return {
|
|
60059
61212
|
status: classifyLog(text),
|
|
60060
61213
|
attempts: 1,
|
|
@@ -62357,7 +63510,7 @@ __export(exports_local_backups, {
|
|
|
62357
63510
|
LOCAL_BACKUP_CHECKSUM_ALGORITHM: () => LOCAL_BACKUP_CHECKSUM_ALGORITHM
|
|
62358
63511
|
});
|
|
62359
63512
|
import { createHash as createHash14 } from "crypto";
|
|
62360
|
-
import { readFileSync as
|
|
63513
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "fs";
|
|
62361
63514
|
import { dirname as dirname9, resolve as resolve20 } from "path";
|
|
62362
63515
|
import { mkdirSync as mkdirSync10 } from "fs";
|
|
62363
63516
|
function stableJson2(value) {
|
|
@@ -62467,7 +63620,7 @@ function writeLocalBackupFile(backup, outputPath) {
|
|
|
62467
63620
|
return path;
|
|
62468
63621
|
}
|
|
62469
63622
|
function readLocalBackupFile(path) {
|
|
62470
|
-
return JSON.parse(
|
|
63623
|
+
return JSON.parse(readFileSync15(resolve20(path), "utf-8"));
|
|
62471
63624
|
}
|
|
62472
63625
|
function verifyLocalBackup(value, options = {}, db) {
|
|
62473
63626
|
const verifiedAt = options.verified_at ?? now();
|
|
@@ -63647,7 +64800,7 @@ __export(exports_agent_replay_simulator, {
|
|
|
63647
64800
|
renderAgentReplaySimulationMarkdown: () => renderAgentReplaySimulationMarkdown
|
|
63648
64801
|
});
|
|
63649
64802
|
import { createHash as createHash16 } from "crypto";
|
|
63650
|
-
import { readFileSync as
|
|
64803
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
63651
64804
|
function isObject2(value) {
|
|
63652
64805
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
63653
64806
|
}
|
|
@@ -63880,7 +65033,7 @@ function simulateAgentReplay(input, options = {}) {
|
|
|
63880
65033
|
};
|
|
63881
65034
|
}
|
|
63882
65035
|
function simulateAgentReplayFile(path, options = {}) {
|
|
63883
|
-
const parsed = JSON.parse(
|
|
65036
|
+
const parsed = JSON.parse(readFileSync16(path, "utf8"));
|
|
63884
65037
|
return simulateAgentReplay(parsed, options);
|
|
63885
65038
|
}
|
|
63886
65039
|
function renderAgentReplaySimulationMarkdown(simulation) {
|
|
@@ -69219,7 +70372,7 @@ __export(exports_environment_snapshots, {
|
|
|
69219
70372
|
captureEnvironmentSnapshot: () => captureEnvironmentSnapshot
|
|
69220
70373
|
});
|
|
69221
70374
|
import { createHash as createHash18 } from "crypto";
|
|
69222
|
-
import { existsSync as existsSync21, readFileSync as
|
|
70375
|
+
import { existsSync as existsSync21, readFileSync as readFileSync17, statSync as statSync11 } from "fs";
|
|
69223
70376
|
import { hostname as hostname2, platform, arch } from "os";
|
|
69224
70377
|
import { dirname as dirname10, join as join22, resolve as resolve21 } from "path";
|
|
69225
70378
|
import { tmpdir as tmpdir4 } from "os";
|
|
@@ -69230,10 +70383,10 @@ function fileRecord(root, relativePath) {
|
|
|
69230
70383
|
const path = join22(root, relativePath);
|
|
69231
70384
|
if (!existsSync21(path))
|
|
69232
70385
|
return null;
|
|
69233
|
-
const stat =
|
|
70386
|
+
const stat = statSync11(path);
|
|
69234
70387
|
if (!stat.isFile())
|
|
69235
70388
|
return null;
|
|
69236
|
-
const content =
|
|
70389
|
+
const content = readFileSync17(path);
|
|
69237
70390
|
return { path: relativePath, sha256: sha2567(content), size_bytes: content.length };
|
|
69238
70391
|
}
|
|
69239
70392
|
function manifestRecord(root, relativePath) {
|
|
@@ -72279,7 +73432,7 @@ __export(exports_config_serve_commands, {
|
|
|
72279
73432
|
registerConfigServeCommands: () => registerConfigServeCommands
|
|
72280
73433
|
});
|
|
72281
73434
|
import chalk7 from "chalk";
|
|
72282
|
-
import { existsSync as existsSync23, mkdirSync as mkdirSync12, readFileSync as
|
|
73435
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync12, readFileSync as readFileSync18, writeFileSync as writeFileSync10 } from "fs";
|
|
72283
73436
|
import { dirname as dirname12, join as join24 } from "path";
|
|
72284
73437
|
function registerConfigServeCommands(program2) {
|
|
72285
73438
|
program2.command("config").description("View or update configuration").option("--get <key>", "Get a config value").option("--set <key=value>", "Set a config value (e.g. completion_guard.enabled=true)").action((opts) => {
|
|
@@ -72310,7 +73463,7 @@ function registerConfigServeCommands(program2) {
|
|
|
72310
73463
|
}
|
|
72311
73464
|
let config2 = {};
|
|
72312
73465
|
try {
|
|
72313
|
-
config2 = JSON.parse(
|
|
73466
|
+
config2 = JSON.parse(readFileSync18(configPath, "utf-8"));
|
|
72314
73467
|
} catch {}
|
|
72315
73468
|
const keys = key2.split(".");
|
|
72316
73469
|
let obj = config2;
|
|
@@ -72450,7 +73603,7 @@ function registerConfigServeCommands(program2) {
|
|
|
72450
73603
|
redaction.command("scan [text]").description("Scan text or a file for secret-like values without printing values").option("--file <path>", "File to scan").action(async (text2, opts) => {
|
|
72451
73604
|
const globalOpts = program2.opts();
|
|
72452
73605
|
const { listSecretFindings: listSecretFindings2 } = await Promise.resolve().then(() => (init_redaction(), exports_redaction));
|
|
72453
|
-
const value = opts.file ?
|
|
73606
|
+
const value = opts.file ? readFileSync18(opts.file, "utf-8") : text2 || "";
|
|
72454
73607
|
const findings = listSecretFindings2(value);
|
|
72455
73608
|
if (globalOpts.json) {
|
|
72456
73609
|
output({ ok: findings.length === 0, findings }, true);
|
|
@@ -74016,7 +75169,7 @@ __export(exports_task_route_sources, {
|
|
|
74016
75169
|
});
|
|
74017
75170
|
import { Database as Database4 } from "bun:sqlite";
|
|
74018
75171
|
import { createHash as createHash19 } from "crypto";
|
|
74019
|
-
import { existsSync as existsSync25, readdirSync as readdirSync5, statSync as
|
|
75172
|
+
import { existsSync as existsSync25, readdirSync as readdirSync5, statSync as statSync12 } from "fs";
|
|
74020
75173
|
import { basename as basename10, dirname as dirname14, join as join26, resolve as resolve22 } from "path";
|
|
74021
75174
|
function normalizePath6(input) {
|
|
74022
75175
|
return resolve22(input);
|
|
@@ -74092,7 +75245,7 @@ function discoverStoresUnderRoot(sourceRoot) {
|
|
|
74092
75245
|
}
|
|
74093
75246
|
let rootStat;
|
|
74094
75247
|
try {
|
|
74095
|
-
rootStat =
|
|
75248
|
+
rootStat = statSync12(rootPath);
|
|
74096
75249
|
} catch (error2) {
|
|
74097
75250
|
const ref = createStoreRef(join26(rootPath, TODO_STORE_RELATIVE_PATH));
|
|
74098
75251
|
errors2.push({
|
|
@@ -74825,7 +75978,7 @@ __export(exports_query_commands, {
|
|
|
74825
75978
|
registerQueryCommands: () => registerQueryCommands
|
|
74826
75979
|
});
|
|
74827
75980
|
import chalk9 from "chalk";
|
|
74828
|
-
import { readFileSync as
|
|
75981
|
+
import { readFileSync as readFileSync19, writeFileSync as writeFileSync12 } from "fs";
|
|
74829
75982
|
function parseJsonObjectOption2(value, label) {
|
|
74830
75983
|
if (!value)
|
|
74831
75984
|
return;
|
|
@@ -75407,8 +76560,29 @@ No task claimed (nothing available).`));
|
|
|
75407
76560
|
handleError(new Error("Failed to assign"));
|
|
75408
76561
|
}
|
|
75409
76562
|
});
|
|
75410
|
-
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) => {
|
|
75411
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
|
+
}
|
|
75412
76586
|
const resolvedId = resolveTaskId(id);
|
|
75413
76587
|
const db = getDatabase();
|
|
75414
76588
|
const task3 = getTask(resolvedId, db);
|
|
@@ -75845,13 +77019,13 @@ Findings`));
|
|
|
75845
77019
|
try {
|
|
75846
77020
|
const db = getDatabase();
|
|
75847
77021
|
const row = db.query("SELECT COUNT(*) as count FROM tasks").get();
|
|
75848
|
-
const { statSync:
|
|
77022
|
+
const { statSync: statSync13 } = await import("fs");
|
|
75849
77023
|
const { join: join27 } = await import("path");
|
|
75850
77024
|
const { getHomeDir: getHomeDir2 } = await Promise.resolve().then(() => (init_sync_utils(), exports_sync_utils));
|
|
75851
77025
|
const dbPath = process.env["HASNA_TODOS_DB_PATH"] || process.env["TODOS_DB_PATH"] || join27(getHomeDir2(), ".hasna", "todos", "todos.db");
|
|
75852
77026
|
let size = "unknown";
|
|
75853
77027
|
try {
|
|
75854
|
-
size = `${(
|
|
77028
|
+
size = `${(statSync13(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
|
|
75855
77029
|
} catch {}
|
|
75856
77030
|
checks.push({ name: "Database", ok: true, message: `${row.count} tasks \xB7 ${size} \xB7 ${chalk9.dim(dbPath)}` });
|
|
75857
77031
|
} catch (e) {
|
|
@@ -76568,7 +77742,7 @@ Findings`));
|
|
|
76568
77742
|
const sessionId = opts.session || globalOpts.session || undefined;
|
|
76569
77743
|
try {
|
|
76570
77744
|
if (opts.import) {
|
|
76571
|
-
const bundle = JSON.parse(
|
|
77745
|
+
const bundle = JSON.parse(readFileSync19(opts.import, "utf-8"));
|
|
76572
77746
|
const result = importHandoffBundle(bundle, { apply: opts.apply }, db);
|
|
76573
77747
|
if (opts.json || globalOpts.json) {
|
|
76574
77748
|
console.log(JSON.stringify(result));
|
|
@@ -76942,7 +78116,7 @@ Findings`));
|
|
|
76942
78116
|
});
|
|
76943
78117
|
calendar.command("import <path>").description("Import VEVENT entries from an ICS file as local imported calendar items").option("-j, --json", "Output JSON").action((path, opts) => {
|
|
76944
78118
|
try {
|
|
76945
|
-
const result = importCalendarIcs(
|
|
78119
|
+
const result = importCalendarIcs(readFileSync19(path, "utf-8"));
|
|
76946
78120
|
if (opts.json || program2.opts().json) {
|
|
76947
78121
|
output(result, true);
|
|
76948
78122
|
return;
|
|
@@ -77093,7 +78267,7 @@ Findings`));
|
|
|
77093
78267
|
});
|
|
77094
78268
|
board.command("import <path>").description("Import local board definitions from a JSON bundle").option("-j, --json", "Output JSON").action((path, opts) => {
|
|
77095
78269
|
try {
|
|
77096
|
-
const bundle = JSON.parse(
|
|
78270
|
+
const bundle = JSON.parse(readFileSync19(path, "utf-8"));
|
|
77097
78271
|
const result = importTaskBoardBundle(bundle);
|
|
77098
78272
|
if (opts.json || program2.opts().json) {
|
|
77099
78273
|
output(result, true);
|
|
@@ -77469,7 +78643,7 @@ Findings`));
|
|
|
77469
78643
|
const { importExternalIssues: importExternalIssues2 } = await Promise.resolve().then(() => (init_external_issue_importers(), exports_external_issue_importers));
|
|
77470
78644
|
let body2 = text2 || "";
|
|
77471
78645
|
if (opts.file)
|
|
77472
|
-
body2 =
|
|
78646
|
+
body2 = readFileSync19(opts.file, "utf-8");
|
|
77473
78647
|
if (!body2 && !opts.url && !process.stdin.isTTY)
|
|
77474
78648
|
body2 = await Bun.stdin.text();
|
|
77475
78649
|
if (!body2.trim() && !opts.url) {
|
|
@@ -77526,7 +78700,7 @@ Findings`));
|
|
|
77526
78700
|
} = await Promise.resolve().then(() => (init_tester_issue_reports(), exports_tester_issue_reports));
|
|
77527
78701
|
let body2 = jsonText || "";
|
|
77528
78702
|
if (opts.file)
|
|
77529
|
-
body2 =
|
|
78703
|
+
body2 = readFileSync19(opts.file, "utf-8");
|
|
77530
78704
|
if (!body2 && !process.stdin.isTTY)
|
|
77531
78705
|
body2 = await Bun.stdin.text();
|
|
77532
78706
|
if (!body2.trim()) {
|
|
@@ -77569,11 +78743,11 @@ Findings`));
|
|
|
77569
78743
|
const inbox = program2.command("inbox").description("Capture local inbox items from pasted errors, CI logs, git context, files, or GitHub issue URLs");
|
|
77570
78744
|
inbox.command("add [text]").description("Create a local inbox item and linked task from text, stdin, or a file").option("--file <path>", "Read captured context from a file").option("--source-type <type>", "pasted_error, ci_log, git_context, github_issue, file, or other").option("--source-name <name>", "Human-readable source name").option("--source-url <url>", "Source URL, including GitHub issue URLs").option("--title <title>", "Task/inbox title").option("--priority <priority>", "Task priority").option("--tags <tags>", "Comma-separated extra tags").option("--metadata <json>", "Additional JSON metadata").option("--no-task", "Only store inbox item; do not create a linked task").option("-j, --json", "Output as JSON").action(async (text2, opts) => {
|
|
77571
78745
|
const globalOpts = program2.opts();
|
|
77572
|
-
const { readFileSync:
|
|
78746
|
+
const { readFileSync: readFileSync20 } = await import("fs");
|
|
77573
78747
|
const { createInboxItem: createInboxItem2 } = await Promise.resolve().then(() => (init_inbox(), exports_inbox));
|
|
77574
78748
|
let body2 = text2 || "";
|
|
77575
78749
|
if (opts.file)
|
|
77576
|
-
body2 =
|
|
78750
|
+
body2 = readFileSync20(opts.file, "utf-8");
|
|
77577
78751
|
if (!body2 && !process.stdin.isTTY)
|
|
77578
78752
|
body2 = await Bun.stdin.text();
|
|
77579
78753
|
if (!body2.trim()) {
|
|
@@ -77633,11 +78807,11 @@ ${diff}` : null].filter(Boolean).join(`
|
|
|
77633
78807
|
});
|
|
77634
78808
|
inbox.command("parse [text]").description("Preview or apply deterministic local natural-language task intake").option("--file <path>", "Read natural-language input from a file").option("--priority <priority>", "Default priority for parsed tasks", "medium").option("--project <id>", "Project ID for applied tasks").option("--list <id>", "Task list ID for applied tasks").option("--reference-date <iso>", "Reference date for due today/tomorrow/next week").option("--apply", "Create parsed tasks; default is dry-run preview").option("-j, --json", "Output as JSON").action(async (text2, opts) => {
|
|
77635
78809
|
const globalOpts = program2.opts();
|
|
77636
|
-
const { readFileSync:
|
|
78810
|
+
const { readFileSync: readFileSync20 } = await import("fs");
|
|
77637
78811
|
const { previewNaturalLanguageIntake: previewNaturalLanguageIntake2 } = await Promise.resolve().then(() => (init_natural_language_intake(), exports_natural_language_intake));
|
|
77638
78812
|
let body2 = text2 || "";
|
|
77639
78813
|
if (opts.file)
|
|
77640
|
-
body2 =
|
|
78814
|
+
body2 = readFileSync20(opts.file, "utf-8");
|
|
77641
78815
|
if (!body2 && !process.stdin.isTTY)
|
|
77642
78816
|
body2 = await Bun.stdin.text();
|
|
77643
78817
|
if (!body2.trim()) {
|
|
@@ -77964,7 +79138,7 @@ __export(exports_mcp_hooks_commands, {
|
|
|
77964
79138
|
});
|
|
77965
79139
|
import chalk10 from "chalk";
|
|
77966
79140
|
import { execSync as execSync3 } from "child_process";
|
|
77967
|
-
import { existsSync as existsSync26, readFileSync as
|
|
79141
|
+
import { existsSync as existsSync26, readFileSync as readFileSync20, writeFileSync as writeFileSync13, mkdirSync as mkdirSync14, chmodSync as chmodSync3 } from "fs";
|
|
77968
79142
|
import { dirname as dirname15, join as join27 } from "path";
|
|
77969
79143
|
function getMcpBinaryPath() {
|
|
77970
79144
|
try {
|
|
@@ -77981,7 +79155,7 @@ function readJsonFile2(path) {
|
|
|
77981
79155
|
if (!existsSync26(path))
|
|
77982
79156
|
return {};
|
|
77983
79157
|
try {
|
|
77984
|
-
return JSON.parse(
|
|
79158
|
+
return JSON.parse(readFileSync20(path, "utf-8"));
|
|
77985
79159
|
} catch {
|
|
77986
79160
|
return {};
|
|
77987
79161
|
}
|
|
@@ -77996,7 +79170,7 @@ function writeJsonFile2(path, data) {
|
|
|
77996
79170
|
function readTomlFile(path) {
|
|
77997
79171
|
if (!existsSync26(path))
|
|
77998
79172
|
return "";
|
|
77999
|
-
return
|
|
79173
|
+
return readFileSync20(path, "utf-8");
|
|
78000
79174
|
}
|
|
78001
79175
|
function writeTomlFile(path, content) {
|
|
78002
79176
|
const dir = dirname15(path);
|
|
@@ -79187,7 +80361,7 @@ Artifacts:`));
|
|
|
79187
80361
|
const hookPath = `${gitDir}/hooks/post-commit`;
|
|
79188
80362
|
const marker = "# todos-auto-link";
|
|
79189
80363
|
if (existsSync26(hookPath)) {
|
|
79190
|
-
const existing =
|
|
80364
|
+
const existing = readFileSync20(hookPath, "utf-8");
|
|
79191
80365
|
if (existing.includes(marker)) {
|
|
79192
80366
|
console.log(chalk10.yellow("Hook already installed."));
|
|
79193
80367
|
return;
|
|
@@ -79217,7 +80391,7 @@ $(dirname "$0")/../../scripts/post-commit-hook.sh
|
|
|
79217
80391
|
console.log(chalk10.dim("No post-commit hook found."));
|
|
79218
80392
|
return;
|
|
79219
80393
|
}
|
|
79220
|
-
const content =
|
|
80394
|
+
const content = readFileSync20(hookPath, "utf-8");
|
|
79221
80395
|
if (!content.includes(marker)) {
|
|
79222
80396
|
console.log(chalk10.dim("Hook not managed by todos."));
|
|
79223
80397
|
return;
|
|
@@ -79468,7 +80642,7 @@ var STDIN_SENTINEL = "-";
|
|
|
79468
80642
|
var init_delegation_brief = () => {};
|
|
79469
80643
|
|
|
79470
80644
|
// src/lib/delegation-policy.ts
|
|
79471
|
-
import { readFileSync as
|
|
80645
|
+
import { readFileSync as readFileSync21 } from "fs";
|
|
79472
80646
|
import { homedir as homedir4 } from "os";
|
|
79473
80647
|
import { join as join28 } from "path";
|
|
79474
80648
|
function defaultDelegationEmbargoPath() {
|
|
@@ -79476,7 +80650,7 @@ function defaultDelegationEmbargoPath() {
|
|
|
79476
80650
|
}
|
|
79477
80651
|
function loadDelegationEmbargo(path = defaultDelegationEmbargoPath()) {
|
|
79478
80652
|
try {
|
|
79479
|
-
const parsed = JSON.parse(
|
|
80653
|
+
const parsed = JSON.parse(readFileSync21(path, "utf8"));
|
|
79480
80654
|
const entries = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.embargoed) ? parsed.embargoed : [];
|
|
79481
80655
|
const names = new Set;
|
|
79482
80656
|
for (const entry2 of entries) {
|
|
@@ -79559,15 +80733,15 @@ __export(exports_delegate, {
|
|
|
79559
80733
|
registerDelegateCommands: () => registerDelegateCommands
|
|
79560
80734
|
});
|
|
79561
80735
|
import chalk12 from "chalk";
|
|
79562
|
-
import { readFileSync as
|
|
80736
|
+
import { readFileSync as readFileSync22 } from "fs";
|
|
79563
80737
|
function registerDelegateCommands(program2) {
|
|
79564
80738
|
program2.command("delegate <task> <worker>").description("Hand a filed task to a worker in one call: brief, depth, lineage, assignment, record and notice").option("--brief <path>", "Path to the self-sufficient brief; `-` reads stdin").option("--brief-text <text>", "Inline brief, as an alternative to --brief").option("--depth-threshold <n>", "Open-task count above which this delegation parks").option("--despite-depth", "Proceed past an armed depth threshold; recorded in the [DISPATCH] comment").option("--owner-directive", "Mark as an owner-directive dispatch: depth warns and never parks").option("--seat <slug>", "Seat whose open count is read (default: the lineage parent)").option("--runtime <name>", "Worker runtime label recorded in the comment (e.g. claude-code-subagent)").option("--reports-to <agent>", "Lineage parent for the worker identity (default: the dispatcher)").option("--reuse-identity", "Skip registration and reuse an existing worker identity").option("--depth <n>", "Explicit delegation_depth (default: the task's current depth + 1)").option("--channel <name>", "Channel for the one-line notice (default: $TODOS_DELEGATE_NOTICE_CHANNEL)").option("--no-post", "Skip the channel notice").option("--claim-window <minutes>", `Minutes before the claim deadline (default: ${DEFAULT_CLAIM_WINDOW_MINUTES})`).option("--assign-seat", "Allow <worker> to name a durable seat (a seat queue has no session watching it)").option("--dry-run", "Report all seven effects and perform none").option("-j, --json", "Output as JSON").action(async (taskRef, workerInput, opts) => {
|
|
79565
80739
|
const globalOpts = program2.opts();
|
|
79566
80740
|
const useJson = Boolean(opts.json || globalOpts.json);
|
|
79567
80741
|
try {
|
|
79568
80742
|
const brief = resolveDelegationBrief({ briefPath: opts.brief, briefText: opts.briefText }, {
|
|
79569
|
-
readFile: (path) =>
|
|
79570
|
-
readStdin: () =>
|
|
80743
|
+
readFile: (path) => readFileSync22(path, "utf8"),
|
|
80744
|
+
readStdin: () => readFileSync22(0, "utf8")
|
|
79571
80745
|
});
|
|
79572
80746
|
if (!brief.ok)
|
|
79573
80747
|
handleError(new Error(brief.message));
|
|
@@ -79815,7 +80989,7 @@ __export(exports_machines, {
|
|
|
79815
80989
|
});
|
|
79816
80990
|
import chalk13 from "chalk";
|
|
79817
80991
|
import { execSync as execSync4 } from "child_process";
|
|
79818
|
-
import { readFileSync as
|
|
80992
|
+
import { readFileSync as readFileSync23, unlinkSync as unlinkSync3, writeFileSync as writeFileSync14 } from "fs";
|
|
79819
80993
|
import { tmpdir as tmpdir5 } from "os";
|
|
79820
80994
|
import { join as join29 } from "path";
|
|
79821
80995
|
function getOrCreateLocalMachineName() {
|
|
@@ -79859,7 +81033,7 @@ function readRemoteBridgeBundle(sshAddress) {
|
|
|
79859
81033
|
try {
|
|
79860
81034
|
runSsh(sshAddress, `todos export --format bridge --allow-plaintext-sensitive --output ${shellQuote(remotePath)}`, 120000);
|
|
79861
81035
|
scpFromRemote(sshAddress, remotePath, localPath);
|
|
79862
|
-
return JSON.parse(
|
|
81036
|
+
return JSON.parse(readFileSync23(localPath, "utf-8"));
|
|
79863
81037
|
} finally {
|
|
79864
81038
|
try {
|
|
79865
81039
|
runSsh(sshAddress, `rm -f ${shellQuote(remotePath)}`, 1e4);
|
|
@@ -84817,7 +85991,7 @@ var exports_roadmap_commands = {};
|
|
|
84817
85991
|
__export(exports_roadmap_commands, {
|
|
84818
85992
|
registerRoadmapCommands: () => registerRoadmapCommands
|
|
84819
85993
|
});
|
|
84820
|
-
import { readFileSync as
|
|
85994
|
+
import { readFileSync as readFileSync24, writeFileSync as writeFileSync16 } from "fs";
|
|
84821
85995
|
import chalk24 from "chalk";
|
|
84822
85996
|
function splitList3(value) {
|
|
84823
85997
|
return value?.split(";").flatMap((part) => part.split(",")).map((item) => item.trim()).filter(Boolean);
|
|
@@ -85046,7 +86220,7 @@ function registerRoadmapCommands(program2) {
|
|
|
85046
86220
|
const globalOpts = globalOptions(program2);
|
|
85047
86221
|
try {
|
|
85048
86222
|
const { importRoadmapBundle: importRoadmapBundle2 } = await Promise.resolve().then(() => (init_roadmaps(), exports_roadmaps));
|
|
85049
|
-
const bundle = JSON.parse(
|
|
86223
|
+
const bundle = JSON.parse(readFileSync24(path, "utf8"));
|
|
85050
86224
|
const result = importRoadmapBundle2(bundle, { apply: Boolean(opts.apply) });
|
|
85051
86225
|
if (globalOpts.json) {
|
|
85052
86226
|
output(result, true);
|
|
@@ -85806,6 +86980,15 @@ function createShadowTodosStorageAdapter(options) {
|
|
|
85806
86980
|
mirror.enqueueUpsert("plans", plan, context);
|
|
85807
86981
|
return plan;
|
|
85808
86982
|
},
|
|
86983
|
+
async completeAtRevision(id, expectedUpdatedAt, context) {
|
|
86984
|
+
if (typeof local.plans.completeAtRevision !== "function") {
|
|
86985
|
+
throw new Error("Atomic plan completion is not supported by the local shadow adapter");
|
|
86986
|
+
}
|
|
86987
|
+
const completed = await local.plans.completeAtRevision(id, expectedUpdatedAt, context);
|
|
86988
|
+
if (completed.applied)
|
|
86989
|
+
mirror.enqueueUpsert("plans", completed.plan, context);
|
|
86990
|
+
return completed;
|
|
86991
|
+
},
|
|
85809
86992
|
async delete(id, context) {
|
|
85810
86993
|
const deleted = await local.plans.delete(id, context);
|
|
85811
86994
|
if (deleted)
|
|
@@ -87861,6 +89044,7 @@ program2.name("todos").description("Universal task management for AI coding agen
|
|
|
87861
89044
|
var authority;
|
|
87862
89045
|
try {
|
|
87863
89046
|
authority = initializeTodosCliAuthority();
|
|
89047
|
+
applyTodosCliAuthorityEnvironment(authority);
|
|
87864
89048
|
} catch (error2) {
|
|
87865
89049
|
console.error(error2 instanceof Error ? error2.message : String(error2));
|
|
87866
89050
|
process.exit(1);
|