@hasna/todos 0.11.56 → 0.11.58
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/README.md +34 -0
- package/dist/cli/commands/dispatch.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/helpers.d.ts +0 -2
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/index.js +1887 -1124
- package/dist/contracts.d.ts +2 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +1408 -95
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/db/task-lifecycle.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1808 -374
- package/dist/json-contracts.d.ts.map +1 -1
- package/dist/lib/dispatch.d.ts +3 -0
- package/dist/lib/dispatch.d.ts.map +1 -1
- package/dist/lib/event-hooks.d.ts +1 -1
- package/dist/lib/event-hooks.d.ts.map +1 -1
- package/dist/lib/json-schemas.d.ts +1 -1
- package/dist/lib/json-schemas.d.ts.map +1 -1
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/lib/shared-events.d.ts +14 -0
- package/dist/lib/shared-events.d.ts.map +1 -0
- package/dist/lib/tester-issue-reports.d.ts +105 -0
- package/dist/lib/tester-issue-reports.d.ts.map +1 -0
- package/dist/lib/tmux.d.ts +19 -1
- package/dist/lib/tmux.d.ts.map +1 -1
- package/dist/mcp/index.js +1053 -256
- package/dist/mcp/tools/dispatch.d.ts.map +1 -1
- package/dist/registry.js +1400 -95
- package/dist/release-provenance.json +7 -0
- package/dist/server/index.js +1049 -252
- package/dist/storage.js +845 -146
- package/package.json +2 -2
package/dist/server/index.js
CHANGED
|
@@ -3989,6 +3989,7 @@ var init_event_hooks = __esm(() => {
|
|
|
3989
3989
|
init_runner_sandbox();
|
|
3990
3990
|
init_config();
|
|
3991
3991
|
LOCAL_EVENT_TYPES = [
|
|
3992
|
+
"task.created",
|
|
3992
3993
|
"task.assigned",
|
|
3993
3994
|
"task.blocked",
|
|
3994
3995
|
"task.started",
|
|
@@ -4013,6 +4014,760 @@ var init_event_hooks = __esm(() => {
|
|
|
4013
4014
|
VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
|
|
4014
4015
|
});
|
|
4015
4016
|
|
|
4017
|
+
// node_modules/.bun/@hasna+events@0.1.9/node_modules/@hasna/events/dist/index.js
|
|
4018
|
+
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
4019
|
+
import { existsSync as existsSync6 } from "fs";
|
|
4020
|
+
import { homedir } from "os";
|
|
4021
|
+
import { join as join5 } from "path";
|
|
4022
|
+
import { createHmac, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
4023
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
4024
|
+
import { spawn } from "child_process";
|
|
4025
|
+
import { randomUUID as randomUUID22 } from "crypto";
|
|
4026
|
+
function getPathValue(input, path) {
|
|
4027
|
+
return path.split(".").reduce((value, part) => {
|
|
4028
|
+
if (value && typeof value === "object" && part in value) {
|
|
4029
|
+
return value[part];
|
|
4030
|
+
}
|
|
4031
|
+
return;
|
|
4032
|
+
}, input);
|
|
4033
|
+
}
|
|
4034
|
+
function wildcardToRegExp(pattern, options = {}) {
|
|
4035
|
+
let body = "";
|
|
4036
|
+
for (let index = 0;index < pattern.length; index += 1) {
|
|
4037
|
+
const char = pattern[index];
|
|
4038
|
+
if (char === "*") {
|
|
4039
|
+
if (pattern[index + 1] === "*") {
|
|
4040
|
+
body += ".*";
|
|
4041
|
+
index += 1;
|
|
4042
|
+
} else {
|
|
4043
|
+
body += options.segmentSafe ? "[^/]*" : ".*";
|
|
4044
|
+
}
|
|
4045
|
+
} else {
|
|
4046
|
+
body += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
4049
|
+
return new RegExp(`^${body}$`);
|
|
4050
|
+
}
|
|
4051
|
+
function matchString(value, matcher, options = {}) {
|
|
4052
|
+
if (matcher === undefined)
|
|
4053
|
+
return true;
|
|
4054
|
+
if (value === undefined)
|
|
4055
|
+
return false;
|
|
4056
|
+
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
4057
|
+
return matchers.some((item) => wildcardToRegExp(item, options).test(value));
|
|
4058
|
+
}
|
|
4059
|
+
function matchRecord(input, matcher) {
|
|
4060
|
+
if (!matcher)
|
|
4061
|
+
return true;
|
|
4062
|
+
return Object.entries(matcher).every(([path, expected]) => {
|
|
4063
|
+
const actual = getPathValue(input, path);
|
|
4064
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
4065
|
+
return matchString(actual === undefined ? undefined : String(actual), expected, {
|
|
4066
|
+
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
4067
|
+
});
|
|
4068
|
+
}
|
|
4069
|
+
return actual === expected;
|
|
4070
|
+
});
|
|
4071
|
+
}
|
|
4072
|
+
function eventMatchesFilter(event, filter) {
|
|
4073
|
+
return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
|
|
4074
|
+
}
|
|
4075
|
+
function channelMatchesEvent(channel, event) {
|
|
4076
|
+
if (!channel.enabled)
|
|
4077
|
+
return false;
|
|
4078
|
+
if (!channel.filters || channel.filters.length === 0)
|
|
4079
|
+
return true;
|
|
4080
|
+
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
4081
|
+
}
|
|
4082
|
+
function getEventsDataDir(override) {
|
|
4083
|
+
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join5(homedir(), ".hasna", "events");
|
|
4084
|
+
}
|
|
4085
|
+
|
|
4086
|
+
class JsonEventsStore {
|
|
4087
|
+
dataDir;
|
|
4088
|
+
channelsPath;
|
|
4089
|
+
eventsPath;
|
|
4090
|
+
deliveriesPath;
|
|
4091
|
+
constructor(dataDir = getEventsDataDir()) {
|
|
4092
|
+
this.dataDir = dataDir;
|
|
4093
|
+
this.channelsPath = join5(dataDir, "channels.json");
|
|
4094
|
+
this.eventsPath = join5(dataDir, "events.json");
|
|
4095
|
+
this.deliveriesPath = join5(dataDir, "deliveries.json");
|
|
4096
|
+
}
|
|
4097
|
+
async init() {
|
|
4098
|
+
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
4099
|
+
await chmod(this.dataDir, 448).catch(() => {
|
|
4100
|
+
return;
|
|
4101
|
+
});
|
|
4102
|
+
await this.ensureArrayFile(this.channelsPath);
|
|
4103
|
+
await this.ensureArrayFile(this.eventsPath);
|
|
4104
|
+
await this.ensureArrayFile(this.deliveriesPath);
|
|
4105
|
+
}
|
|
4106
|
+
async addChannel(channel) {
|
|
4107
|
+
await this.init();
|
|
4108
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
4109
|
+
const index = channels.findIndex((item) => item.id === channel.id);
|
|
4110
|
+
if (index >= 0) {
|
|
4111
|
+
channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
|
|
4112
|
+
} else {
|
|
4113
|
+
channels.push(channel);
|
|
4114
|
+
}
|
|
4115
|
+
await this.writeJson(this.channelsPath, channels);
|
|
4116
|
+
return index >= 0 ? channels[index] : channel;
|
|
4117
|
+
}
|
|
4118
|
+
async listChannels() {
|
|
4119
|
+
await this.init();
|
|
4120
|
+
return this.readJson(this.channelsPath, []);
|
|
4121
|
+
}
|
|
4122
|
+
async getChannel(id) {
|
|
4123
|
+
const channels = await this.listChannels();
|
|
4124
|
+
return channels.find((channel) => channel.id === id);
|
|
4125
|
+
}
|
|
4126
|
+
async removeChannel(id) {
|
|
4127
|
+
await this.init();
|
|
4128
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
4129
|
+
const next = channels.filter((channel) => channel.id !== id);
|
|
4130
|
+
await this.writeJson(this.channelsPath, next);
|
|
4131
|
+
return next.length !== channels.length;
|
|
4132
|
+
}
|
|
4133
|
+
async appendEvent(event) {
|
|
4134
|
+
await this.init();
|
|
4135
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
4136
|
+
events.push(event);
|
|
4137
|
+
await this.writeJson(this.eventsPath, events);
|
|
4138
|
+
return event;
|
|
4139
|
+
}
|
|
4140
|
+
async listEvents() {
|
|
4141
|
+
await this.init();
|
|
4142
|
+
return this.readJson(this.eventsPath, []);
|
|
4143
|
+
}
|
|
4144
|
+
async findEventByIdentity(identity) {
|
|
4145
|
+
const events = await this.listEvents();
|
|
4146
|
+
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
4147
|
+
}
|
|
4148
|
+
async appendDelivery(result) {
|
|
4149
|
+
await this.init();
|
|
4150
|
+
const deliveries = await this.readJson(this.deliveriesPath, []);
|
|
4151
|
+
deliveries.push(result);
|
|
4152
|
+
await this.writeJson(this.deliveriesPath, deliveries);
|
|
4153
|
+
return result;
|
|
4154
|
+
}
|
|
4155
|
+
async listDeliveries() {
|
|
4156
|
+
await this.init();
|
|
4157
|
+
return this.readJson(this.deliveriesPath, []);
|
|
4158
|
+
}
|
|
4159
|
+
async exportData() {
|
|
4160
|
+
return {
|
|
4161
|
+
channels: await this.listChannels(),
|
|
4162
|
+
events: await this.listEvents(),
|
|
4163
|
+
deliveries: await this.listDeliveries()
|
|
4164
|
+
};
|
|
4165
|
+
}
|
|
4166
|
+
async ensureArrayFile(path) {
|
|
4167
|
+
if (!existsSync6(path)) {
|
|
4168
|
+
await writeFile(path, `[]
|
|
4169
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
4170
|
+
}
|
|
4171
|
+
await chmod(path, 384).catch(() => {
|
|
4172
|
+
return;
|
|
4173
|
+
});
|
|
4174
|
+
}
|
|
4175
|
+
async readJson(path, fallback) {
|
|
4176
|
+
try {
|
|
4177
|
+
const raw = await readFile(path, "utf-8");
|
|
4178
|
+
if (!raw.trim())
|
|
4179
|
+
return fallback;
|
|
4180
|
+
return JSON.parse(raw);
|
|
4181
|
+
} catch (error) {
|
|
4182
|
+
if (error.code === "ENOENT")
|
|
4183
|
+
return fallback;
|
|
4184
|
+
throw error;
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
async writeJson(path, value) {
|
|
4188
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
4189
|
+
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
|
|
4190
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
4191
|
+
await rename(tempPath, path);
|
|
4192
|
+
await chmod(path, 384).catch(() => {
|
|
4193
|
+
return;
|
|
4194
|
+
});
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
function buildSignatureBase(timestamp, body) {
|
|
4198
|
+
return `${timestamp}.${body}`;
|
|
4199
|
+
}
|
|
4200
|
+
function signPayload(secret, timestamp, body) {
|
|
4201
|
+
const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
|
|
4202
|
+
return `sha256=${digest}`;
|
|
4203
|
+
}
|
|
4204
|
+
function now2() {
|
|
4205
|
+
return new Date().toISOString();
|
|
4206
|
+
}
|
|
4207
|
+
function truncate(value, max = 4096) {
|
|
4208
|
+
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
4209
|
+
}
|
|
4210
|
+
function buildWebhookRequest(event, channel) {
|
|
4211
|
+
if (!channel.webhook)
|
|
4212
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
4213
|
+
const body = JSON.stringify(event);
|
|
4214
|
+
const timestamp = event.time;
|
|
4215
|
+
const headers = {
|
|
4216
|
+
"Content-Type": "application/json",
|
|
4217
|
+
"User-Agent": "@hasna/events",
|
|
4218
|
+
"X-Hasna-Event-Id": event.id,
|
|
4219
|
+
"X-Hasna-Event-Type": event.type,
|
|
4220
|
+
"X-Hasna-Timestamp": timestamp,
|
|
4221
|
+
...channel.webhook.headers
|
|
4222
|
+
};
|
|
4223
|
+
if (channel.webhook.secret) {
|
|
4224
|
+
headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
|
|
4225
|
+
}
|
|
4226
|
+
return { body, headers };
|
|
4227
|
+
}
|
|
4228
|
+
async function dispatchWebhook(event, channel, options = {}) {
|
|
4229
|
+
if (!channel.webhook)
|
|
4230
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
4231
|
+
const startedAt = now2();
|
|
4232
|
+
const { body, headers } = buildWebhookRequest(event, channel);
|
|
4233
|
+
const controller = new AbortController;
|
|
4234
|
+
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
4235
|
+
try {
|
|
4236
|
+
const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
|
|
4237
|
+
method: "POST",
|
|
4238
|
+
headers,
|
|
4239
|
+
body,
|
|
4240
|
+
signal: controller.signal
|
|
4241
|
+
});
|
|
4242
|
+
const responseBody = truncate(await response.text());
|
|
4243
|
+
return {
|
|
4244
|
+
attempt: 1,
|
|
4245
|
+
status: response.ok ? "success" : "failed",
|
|
4246
|
+
startedAt,
|
|
4247
|
+
completedAt: now2(),
|
|
4248
|
+
responseStatus: response.status,
|
|
4249
|
+
responseBody,
|
|
4250
|
+
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
4251
|
+
};
|
|
4252
|
+
} catch (error) {
|
|
4253
|
+
return {
|
|
4254
|
+
attempt: 1,
|
|
4255
|
+
status: "failed",
|
|
4256
|
+
startedAt,
|
|
4257
|
+
completedAt: now2(),
|
|
4258
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4259
|
+
};
|
|
4260
|
+
} finally {
|
|
4261
|
+
clearTimeout(timeout);
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
async function dispatchCommand(event, channel) {
|
|
4265
|
+
if (!channel.command)
|
|
4266
|
+
throw new Error(`Channel ${channel.id} has no command config`);
|
|
4267
|
+
const startedAt = now2();
|
|
4268
|
+
const eventJson = JSON.stringify(event);
|
|
4269
|
+
const env = {
|
|
4270
|
+
...process.env,
|
|
4271
|
+
...channel.command.env,
|
|
4272
|
+
HASNA_CHANNEL_ID: channel.id,
|
|
4273
|
+
HASNA_EVENT_ID: event.id,
|
|
4274
|
+
HASNA_EVENT_TYPE: event.type,
|
|
4275
|
+
HASNA_EVENT_SOURCE: event.source,
|
|
4276
|
+
HASNA_EVENT_SUBJECT: event.subject ?? "",
|
|
4277
|
+
HASNA_EVENT_SEVERITY: event.severity,
|
|
4278
|
+
HASNA_EVENT_TIME: event.time,
|
|
4279
|
+
HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
|
|
4280
|
+
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
4281
|
+
HASNA_EVENT_JSON: eventJson
|
|
4282
|
+
};
|
|
4283
|
+
return new Promise((resolve6) => {
|
|
4284
|
+
const child = spawn(channel.command.command, channel.command.args ?? [], {
|
|
4285
|
+
cwd: channel.command.cwd,
|
|
4286
|
+
env,
|
|
4287
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
4288
|
+
});
|
|
4289
|
+
let stdout = "";
|
|
4290
|
+
let stderr = "";
|
|
4291
|
+
const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
|
|
4292
|
+
child.stdin.end(eventJson);
|
|
4293
|
+
child.stdout.on("data", (chunk) => {
|
|
4294
|
+
stdout += chunk.toString();
|
|
4295
|
+
});
|
|
4296
|
+
child.stderr.on("data", (chunk) => {
|
|
4297
|
+
stderr += chunk.toString();
|
|
4298
|
+
});
|
|
4299
|
+
child.on("error", (error) => {
|
|
4300
|
+
clearTimeout(timeout);
|
|
4301
|
+
resolve6({
|
|
4302
|
+
attempt: 1,
|
|
4303
|
+
status: "failed",
|
|
4304
|
+
startedAt,
|
|
4305
|
+
completedAt: now2(),
|
|
4306
|
+
stdout: truncate(stdout),
|
|
4307
|
+
stderr: truncate(stderr),
|
|
4308
|
+
error: error.message
|
|
4309
|
+
});
|
|
4310
|
+
});
|
|
4311
|
+
child.on("close", (code, signal) => {
|
|
4312
|
+
clearTimeout(timeout);
|
|
4313
|
+
const success = code === 0;
|
|
4314
|
+
resolve6({
|
|
4315
|
+
attempt: 1,
|
|
4316
|
+
status: success ? "success" : "failed",
|
|
4317
|
+
startedAt,
|
|
4318
|
+
completedAt: now2(),
|
|
4319
|
+
stdout: truncate(stdout),
|
|
4320
|
+
stderr: truncate(stderr),
|
|
4321
|
+
error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
4322
|
+
});
|
|
4323
|
+
});
|
|
4324
|
+
});
|
|
4325
|
+
}
|
|
4326
|
+
async function dispatchChannel(event, channel, options = {}) {
|
|
4327
|
+
if (channel.transport === "webhook")
|
|
4328
|
+
return dispatchWebhook(event, channel, options);
|
|
4329
|
+
if (channel.transport === "command")
|
|
4330
|
+
return dispatchCommand(event, channel);
|
|
4331
|
+
return {
|
|
4332
|
+
attempt: 1,
|
|
4333
|
+
status: "skipped",
|
|
4334
|
+
startedAt: now2(),
|
|
4335
|
+
completedAt: now2(),
|
|
4336
|
+
error: `Unsupported transport: ${channel.transport}`
|
|
4337
|
+
};
|
|
4338
|
+
}
|
|
4339
|
+
function createDeliveryResult(event, channel, attempts) {
|
|
4340
|
+
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
4341
|
+
return {
|
|
4342
|
+
id: randomUUID2(),
|
|
4343
|
+
eventId: event.id,
|
|
4344
|
+
channelId: channel.id,
|
|
4345
|
+
transport: channel.transport,
|
|
4346
|
+
status,
|
|
4347
|
+
attempts,
|
|
4348
|
+
createdAt: attempts[0]?.startedAt ?? now2(),
|
|
4349
|
+
completedAt: attempts.at(-1)?.completedAt ?? now2()
|
|
4350
|
+
};
|
|
4351
|
+
}
|
|
4352
|
+
function createEvent(input) {
|
|
4353
|
+
return {
|
|
4354
|
+
id: input.id ?? randomUUID22(),
|
|
4355
|
+
source: input.source,
|
|
4356
|
+
type: input.type,
|
|
4357
|
+
time: normalizeTime(input.time),
|
|
4358
|
+
subject: input.subject,
|
|
4359
|
+
severity: input.severity ?? "info",
|
|
4360
|
+
data: input.data ?? {},
|
|
4361
|
+
message: input.message,
|
|
4362
|
+
dedupeKey: input.dedupeKey,
|
|
4363
|
+
schemaVersion: input.schemaVersion ?? "1.0",
|
|
4364
|
+
metadata: input.metadata ?? {}
|
|
4365
|
+
};
|
|
4366
|
+
}
|
|
4367
|
+
|
|
4368
|
+
class EventsClient {
|
|
4369
|
+
store;
|
|
4370
|
+
redactors;
|
|
4371
|
+
transportOptions;
|
|
4372
|
+
constructor(options = {}) {
|
|
4373
|
+
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
4374
|
+
this.redactors = options.redactors ?? [];
|
|
4375
|
+
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
4376
|
+
}
|
|
4377
|
+
async addChannel(input) {
|
|
4378
|
+
const timestamp = new Date().toISOString();
|
|
4379
|
+
return this.store.addChannel({
|
|
4380
|
+
...input,
|
|
4381
|
+
createdAt: input.createdAt ?? timestamp,
|
|
4382
|
+
updatedAt: input.updatedAt ?? timestamp
|
|
4383
|
+
});
|
|
4384
|
+
}
|
|
4385
|
+
async listChannels() {
|
|
4386
|
+
return this.store.listChannels();
|
|
4387
|
+
}
|
|
4388
|
+
async removeChannel(id) {
|
|
4389
|
+
return this.store.removeChannel(id);
|
|
4390
|
+
}
|
|
4391
|
+
async emit(input, options = {}) {
|
|
4392
|
+
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
4393
|
+
if (options.dedupe !== false) {
|
|
4394
|
+
const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
|
|
4395
|
+
if (existing) {
|
|
4396
|
+
return { event: existing, deliveries: [], deduped: true };
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4399
|
+
await this.store.appendEvent(event);
|
|
4400
|
+
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
4401
|
+
return { event, deliveries, deduped: false };
|
|
4402
|
+
}
|
|
4403
|
+
async listEvents() {
|
|
4404
|
+
return this.store.listEvents();
|
|
4405
|
+
}
|
|
4406
|
+
async listDeliveries() {
|
|
4407
|
+
return this.store.listDeliveries();
|
|
4408
|
+
}
|
|
4409
|
+
async deliver(event) {
|
|
4410
|
+
const channels = await this.store.listChannels();
|
|
4411
|
+
const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
|
|
4412
|
+
const deliveries = [];
|
|
4413
|
+
for (const channel of selected) {
|
|
4414
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
4415
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
4416
|
+
await this.store.appendDelivery(result);
|
|
4417
|
+
deliveries.push(result);
|
|
4418
|
+
}
|
|
4419
|
+
return deliveries;
|
|
4420
|
+
}
|
|
4421
|
+
async matchChannel(id, input = {}) {
|
|
4422
|
+
const channel = await this.store.getChannel(id);
|
|
4423
|
+
if (!channel)
|
|
4424
|
+
throw new Error(`Channel not found: ${id}`);
|
|
4425
|
+
const event = createEvent({
|
|
4426
|
+
source: input.source ?? "hasna.events",
|
|
4427
|
+
type: input.type ?? "events.test",
|
|
4428
|
+
subject: input.subject ?? id,
|
|
4429
|
+
severity: input.severity ?? "info",
|
|
4430
|
+
data: input.data ?? { test: true },
|
|
4431
|
+
message: input.message ?? "Hasna events test delivery",
|
|
4432
|
+
dedupeKey: input.dedupeKey,
|
|
4433
|
+
schemaVersion: input.schemaVersion,
|
|
4434
|
+
metadata: input.metadata,
|
|
4435
|
+
time: input.time,
|
|
4436
|
+
id: input.id
|
|
4437
|
+
});
|
|
4438
|
+
const matched = channelMatchesEvent(channel, event);
|
|
4439
|
+
return {
|
|
4440
|
+
channelId: channel.id,
|
|
4441
|
+
matched,
|
|
4442
|
+
event,
|
|
4443
|
+
filters: channel.filters,
|
|
4444
|
+
reason: matched ? undefined : channel.enabled ? "event did not match channel filters" : "channel is disabled"
|
|
4445
|
+
};
|
|
4446
|
+
}
|
|
4447
|
+
async testChannel(id, input = {}, options = {}) {
|
|
4448
|
+
const channel = await this.store.getChannel(id);
|
|
4449
|
+
if (!channel)
|
|
4450
|
+
throw new Error(`Channel not found: ${id}`);
|
|
4451
|
+
const match = await this.matchChannel(id, input);
|
|
4452
|
+
const event = match.event;
|
|
4453
|
+
if (options.honorFilters && !match.matched) {
|
|
4454
|
+
const timestamp = new Date().toISOString();
|
|
4455
|
+
const result2 = createDeliveryResult(event, channel, [{
|
|
4456
|
+
attempt: 1,
|
|
4457
|
+
status: "skipped",
|
|
4458
|
+
startedAt: timestamp,
|
|
4459
|
+
completedAt: timestamp,
|
|
4460
|
+
error: match.reason
|
|
4461
|
+
}]);
|
|
4462
|
+
result2.metadata = { reason: "filter_mismatch" };
|
|
4463
|
+
await this.store.appendDelivery(result2);
|
|
4464
|
+
return result2;
|
|
4465
|
+
}
|
|
4466
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
4467
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
4468
|
+
await this.store.appendDelivery(result);
|
|
4469
|
+
return result;
|
|
4470
|
+
}
|
|
4471
|
+
async replay(options = {}) {
|
|
4472
|
+
const events = (await this.store.listEvents()).filter((event) => {
|
|
4473
|
+
if (options.eventId && event.id !== options.eventId)
|
|
4474
|
+
return false;
|
|
4475
|
+
if (options.source && event.source !== options.source)
|
|
4476
|
+
return false;
|
|
4477
|
+
if (options.type && event.type !== options.type)
|
|
4478
|
+
return false;
|
|
4479
|
+
return true;
|
|
4480
|
+
});
|
|
4481
|
+
if (options.dryRun)
|
|
4482
|
+
return { events, deliveries: [] };
|
|
4483
|
+
const deliveries = [];
|
|
4484
|
+
for (const event of events) {
|
|
4485
|
+
deliveries.push(...await this.deliver(event));
|
|
4486
|
+
}
|
|
4487
|
+
return { events, deliveries };
|
|
4488
|
+
}
|
|
4489
|
+
async applyRedaction(event, channel) {
|
|
4490
|
+
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
4491
|
+
for (const redactor of this.redactors) {
|
|
4492
|
+
next = await redactor(next, channel);
|
|
4493
|
+
}
|
|
4494
|
+
return next;
|
|
4495
|
+
}
|
|
4496
|
+
async deliverWithRetry(event, channel) {
|
|
4497
|
+
const policy = normalizeRetryPolicy(channel.retry);
|
|
4498
|
+
const attempts = [];
|
|
4499
|
+
for (let index = 0;index < policy.maxAttempts; index += 1) {
|
|
4500
|
+
const attempt = await dispatchChannel(event, channel, this.transportOptions);
|
|
4501
|
+
attempt.attempt = index + 1;
|
|
4502
|
+
if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
|
|
4503
|
+
attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
|
|
4504
|
+
}
|
|
4505
|
+
attempts.push(attempt);
|
|
4506
|
+
if (attempt.status !== "failed")
|
|
4507
|
+
break;
|
|
4508
|
+
if (attempt.nextBackoffMs)
|
|
4509
|
+
await Bun.sleep(attempt.nextBackoffMs);
|
|
4510
|
+
}
|
|
4511
|
+
return createDeliveryResult(event, channel, attempts);
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
4515
|
+
if (paths.length === 0)
|
|
4516
|
+
return event;
|
|
4517
|
+
const copy = structuredClone(event);
|
|
4518
|
+
for (const path of paths) {
|
|
4519
|
+
setPath(copy, path, replacement);
|
|
4520
|
+
}
|
|
4521
|
+
return copy;
|
|
4522
|
+
}
|
|
4523
|
+
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
4524
|
+
return redactValue2(event, replacement);
|
|
4525
|
+
}
|
|
4526
|
+
function shouldRedactKey(key) {
|
|
4527
|
+
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
4528
|
+
}
|
|
4529
|
+
function redactValue2(value, replacement) {
|
|
4530
|
+
if (Array.isArray(value))
|
|
4531
|
+
return value.map((item) => redactValue2(item, replacement));
|
|
4532
|
+
if (!value || typeof value !== "object")
|
|
4533
|
+
return value;
|
|
4534
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
4535
|
+
key,
|
|
4536
|
+
shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
|
|
4537
|
+
]));
|
|
4538
|
+
}
|
|
4539
|
+
function setPath(input, path, replacement) {
|
|
4540
|
+
const parts = path.split(".");
|
|
4541
|
+
let cursor = input;
|
|
4542
|
+
for (const part of parts.slice(0, -1)) {
|
|
4543
|
+
const next = cursor[part];
|
|
4544
|
+
if (!next || typeof next !== "object")
|
|
4545
|
+
return;
|
|
4546
|
+
cursor = next;
|
|
4547
|
+
}
|
|
4548
|
+
const last = parts.at(-1);
|
|
4549
|
+
if (last && last in cursor)
|
|
4550
|
+
cursor[last] = replacement;
|
|
4551
|
+
}
|
|
4552
|
+
function normalizeTime(value) {
|
|
4553
|
+
if (!value)
|
|
4554
|
+
return new Date().toISOString();
|
|
4555
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
4556
|
+
}
|
|
4557
|
+
function normalizeRetryPolicy(policy) {
|
|
4558
|
+
return {
|
|
4559
|
+
maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
|
|
4560
|
+
backoffMs: Math.max(0, policy?.backoffMs ?? 250),
|
|
4561
|
+
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
4562
|
+
};
|
|
4563
|
+
}
|
|
4564
|
+
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", DEFAULT_SIGNATURE_TOLERANCE_MS;
|
|
4565
|
+
var init_dist = __esm(() => {
|
|
4566
|
+
DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
4567
|
+
});
|
|
4568
|
+
|
|
4569
|
+
// src/db/task-lists.ts
|
|
4570
|
+
function rowToTaskList(row) {
|
|
4571
|
+
return {
|
|
4572
|
+
...row,
|
|
4573
|
+
metadata: JSON.parse(row.metadata || "{}")
|
|
4574
|
+
};
|
|
4575
|
+
}
|
|
4576
|
+
function createTaskList(input, db) {
|
|
4577
|
+
const d = db || getDatabase();
|
|
4578
|
+
const id = uuid();
|
|
4579
|
+
const timestamp = now();
|
|
4580
|
+
const slug = input.slug || slugify(input.name);
|
|
4581
|
+
if (!input.project_id) {
|
|
4582
|
+
const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
|
|
4583
|
+
if (existing) {
|
|
4584
|
+
throw new Error(`Standalone task list with slug "${slug}" already exists`);
|
|
4585
|
+
}
|
|
4586
|
+
}
|
|
4587
|
+
d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
|
|
4588
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
|
|
4589
|
+
return getTaskList(id, d);
|
|
4590
|
+
}
|
|
4591
|
+
function getTaskList(id, db) {
|
|
4592
|
+
const d = db || getDatabase();
|
|
4593
|
+
const row = d.query("SELECT * FROM task_lists WHERE id = ?").get(id);
|
|
4594
|
+
return row ? rowToTaskList(row) : null;
|
|
4595
|
+
}
|
|
4596
|
+
function getTaskListBySlug(slug, projectId, db) {
|
|
4597
|
+
const d = db || getDatabase();
|
|
4598
|
+
let row;
|
|
4599
|
+
if (projectId) {
|
|
4600
|
+
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id = ?").get(slug, projectId);
|
|
4601
|
+
} else {
|
|
4602
|
+
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id IS NULL").get(slug);
|
|
4603
|
+
}
|
|
4604
|
+
return row ? rowToTaskList(row) : null;
|
|
4605
|
+
}
|
|
4606
|
+
function listTaskLists(projectId, db) {
|
|
4607
|
+
const d = db || getDatabase();
|
|
4608
|
+
if (projectId) {
|
|
4609
|
+
return d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(projectId).map(rowToTaskList);
|
|
4610
|
+
}
|
|
4611
|
+
return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
|
|
4612
|
+
}
|
|
4613
|
+
function updateTaskList(id, input, db) {
|
|
4614
|
+
const d = db || getDatabase();
|
|
4615
|
+
const existing = getTaskList(id, d);
|
|
4616
|
+
if (!existing)
|
|
4617
|
+
throw new TaskListNotFoundError(id);
|
|
4618
|
+
const sets = ["updated_at = ?"];
|
|
4619
|
+
const params = [now()];
|
|
4620
|
+
if (input.name !== undefined) {
|
|
4621
|
+
sets.push("name = ?");
|
|
4622
|
+
params.push(input.name);
|
|
4623
|
+
}
|
|
4624
|
+
if (input.description !== undefined) {
|
|
4625
|
+
sets.push("description = ?");
|
|
4626
|
+
params.push(input.description);
|
|
4627
|
+
}
|
|
4628
|
+
if (input.metadata !== undefined) {
|
|
4629
|
+
sets.push("metadata = ?");
|
|
4630
|
+
params.push(JSON.stringify(input.metadata));
|
|
4631
|
+
}
|
|
4632
|
+
params.push(id);
|
|
4633
|
+
d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
4634
|
+
return getTaskList(id, d);
|
|
4635
|
+
}
|
|
4636
|
+
function deleteTaskList(id, db) {
|
|
4637
|
+
const d = db || getDatabase();
|
|
4638
|
+
return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
4639
|
+
}
|
|
4640
|
+
function ensureTaskList(name, slug, projectId, db) {
|
|
4641
|
+
const d = db || getDatabase();
|
|
4642
|
+
const existing = getTaskListBySlug(slug, projectId, d);
|
|
4643
|
+
if (existing)
|
|
4644
|
+
return existing;
|
|
4645
|
+
return createTaskList({ name, slug, project_id: projectId }, d);
|
|
4646
|
+
}
|
|
4647
|
+
var init_task_lists = __esm(() => {
|
|
4648
|
+
init_types();
|
|
4649
|
+
init_database();
|
|
4650
|
+
init_projects();
|
|
4651
|
+
});
|
|
4652
|
+
|
|
4653
|
+
// src/lib/shared-events.ts
|
|
4654
|
+
function taskEventData(task, extra = {}) {
|
|
4655
|
+
return {
|
|
4656
|
+
id: task.id,
|
|
4657
|
+
task_id: task.id,
|
|
4658
|
+
short_id: task.short_id,
|
|
4659
|
+
title: task.title,
|
|
4660
|
+
description: task.description,
|
|
4661
|
+
status: task.status,
|
|
4662
|
+
priority: task.priority,
|
|
4663
|
+
project_id: task.project_id,
|
|
4664
|
+
parent_id: task.parent_id,
|
|
4665
|
+
plan_id: task.plan_id,
|
|
4666
|
+
task_list_id: task.task_list_id,
|
|
4667
|
+
agent_id: task.agent_id,
|
|
4668
|
+
assigned_to: task.assigned_to,
|
|
4669
|
+
session_id: task.session_id,
|
|
4670
|
+
working_dir: task.working_dir,
|
|
4671
|
+
tags: task.tags,
|
|
4672
|
+
metadata: task.metadata,
|
|
4673
|
+
version: task.version,
|
|
4674
|
+
created_at: task.created_at,
|
|
4675
|
+
updated_at: task.updated_at,
|
|
4676
|
+
started_at: task.started_at,
|
|
4677
|
+
completed_at: task.completed_at,
|
|
4678
|
+
due_at: task.due_at,
|
|
4679
|
+
...extra
|
|
4680
|
+
};
|
|
4681
|
+
}
|
|
4682
|
+
function taskEventMetadata(task) {
|
|
4683
|
+
const metadata = {
|
|
4684
|
+
package: "@hasna/todos",
|
|
4685
|
+
todos_event_schema_version: 1,
|
|
4686
|
+
task_id: task.id,
|
|
4687
|
+
task_short_id: task.short_id,
|
|
4688
|
+
project_id: task.project_id,
|
|
4689
|
+
task_list_id: task.task_list_id,
|
|
4690
|
+
working_dir: task.working_dir
|
|
4691
|
+
};
|
|
4692
|
+
try {
|
|
4693
|
+
const project = task.project_id ? getProject(task.project_id) : null;
|
|
4694
|
+
const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
|
|
4695
|
+
if (project) {
|
|
4696
|
+
metadata.project_id = project.id;
|
|
4697
|
+
metadata.project_name = project.name;
|
|
4698
|
+
metadata.project_path = projectPath;
|
|
4699
|
+
metadata.project_canonical_path = project.path;
|
|
4700
|
+
metadata.project_default_task_list_slug = project.task_list_id;
|
|
4701
|
+
metadata.root_project_id = inferRootProjectId(project);
|
|
4702
|
+
} else if (projectPath) {
|
|
4703
|
+
metadata.project_path = projectPath;
|
|
4704
|
+
metadata.project_canonical_path = projectPath;
|
|
4705
|
+
}
|
|
4706
|
+
if (projectPath) {
|
|
4707
|
+
metadata.project_kind = classifyProjectKind(projectPath);
|
|
4708
|
+
metadata.project_is_worktree = isWorktreePath(projectPath);
|
|
4709
|
+
if (typeof task.metadata.route_enabled === "boolean") {
|
|
4710
|
+
metadata.route_enabled = task.metadata.route_enabled;
|
|
4711
|
+
}
|
|
4712
|
+
metadata.working_dir = task.working_dir ?? projectPath;
|
|
4713
|
+
}
|
|
4714
|
+
const taskList = task.task_list_id ? getTaskList(task.task_list_id) ?? (project ? getTaskListBySlug(task.task_list_id, project.id) : null) : project?.task_list_id ? getTaskListBySlug(project.task_list_id, project.id) : null;
|
|
4715
|
+
if (taskList) {
|
|
4716
|
+
metadata.task_list_id = taskList.id;
|
|
4717
|
+
metadata.task_list_slug = taskList.slug;
|
|
4718
|
+
metadata.task_list_name = taskList.name;
|
|
4719
|
+
metadata.task_list_project_id = taskList.project_id;
|
|
4720
|
+
metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
|
|
4721
|
+
}
|
|
4722
|
+
} catch {}
|
|
4723
|
+
return metadata;
|
|
4724
|
+
}
|
|
4725
|
+
function classifyProjectKind(path) {
|
|
4726
|
+
return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
|
|
4727
|
+
}
|
|
4728
|
+
function isWorktreePath(path) {
|
|
4729
|
+
return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
|
|
4730
|
+
}
|
|
4731
|
+
function inferRootProjectId(project) {
|
|
4732
|
+
return isWorktreePath(project.path) ? null : project.id;
|
|
4733
|
+
}
|
|
4734
|
+
function readMachineLocalPath(project) {
|
|
4735
|
+
const machineId = process.env["TODOS_MACHINE_ID"];
|
|
4736
|
+
if (!machineId)
|
|
4737
|
+
return null;
|
|
4738
|
+
try {
|
|
4739
|
+
const row = getDatabase().query("SELECT path FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(project.id, machineId);
|
|
4740
|
+
return row?.path ?? null;
|
|
4741
|
+
} catch {
|
|
4742
|
+
return null;
|
|
4743
|
+
}
|
|
4744
|
+
}
|
|
4745
|
+
async function emitSharedTaskEvent(input) {
|
|
4746
|
+
const data = taskEventData(input.task, input.data);
|
|
4747
|
+
await new EventsClient().emit({
|
|
4748
|
+
source: SOURCE,
|
|
4749
|
+
type: input.type,
|
|
4750
|
+
subject: input.task.id,
|
|
4751
|
+
severity: input.severity ?? "info",
|
|
4752
|
+
message: input.message ?? `${input.type}: ${input.task.title}`,
|
|
4753
|
+
data,
|
|
4754
|
+
dedupeKey: input.dedupeKey ?? `${input.type}:${input.task.id}:${input.task.version}`,
|
|
4755
|
+
metadata: taskEventMetadata(input.task)
|
|
4756
|
+
}, { deliver: true, dedupe: true });
|
|
4757
|
+
}
|
|
4758
|
+
function emitSharedTaskEventQuiet(input) {
|
|
4759
|
+
emitSharedTaskEvent(input).catch(() => {
|
|
4760
|
+
return;
|
|
4761
|
+
});
|
|
4762
|
+
}
|
|
4763
|
+
var SOURCE = "todos";
|
|
4764
|
+
var init_shared_events = __esm(() => {
|
|
4765
|
+
init_dist();
|
|
4766
|
+
init_database();
|
|
4767
|
+
init_projects();
|
|
4768
|
+
init_task_lists();
|
|
4769
|
+
});
|
|
4770
|
+
|
|
4016
4771
|
// src/lib/secret-redaction.ts
|
|
4017
4772
|
function isAllowlisted(text, match, allowlist) {
|
|
4018
4773
|
const context = text.slice(Math.max(0, text.indexOf(match) - 20), text.indexOf(match) + match.length + 20);
|
|
@@ -4480,7 +5235,7 @@ async function deliverWebhook(wh, event, body, attempt, db) {
|
|
|
4480
5235
|
activeDeliveries--;
|
|
4481
5236
|
}
|
|
4482
5237
|
}
|
|
4483
|
-
async function
|
|
5238
|
+
async function dispatchWebhook2(event, payload, db) {
|
|
4484
5239
|
const d = db || getDatabase();
|
|
4485
5240
|
const webhooks = listWebhooks(d).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
|
|
4486
5241
|
const payloadObj = typeof payload === "object" && payload !== null ? payload : {};
|
|
@@ -4597,7 +5352,10 @@ function createTask(input, db) {
|
|
|
4597
5352
|
insertTaskTags(id, tags, d);
|
|
4598
5353
|
}
|
|
4599
5354
|
const task = getTask(id, d);
|
|
4600
|
-
|
|
5355
|
+
const payload = taskEventData(task);
|
|
5356
|
+
dispatchWebhook2("task.created", payload, d).catch(() => {});
|
|
5357
|
+
emitLocalEventHooksQuiet({ type: "task.created", payload });
|
|
5358
|
+
emitSharedTaskEventQuiet({ type: "task.created", task });
|
|
4601
5359
|
return task;
|
|
4602
5360
|
}
|
|
4603
5361
|
function getTask(id, db) {
|
|
@@ -4941,18 +5699,7 @@ function updateTask(id, input, db) {
|
|
|
4941
5699
|
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
4942
5700
|
if (input.approved_by !== undefined)
|
|
4943
5701
|
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
4944
|
-
|
|
4945
|
-
dispatchWebhook("task.assigned", { id, assigned_to: input.assigned_to, title: task.title }, d).catch(() => {});
|
|
4946
|
-
emitLocalEventHooksQuiet({ type: "task.assigned", payload: { id, assigned_to: input.assigned_to, title: task.title } });
|
|
4947
|
-
}
|
|
4948
|
-
if (input.status !== undefined && input.status !== task.status) {
|
|
4949
|
-
dispatchWebhook("task.status_changed", { id, old_status: task.status, new_status: input.status, title: task.title }, d).catch(() => {});
|
|
4950
|
-
emitLocalEventHooksQuiet({ type: "task.status_changed", payload: { id, old_status: task.status, new_status: input.status, title: task.title } });
|
|
4951
|
-
}
|
|
4952
|
-
if (input.approved_by !== undefined) {
|
|
4953
|
-
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
|
|
4954
|
-
}
|
|
4955
|
-
return {
|
|
5702
|
+
const updatedTask = {
|
|
4956
5703
|
...task,
|
|
4957
5704
|
...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
|
|
4958
5705
|
tags: input.tags ?? task.tags,
|
|
@@ -4970,6 +5717,22 @@ function updateTask(id, input, db) {
|
|
|
4970
5717
|
approved_by: input.approved_by ?? task.approved_by,
|
|
4971
5718
|
approved_at: input.approved_by ? timestamp : task.approved_at
|
|
4972
5719
|
};
|
|
5720
|
+
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
|
|
5721
|
+
const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
|
|
5722
|
+
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
5723
|
+
emitLocalEventHooksQuiet({ type: "task.assigned", payload });
|
|
5724
|
+
emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to } });
|
|
5725
|
+
}
|
|
5726
|
+
if (input.status !== undefined && input.status !== task.status) {
|
|
5727
|
+
const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
|
|
5728
|
+
dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
|
|
5729
|
+
emitLocalEventHooksQuiet({ type: "task.status_changed", payload });
|
|
5730
|
+
emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status } });
|
|
5731
|
+
}
|
|
5732
|
+
if (input.approved_by !== undefined) {
|
|
5733
|
+
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
|
|
5734
|
+
}
|
|
5735
|
+
return updatedTask;
|
|
4973
5736
|
}
|
|
4974
5737
|
function deleteTask(id, db) {
|
|
4975
5738
|
const d = db || getDatabase();
|
|
@@ -4981,6 +5744,7 @@ var init_task_crud = __esm(() => {
|
|
|
4981
5744
|
init_database();
|
|
4982
5745
|
init_completion_guard();
|
|
4983
5746
|
init_event_hooks();
|
|
5747
|
+
init_shared_events();
|
|
4984
5748
|
init_audit();
|
|
4985
5749
|
init_webhooks();
|
|
4986
5750
|
init_checklists();
|
|
@@ -5736,9 +6500,12 @@ function startTask(id, agentId, db) {
|
|
|
5736
6500
|
throw new Error(`Task ${id} could not be started because it changed during claim`);
|
|
5737
6501
|
}
|
|
5738
6502
|
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
6503
|
+
const startedTask = { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, started_at: task.started_at || timestamp, version: task.version + 1, updated_at: timestamp };
|
|
6504
|
+
const payload = taskEventData(startedTask, { agent_id: agentId });
|
|
6505
|
+
dispatchWebhook2("task.started", payload, d).catch(() => {});
|
|
6506
|
+
emitLocalEventHooksQuiet({ type: "task.started", payload });
|
|
6507
|
+
emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId } });
|
|
6508
|
+
return startedTask;
|
|
5742
6509
|
}
|
|
5743
6510
|
function completeTask(id, agentId, db, options) {
|
|
5744
6511
|
const d = db || getDatabase();
|
|
@@ -5774,8 +6541,21 @@ function completeTask(id, agentId, db, options) {
|
|
|
5774
6541
|
});
|
|
5775
6542
|
tx();
|
|
5776
6543
|
logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
|
|
5777
|
-
|
|
5778
|
-
|
|
6544
|
+
const completedTaskForEvent = {
|
|
6545
|
+
...task,
|
|
6546
|
+
status: "completed",
|
|
6547
|
+
locked_by: null,
|
|
6548
|
+
locked_at: null,
|
|
6549
|
+
completed_at: timestamp,
|
|
6550
|
+
confidence,
|
|
6551
|
+
version: task.version + 1,
|
|
6552
|
+
updated_at: timestamp,
|
|
6553
|
+
metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
|
|
6554
|
+
};
|
|
6555
|
+
const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
|
|
6556
|
+
dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
|
|
6557
|
+
emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload });
|
|
6558
|
+
emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp } });
|
|
5779
6559
|
let spawnedTask = null;
|
|
5780
6560
|
if (task.recurrence_rule && !options?.skip_recurrence) {
|
|
5781
6561
|
spawnedTask = spawnNextRecurrence(task, d, timestamp);
|
|
@@ -5816,8 +6596,12 @@ function completeTask(id, agentId, db, options) {
|
|
|
5816
6596
|
if (unblockedDeps.length > 0) {
|
|
5817
6597
|
meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
|
|
5818
6598
|
for (const dep of unblockedDeps) {
|
|
5819
|
-
|
|
5820
|
-
|
|
6599
|
+
const depTask = getTask(dep.id, d);
|
|
6600
|
+
const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
|
|
6601
|
+
dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
|
|
6602
|
+
emitLocalEventHooksQuiet({ type: "task.unblocked", payload });
|
|
6603
|
+
if (depTask)
|
|
6604
|
+
emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id } });
|
|
5821
6605
|
}
|
|
5822
6606
|
}
|
|
5823
6607
|
return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
|
|
@@ -6003,9 +6787,6 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
6003
6787
|
const timestamp = now();
|
|
6004
6788
|
d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
|
|
6005
6789
|
WHERE id = ?`, [JSON.stringify(meta), timestamp, id]);
|
|
6006
|
-
logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
|
|
6007
|
-
dispatchWebhook("task.failed", { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title }, d).catch(() => {});
|
|
6008
|
-
emitLocalEventHooksQuiet({ type: "task.failed", payload: { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title } });
|
|
6009
6790
|
const failedTask = {
|
|
6010
6791
|
...task,
|
|
6011
6792
|
status: "failed",
|
|
@@ -6015,6 +6796,11 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
6015
6796
|
version: task.version + 1,
|
|
6016
6797
|
updated_at: timestamp
|
|
6017
6798
|
};
|
|
6799
|
+
logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
|
|
6800
|
+
const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
|
|
6801
|
+
dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
|
|
6802
|
+
emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload });
|
|
6803
|
+
emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning" });
|
|
6018
6804
|
let retryTask;
|
|
6019
6805
|
if (options?.retry) {
|
|
6020
6806
|
const retryCount = (task.retry_count || 0) + 1;
|
|
@@ -6089,9 +6875,12 @@ function stealTask(agentId, opts, db) {
|
|
|
6089
6875
|
return null;
|
|
6090
6876
|
logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
|
|
6091
6877
|
logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6878
|
+
const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
|
|
6879
|
+
const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
|
|
6880
|
+
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
6881
|
+
emitLocalEventHooksQuiet({ type: "task.assigned", payload });
|
|
6882
|
+
emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to } });
|
|
6883
|
+
return stolenTask;
|
|
6095
6884
|
}
|
|
6096
6885
|
function claimOrSteal(agentId, filters, db) {
|
|
6097
6886
|
const d = db || getDatabase();
|
|
@@ -6139,6 +6928,7 @@ var init_task_lifecycle = __esm(() => {
|
|
|
6139
6928
|
init_database();
|
|
6140
6929
|
init_completion_guard();
|
|
6141
6930
|
init_event_hooks();
|
|
6931
|
+
init_shared_events();
|
|
6142
6932
|
init_audit();
|
|
6143
6933
|
init_recurrence();
|
|
6144
6934
|
init_webhooks();
|
|
@@ -6826,7 +7616,7 @@ function getTaskWatchers(taskId, db) {
|
|
|
6826
7616
|
}
|
|
6827
7617
|
function notifyWatchers(taskId, event, data, db) {
|
|
6828
7618
|
const watchers = getTaskWatchers(taskId, db);
|
|
6829
|
-
|
|
7619
|
+
dispatchWebhook2(`task.watcher.${event}`, { task_id: taskId, watchers: watchers.map((w) => w.agent_id), ...data }, db).catch(() => {});
|
|
6830
7620
|
}
|
|
6831
7621
|
function logCost(taskId, tokens, usd, db) {
|
|
6832
7622
|
const d = db || getDatabase();
|
|
@@ -7320,8 +8110,8 @@ var init_boards = __esm(() => {
|
|
|
7320
8110
|
|
|
7321
8111
|
// src/lib/artifact-store.ts
|
|
7322
8112
|
import { createHash as createHash3 } from "crypto";
|
|
7323
|
-
import { existsSync as
|
|
7324
|
-
import { basename, dirname as dirname5, join as
|
|
8113
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync3, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
8114
|
+
import { basename, dirname as dirname5, join as join6, resolve as resolve6 } from "path";
|
|
7325
8115
|
import { tmpdir } from "os";
|
|
7326
8116
|
function isInMemoryDb2(path) {
|
|
7327
8117
|
return path === ":memory:" || path.startsWith("file::memory:");
|
|
@@ -7333,15 +8123,15 @@ function artifactStoreRoot() {
|
|
|
7333
8123
|
return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
|
|
7334
8124
|
const dbPath = getDatabasePath();
|
|
7335
8125
|
if (isInMemoryDb2(dbPath))
|
|
7336
|
-
return
|
|
7337
|
-
return
|
|
8126
|
+
return join6(tmpdir(), "hasna-todos-artifacts");
|
|
8127
|
+
return join6(dirname5(resolve6(dbPath)), "artifacts");
|
|
7338
8128
|
}
|
|
7339
8129
|
function artifactStorePath(relativePath) {
|
|
7340
8130
|
const normalized = relativePath.replace(/\\/g, "/");
|
|
7341
8131
|
if (normalized.includes("..") || normalized.startsWith("/") || normalized.length === 0) {
|
|
7342
8132
|
throw new Error("Invalid artifact store path");
|
|
7343
8133
|
}
|
|
7344
|
-
return
|
|
8134
|
+
return join6(artifactStoreRoot(), normalized);
|
|
7345
8135
|
}
|
|
7346
8136
|
function sha256(buffer) {
|
|
7347
8137
|
return createHash3("sha256").update(buffer).digest("hex");
|
|
@@ -7382,7 +8172,7 @@ function mediaTypeFor(path, textLike) {
|
|
|
7382
8172
|
}
|
|
7383
8173
|
function storeArtifactContent(input) {
|
|
7384
8174
|
const sourcePath = resolve6(input.path);
|
|
7385
|
-
if (!
|
|
8175
|
+
if (!existsSync7(sourcePath))
|
|
7386
8176
|
return null;
|
|
7387
8177
|
const sourceStat = statSync2(sourcePath);
|
|
7388
8178
|
if (!sourceStat.isFile())
|
|
@@ -7399,9 +8189,9 @@ function storeArtifactContent(input) {
|
|
|
7399
8189
|
redactionStatus = "redacted";
|
|
7400
8190
|
}
|
|
7401
8191
|
const storedSha = sha256(storedBuffer);
|
|
7402
|
-
const relativePath =
|
|
8192
|
+
const relativePath = join6("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
|
|
7403
8193
|
const destination = artifactStorePath(relativePath);
|
|
7404
|
-
if (!
|
|
8194
|
+
if (!existsSync7(destination)) {
|
|
7405
8195
|
mkdirSync4(dirname5(destination), { recursive: true });
|
|
7406
8196
|
writeFileSync2(destination, storedBuffer);
|
|
7407
8197
|
}
|
|
@@ -7461,7 +8251,7 @@ function verifyStoredArtifact(input) {
|
|
|
7461
8251
|
};
|
|
7462
8252
|
}
|
|
7463
8253
|
const storedPath = artifactStorePath(store.relative_path);
|
|
7464
|
-
if (!
|
|
8254
|
+
if (!existsSync7(storedPath)) {
|
|
7465
8255
|
return {
|
|
7466
8256
|
id: input.id,
|
|
7467
8257
|
path: input.path,
|
|
@@ -9488,8 +10278,8 @@ var exports_doctor = {};
|
|
|
9488
10278
|
__export(exports_doctor, {
|
|
9489
10279
|
runTodosDoctor: () => runTodosDoctor
|
|
9490
10280
|
});
|
|
9491
|
-
import { chmodSync, copyFileSync, existsSync as
|
|
9492
|
-
import { basename as basename2, dirname as dirname6, join as
|
|
10281
|
+
import { chmodSync, copyFileSync, existsSync as existsSync8, mkdirSync as mkdirSync5, statSync as statSync3 } from "fs";
|
|
10282
|
+
import { basename as basename2, dirname as dirname6, join as join7 } from "path";
|
|
9493
10283
|
function tableExists(db, table) {
|
|
9494
10284
|
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
|
9495
10285
|
}
|
|
@@ -9583,7 +10373,7 @@ function findMissingProjectRoots(db) {
|
|
|
9583
10373
|
continue;
|
|
9584
10374
|
if (!row.path.startsWith("/"))
|
|
9585
10375
|
continue;
|
|
9586
|
-
if (!
|
|
10376
|
+
if (!existsSync8(row.path))
|
|
9587
10377
|
missing++;
|
|
9588
10378
|
}
|
|
9589
10379
|
return missing;
|
|
@@ -9643,16 +10433,16 @@ function databasePermissionsAreUnsafe(dbPath) {
|
|
|
9643
10433
|
function createBackup(dbPath) {
|
|
9644
10434
|
if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
|
|
9645
10435
|
return;
|
|
9646
|
-
if (!
|
|
10436
|
+
if (!existsSync8(dbPath))
|
|
9647
10437
|
return;
|
|
9648
10438
|
const stamp = now().replace(/[:.]/g, "-");
|
|
9649
|
-
const backupDir =
|
|
10439
|
+
const backupDir = join7(dirname6(dbPath), `${basename2(dbPath)}.backup-${stamp}`);
|
|
9650
10440
|
const files = [];
|
|
9651
10441
|
mkdirSync5(backupDir, { recursive: true });
|
|
9652
10442
|
for (const source of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
9653
|
-
if (!
|
|
10443
|
+
if (!existsSync8(source))
|
|
9654
10444
|
continue;
|
|
9655
|
-
const target =
|
|
10445
|
+
const target = join7(backupDir, basename2(source));
|
|
9656
10446
|
copyFileSync(source, target);
|
|
9657
10447
|
files.push(target);
|
|
9658
10448
|
}
|
|
@@ -9878,7 +10668,7 @@ var init_doctor = __esm(() => {
|
|
|
9878
10668
|
});
|
|
9879
10669
|
|
|
9880
10670
|
// src/server/routes.ts
|
|
9881
|
-
import { join as
|
|
10671
|
+
import { join as join8, resolve as resolve7, sep } from "path";
|
|
9882
10672
|
function parseFieldsParam(url) {
|
|
9883
10673
|
const fieldsParam = url.searchParams.get("fields");
|
|
9884
10674
|
return fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
|
|
@@ -10582,7 +11372,7 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
|
|
|
10582
11372
|
if (!ctx.dashboardExists || method !== "GET" && method !== "HEAD")
|
|
10583
11373
|
return null;
|
|
10584
11374
|
if (path !== "/") {
|
|
10585
|
-
const filePath =
|
|
11375
|
+
const filePath = join8(ctx.dashboardDir, path);
|
|
10586
11376
|
const resolvedFile = resolve7(filePath);
|
|
10587
11377
|
const resolvedBase = resolve7(ctx.dashboardDir);
|
|
10588
11378
|
if (!resolvedFile.startsWith(resolvedBase + sep) && resolvedFile !== resolvedBase) {
|
|
@@ -10592,7 +11382,7 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
|
|
|
10592
11382
|
if (res2)
|
|
10593
11383
|
return res2;
|
|
10594
11384
|
}
|
|
10595
|
-
const indexPath =
|
|
11385
|
+
const indexPath = join8(ctx.dashboardDir, "index.html");
|
|
10596
11386
|
const res = serveStaticFile2(indexPath);
|
|
10597
11387
|
if (res)
|
|
10598
11388
|
return res;
|
|
@@ -13984,7 +14774,7 @@ class JSONSchemaGenerator {
|
|
|
13984
14774
|
if (val === undefined) {
|
|
13985
14775
|
if (this.unrepresentable === "throw") {
|
|
13986
14776
|
throw new Error("Literal `undefined` cannot be represented in JSON Schema");
|
|
13987
|
-
}
|
|
14777
|
+
} else {}
|
|
13988
14778
|
} else if (typeof val === "bigint") {
|
|
13989
14779
|
if (this.unrepresentable === "throw") {
|
|
13990
14780
|
throw new Error("BigInt literals cannot be represented in JSON Schema");
|
|
@@ -31037,17 +31827,73 @@ var init_dispatch_formatter = __esm(() => {
|
|
|
31037
31827
|
});
|
|
31038
31828
|
|
|
31039
31829
|
// src/lib/tmux.ts
|
|
31830
|
+
async function inspectTmuxPane(target) {
|
|
31831
|
+
const proc = Bun.spawn([
|
|
31832
|
+
"tmux",
|
|
31833
|
+
"display-message",
|
|
31834
|
+
"-p",
|
|
31835
|
+
"-t",
|
|
31836
|
+
target,
|
|
31837
|
+
"#{pane_id}\t#{pane_current_command}\t#{pane_dead}\t#{pane_input_off}\t#{pane_in_mode}"
|
|
31838
|
+
], {
|
|
31839
|
+
stdout: "pipe",
|
|
31840
|
+
stderr: "pipe"
|
|
31841
|
+
});
|
|
31842
|
+
const exitCode = await proc.exited;
|
|
31843
|
+
if (exitCode !== 0) {
|
|
31844
|
+
const stderr = await new Response(proc.stderr).text();
|
|
31845
|
+
throw new Error(`tmux target "${target}" not found: ${stderr.trim() || "unknown error"}`);
|
|
31846
|
+
}
|
|
31847
|
+
const stdout = (await new Response(proc.stdout).text()).trim();
|
|
31848
|
+
const [paneId, currentCommand = "", paneDead = "0", inputOff = "0", inMode = "0"] = stdout.split("\t");
|
|
31849
|
+
if (!paneId) {
|
|
31850
|
+
throw new Error(`tmux target "${target}" did not resolve to a pane`);
|
|
31851
|
+
}
|
|
31852
|
+
return {
|
|
31853
|
+
target,
|
|
31854
|
+
paneId,
|
|
31855
|
+
currentCommand,
|
|
31856
|
+
paneDead: paneDead === "1",
|
|
31857
|
+
inputOff: inputOff === "1",
|
|
31858
|
+
inMode: inMode === "1"
|
|
31859
|
+
};
|
|
31860
|
+
}
|
|
31861
|
+
function tmuxPaneBusyStatus(pane) {
|
|
31862
|
+
if (pane.paneDead) {
|
|
31863
|
+
return { busy: true, reason: "pane is dead" };
|
|
31864
|
+
}
|
|
31865
|
+
if (pane.inputOff) {
|
|
31866
|
+
return { busy: true, reason: "pane input is disabled" };
|
|
31867
|
+
}
|
|
31868
|
+
if (pane.inMode) {
|
|
31869
|
+
return { busy: true, reason: "pane is in copy or alternate mode" };
|
|
31870
|
+
}
|
|
31871
|
+
const currentCommand = pane.currentCommand.trim();
|
|
31872
|
+
if (currentCommand && !IDLE_TMUX_COMMANDS.has(currentCommand)) {
|
|
31873
|
+
return { busy: true, reason: `pane is running ${currentCommand}` };
|
|
31874
|
+
}
|
|
31875
|
+
return { busy: false, reason: null };
|
|
31876
|
+
}
|
|
31040
31877
|
function calculateDelay(message) {
|
|
31041
31878
|
const len = message.length;
|
|
31042
31879
|
const extra = Math.floor(len / 100 * 40);
|
|
31043
31880
|
return Math.min(DELAY_MIN + extra, DELAY_MAX);
|
|
31044
31881
|
}
|
|
31045
|
-
async function sendToTmux(target, message, delayMs,
|
|
31882
|
+
async function sendToTmux(target, message, delayMs, options = false) {
|
|
31883
|
+
const opts = typeof options === "boolean" ? { dryRun: options } : options;
|
|
31884
|
+
const dryRun = opts.dryRun ?? false;
|
|
31046
31885
|
if (dryRun) {
|
|
31047
31886
|
console.log(`[dry-run] sendToTmux target=${target} delay=${delayMs}ms`);
|
|
31048
31887
|
console.log(`[dry-run] message: ${message.slice(0, 200)}`);
|
|
31049
31888
|
return;
|
|
31050
31889
|
}
|
|
31890
|
+
if (!opts.confirmBusy) {
|
|
31891
|
+
const pane = await inspectTmuxPane(target);
|
|
31892
|
+
const status = tmuxPaneBusyStatus(pane);
|
|
31893
|
+
if (status.busy) {
|
|
31894
|
+
throw new Error(`tmux target "${target}" appears busy (${status.reason}). Re-run with --confirm-busy to send anyway.`);
|
|
31895
|
+
}
|
|
31896
|
+
}
|
|
31051
31897
|
const sendProc = Bun.spawn(["tmux", "send-keys", "-t", target, message, ""], {
|
|
31052
31898
|
stdout: "pipe",
|
|
31053
31899
|
stderr: "pipe"
|
|
@@ -31068,7 +31914,21 @@ async function sendToTmux(target, message, delayMs, dryRun = false) {
|
|
|
31068
31914
|
throw new Error(`tmux send-keys Enter failed for target "${target}": ${stderr.trim()}`);
|
|
31069
31915
|
}
|
|
31070
31916
|
}
|
|
31071
|
-
var DELAY_MIN = 3000, DELAY_MAX = 5000;
|
|
31917
|
+
var DELAY_MIN = 3000, DELAY_MAX = 5000, IDLE_TMUX_COMMANDS;
|
|
31918
|
+
var init_tmux = __esm(() => {
|
|
31919
|
+
IDLE_TMUX_COMMANDS = new Set([
|
|
31920
|
+
"bash",
|
|
31921
|
+
"dash",
|
|
31922
|
+
"elvish",
|
|
31923
|
+
"fish",
|
|
31924
|
+
"ksh",
|
|
31925
|
+
"nu",
|
|
31926
|
+
"pwsh",
|
|
31927
|
+
"sh",
|
|
31928
|
+
"tmux",
|
|
31929
|
+
"zsh"
|
|
31930
|
+
]);
|
|
31931
|
+
});
|
|
31072
31932
|
|
|
31073
31933
|
// src/lib/dispatch.ts
|
|
31074
31934
|
var exports_dispatch = {};
|
|
@@ -31086,7 +31946,10 @@ async function executeDispatch(dispatch, opts = {}, db) {
|
|
|
31086
31946
|
}
|
|
31087
31947
|
const delayMs = dispatch.delay_ms ?? calculateDelay(message);
|
|
31088
31948
|
try {
|
|
31089
|
-
await sendToTmux(dispatch.target_window, message, delayMs,
|
|
31949
|
+
await sendToTmux(dispatch.target_window, message, delayMs, {
|
|
31950
|
+
dryRun: opts.dryRun ?? false,
|
|
31951
|
+
confirmBusy: opts.confirmBusy ?? false
|
|
31952
|
+
});
|
|
31090
31953
|
createDispatchLog({
|
|
31091
31954
|
dispatch_id: dispatch.id,
|
|
31092
31955
|
target_window: dispatch.target_window,
|
|
@@ -31145,90 +32008,7 @@ var init_dispatch = __esm(() => {
|
|
|
31145
32008
|
init_tasks();
|
|
31146
32009
|
init_database();
|
|
31147
32010
|
init_dispatch_formatter();
|
|
31148
|
-
|
|
31149
|
-
|
|
31150
|
-
// src/db/task-lists.ts
|
|
31151
|
-
function rowToTaskList(row) {
|
|
31152
|
-
return {
|
|
31153
|
-
...row,
|
|
31154
|
-
metadata: JSON.parse(row.metadata || "{}")
|
|
31155
|
-
};
|
|
31156
|
-
}
|
|
31157
|
-
function createTaskList(input, db) {
|
|
31158
|
-
const d = db || getDatabase();
|
|
31159
|
-
const id = uuid();
|
|
31160
|
-
const timestamp = now();
|
|
31161
|
-
const slug = input.slug || slugify(input.name);
|
|
31162
|
-
if (!input.project_id) {
|
|
31163
|
-
const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
|
|
31164
|
-
if (existing) {
|
|
31165
|
-
throw new Error(`Standalone task list with slug "${slug}" already exists`);
|
|
31166
|
-
}
|
|
31167
|
-
}
|
|
31168
|
-
d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
|
|
31169
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
|
|
31170
|
-
return getTaskList(id, d);
|
|
31171
|
-
}
|
|
31172
|
-
function getTaskList(id, db) {
|
|
31173
|
-
const d = db || getDatabase();
|
|
31174
|
-
const row = d.query("SELECT * FROM task_lists WHERE id = ?").get(id);
|
|
31175
|
-
return row ? rowToTaskList(row) : null;
|
|
31176
|
-
}
|
|
31177
|
-
function getTaskListBySlug(slug, projectId, db) {
|
|
31178
|
-
const d = db || getDatabase();
|
|
31179
|
-
let row;
|
|
31180
|
-
if (projectId) {
|
|
31181
|
-
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id = ?").get(slug, projectId);
|
|
31182
|
-
} else {
|
|
31183
|
-
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id IS NULL").get(slug);
|
|
31184
|
-
}
|
|
31185
|
-
return row ? rowToTaskList(row) : null;
|
|
31186
|
-
}
|
|
31187
|
-
function listTaskLists(projectId, db) {
|
|
31188
|
-
const d = db || getDatabase();
|
|
31189
|
-
if (projectId) {
|
|
31190
|
-
return d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(projectId).map(rowToTaskList);
|
|
31191
|
-
}
|
|
31192
|
-
return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
|
|
31193
|
-
}
|
|
31194
|
-
function updateTaskList(id, input, db) {
|
|
31195
|
-
const d = db || getDatabase();
|
|
31196
|
-
const existing = getTaskList(id, d);
|
|
31197
|
-
if (!existing)
|
|
31198
|
-
throw new TaskListNotFoundError(id);
|
|
31199
|
-
const sets = ["updated_at = ?"];
|
|
31200
|
-
const params = [now()];
|
|
31201
|
-
if (input.name !== undefined) {
|
|
31202
|
-
sets.push("name = ?");
|
|
31203
|
-
params.push(input.name);
|
|
31204
|
-
}
|
|
31205
|
-
if (input.description !== undefined) {
|
|
31206
|
-
sets.push("description = ?");
|
|
31207
|
-
params.push(input.description);
|
|
31208
|
-
}
|
|
31209
|
-
if (input.metadata !== undefined) {
|
|
31210
|
-
sets.push("metadata = ?");
|
|
31211
|
-
params.push(JSON.stringify(input.metadata));
|
|
31212
|
-
}
|
|
31213
|
-
params.push(id);
|
|
31214
|
-
d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
31215
|
-
return getTaskList(id, d);
|
|
31216
|
-
}
|
|
31217
|
-
function deleteTaskList(id, db) {
|
|
31218
|
-
const d = db || getDatabase();
|
|
31219
|
-
return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
31220
|
-
}
|
|
31221
|
-
function ensureTaskList(name, slug, projectId, db) {
|
|
31222
|
-
const d = db || getDatabase();
|
|
31223
|
-
const existing = getTaskListBySlug(slug, projectId, d);
|
|
31224
|
-
if (existing)
|
|
31225
|
-
return existing;
|
|
31226
|
-
return createTaskList({ name, slug, project_id: projectId }, d);
|
|
31227
|
-
}
|
|
31228
|
-
var init_task_lists = __esm(() => {
|
|
31229
|
-
init_types();
|
|
31230
|
-
init_database();
|
|
31231
|
-
init_projects();
|
|
32011
|
+
init_tmux();
|
|
31232
32012
|
});
|
|
31233
32013
|
|
|
31234
32014
|
// src/mcp/tools/dispatch.ts
|
|
@@ -31239,8 +32019,9 @@ function registerDispatchTools(server, { shouldRegisterTool, resolveId, formatEr
|
|
|
31239
32019
|
target: exports_external.string().describe("tmux target \u2014 window name, session:window, or session:window.pane"),
|
|
31240
32020
|
delay_ms: exports_external.number().optional().describe("Delay in ms between sending the message and hitting Enter. Auto-calculated from message length (3-5s) if omitted."),
|
|
31241
32021
|
scheduled_at: exports_external.string().optional().describe("ISO datetime to schedule the dispatch for. Fires immediately if omitted."),
|
|
32022
|
+
confirm_busy: exports_external.boolean().optional().describe("Send even if the target tmux pane appears busy. Default: false."),
|
|
31242
32023
|
dry_run: exports_external.boolean().optional().describe("Preview the formatted message without sending. Default: false.")
|
|
31243
|
-
}, async ({ task_ids, target, delay_ms, scheduled_at, dry_run }) => {
|
|
32024
|
+
}, async ({ task_ids, target, delay_ms, scheduled_at, confirm_busy, dry_run }) => {
|
|
31244
32025
|
try {
|
|
31245
32026
|
const db = getDatabase();
|
|
31246
32027
|
const resolvedIds = task_ids.map((id) => resolveId(id));
|
|
@@ -31256,7 +32037,7 @@ ${message}` }]
|
|
|
31256
32037
|
}
|
|
31257
32038
|
const dispatch = createDispatch({ task_ids: resolvedIds, target_window: target, message, delay_ms: effectiveDelay, scheduled_at }, db);
|
|
31258
32039
|
if (!scheduled_at)
|
|
31259
|
-
await executeDispatch(dispatch, {}, db);
|
|
32040
|
+
await executeDispatch(dispatch, { confirmBusy: confirm_busy ?? false }, db);
|
|
31260
32041
|
return {
|
|
31261
32042
|
content: [{
|
|
31262
32043
|
type: "text",
|
|
@@ -31282,8 +32063,9 @@ ${message}`
|
|
|
31282
32063
|
filter_status: exports_external.array(exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"])).optional().describe("Only include tasks with these statuses. Default: pending."),
|
|
31283
32064
|
delay_ms: exports_external.number().optional().describe("Delay in ms between sending and Enter. Auto-calculated if omitted."),
|
|
31284
32065
|
scheduled_at: exports_external.string().optional().describe("ISO datetime to schedule. Fires immediately if omitted."),
|
|
32066
|
+
confirm_busy: exports_external.boolean().optional().describe("Send even if the target tmux pane appears busy. Default: false."),
|
|
31285
32067
|
dry_run: exports_external.boolean().optional().describe("Preview without sending. Default: false.")
|
|
31286
|
-
}, async ({ task_list_id, target, filter_status, delay_ms, scheduled_at, dry_run }) => {
|
|
32068
|
+
}, async ({ task_list_id, target, filter_status, delay_ms, scheduled_at, confirm_busy, dry_run }) => {
|
|
31287
32069
|
try {
|
|
31288
32070
|
const db = getDatabase();
|
|
31289
32071
|
const resolvedListId = resolveId(task_list_id, "task_lists");
|
|
@@ -31303,7 +32085,7 @@ ${message}` }]
|
|
|
31303
32085
|
}
|
|
31304
32086
|
const dispatch = createDispatch({ title: `Task list: ${taskList.name}`, task_list_id: resolvedListId, target_window: target, message, delay_ms: effectiveDelay, scheduled_at }, db);
|
|
31305
32087
|
if (!scheduled_at)
|
|
31306
|
-
await executeDispatch(dispatch, {}, db);
|
|
32088
|
+
await executeDispatch(dispatch, { confirmBusy: confirm_busy ?? false }, db);
|
|
31307
32089
|
return {
|
|
31308
32090
|
content: [{
|
|
31309
32091
|
type: "text",
|
|
@@ -31330,15 +32112,16 @@ ${message}`
|
|
|
31330
32112
|
task_list_id: exports_external.string().optional().describe("Task list ID to dispatch (use this or task_ids)"),
|
|
31331
32113
|
stagger_ms: exports_external.number().optional().describe("Delay between each window dispatch. Default: 500ms."),
|
|
31332
32114
|
delay_ms: exports_external.number().optional().describe("Delay between message send and Enter. Auto-calculated if omitted."),
|
|
32115
|
+
confirm_busy: exports_external.boolean().optional().describe("Send even if target tmux panes appear busy. Default: false."),
|
|
31333
32116
|
dry_run: exports_external.boolean().optional().describe("Preview without sending. Default: false.")
|
|
31334
|
-
}, async ({ targets, task_ids, task_list_id, stagger_ms, delay_ms, dry_run }) => {
|
|
32117
|
+
}, async ({ targets, task_ids, task_list_id, stagger_ms, delay_ms, confirm_busy, dry_run }) => {
|
|
31335
32118
|
try {
|
|
31336
32119
|
if (!task_ids && !task_list_id)
|
|
31337
32120
|
throw new Error("Either task_ids or task_list_id is required");
|
|
31338
32121
|
const db = getDatabase();
|
|
31339
32122
|
const resolvedTaskIds = task_ids ? task_ids.map((id) => resolveId(id)) : undefined;
|
|
31340
32123
|
const resolvedListId = task_list_id ? resolveId(task_list_id, "task_lists") : undefined;
|
|
31341
|
-
const dispatches = await dispatchToMultiple({ targets, task_ids: resolvedTaskIds, task_list_id: resolvedListId, delay_ms, stagger_ms }, { dryRun: dry_run }, db);
|
|
32124
|
+
const dispatches = await dispatchToMultiple({ targets, task_ids: resolvedTaskIds, task_list_id: resolvedListId, delay_ms, stagger_ms }, { dryRun: dry_run, confirmBusy: confirm_busy ?? false }, db);
|
|
31342
32125
|
const lines = dispatches.map((d) => `${d.target_window}: ${d.id} [${d.status}]`);
|
|
31343
32126
|
return { content: [{ type: "text", text: `Dispatched to ${dispatches.length} target(s):
|
|
31344
32127
|
${lines.join(`
|
|
@@ -31383,11 +32166,12 @@ ${lines.join(`
|
|
|
31383
32166
|
if (shouldRegisterTool("run_due_dispatches")) {
|
|
31384
32167
|
server.tool("run_due_dispatches", "Manually trigger all pending dispatches that are due (scheduled_at <= now). Returns the count fired.", {
|
|
31385
32168
|
dry_run: exports_external.boolean().optional().describe("Preview without sending. Default: false."),
|
|
32169
|
+
confirm_busy: exports_external.boolean().optional().describe("Send even if target tmux panes appear busy. Default: false."),
|
|
31386
32170
|
all: exports_external.boolean().optional().describe("Ignore scheduled_at and fire all pending dispatches immediately.")
|
|
31387
|
-
}, async ({ dry_run }) => {
|
|
32171
|
+
}, async ({ dry_run, confirm_busy }) => {
|
|
31388
32172
|
try {
|
|
31389
32173
|
const { runDueDispatches: runDueDispatches2 } = await Promise.resolve().then(() => (init_dispatch(), exports_dispatch));
|
|
31390
|
-
const count = await runDueDispatches2({ dryRun: dry_run });
|
|
32174
|
+
const count = await runDueDispatches2({ dryRun: dry_run, confirmBusy: confirm_busy ?? false });
|
|
31391
32175
|
return { content: [{ type: "text", text: `Fired ${count} dispatch(es).` }] };
|
|
31392
32176
|
} catch (e) {
|
|
31393
32177
|
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
@@ -31400,6 +32184,7 @@ var init_dispatch2 = __esm(() => {
|
|
|
31400
32184
|
init_dispatches();
|
|
31401
32185
|
init_dispatch();
|
|
31402
32186
|
init_dispatch_formatter();
|
|
32187
|
+
init_tmux();
|
|
31403
32188
|
init_tasks();
|
|
31404
32189
|
init_task_lists();
|
|
31405
32190
|
init_database();
|
|
@@ -32207,7 +32992,7 @@ var init_task_crud2 = __esm(() => {
|
|
|
32207
32992
|
});
|
|
32208
32993
|
|
|
32209
32994
|
// src/lib/project-bootstrap.ts
|
|
32210
|
-
import { existsSync as
|
|
32995
|
+
import { existsSync as existsSync9, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
|
|
32211
32996
|
import { basename as basename3, dirname as dirname7, resolve as resolve8 } from "path";
|
|
32212
32997
|
function safeStat(path) {
|
|
32213
32998
|
try {
|
|
@@ -32226,7 +33011,7 @@ function canonicalPath(input) {
|
|
|
32226
33011
|
function findUp(start, marker) {
|
|
32227
33012
|
let current = canonicalPath(start);
|
|
32228
33013
|
while (true) {
|
|
32229
|
-
if (
|
|
33014
|
+
if (existsSync9(resolve8(current, marker)))
|
|
32230
33015
|
return current;
|
|
32231
33016
|
const parent = dirname7(current);
|
|
32232
33017
|
if (parent === current)
|
|
@@ -32238,7 +33023,7 @@ function readPackageJson(path) {
|
|
|
32238
33023
|
if (!path)
|
|
32239
33024
|
return null;
|
|
32240
33025
|
const file = resolve8(path, "package.json");
|
|
32241
|
-
if (!
|
|
33026
|
+
if (!existsSync9(file))
|
|
32242
33027
|
return null;
|
|
32243
33028
|
try {
|
|
32244
33029
|
const parsed = JSON.parse(readFileSync4(file, "utf-8"));
|
|
@@ -32260,7 +33045,7 @@ function workspaceMarker(root, rootPackage) {
|
|
|
32260
33045
|
if (rootPackage?.workspaces)
|
|
32261
33046
|
markers.push("package.json#workspaces");
|
|
32262
33047
|
for (const marker of ["pnpm-workspace.yaml", "turbo.json", "nx.json", "lerna.json", "rush.json", "bun.lock", "bun.lockb"]) {
|
|
32263
|
-
if (
|
|
33048
|
+
if (existsSync9(resolve8(root, marker)))
|
|
32264
33049
|
markers.push(marker);
|
|
32265
33050
|
}
|
|
32266
33051
|
const kind = markers.find((marker) => marker !== "bun.lock" && marker !== "bun.lockb") ?? null;
|
|
@@ -32561,7 +33346,7 @@ var init_tags = __esm(() => {
|
|
|
32561
33346
|
});
|
|
32562
33347
|
|
|
32563
33348
|
// src/lib/retention-cleanup.ts
|
|
32564
|
-
import { existsSync as
|
|
33349
|
+
import { existsSync as existsSync10, unlinkSync } from "fs";
|
|
32565
33350
|
function normalizeScopes(scopes) {
|
|
32566
33351
|
if (!scopes || scopes.length === 0)
|
|
32567
33352
|
return [...ALL_SCOPES];
|
|
@@ -32764,7 +33549,7 @@ function applyRetentionCleanup(input, db) {
|
|
|
32764
33549
|
for (const artifact of report.candidates.artifact_files) {
|
|
32765
33550
|
try {
|
|
32766
33551
|
const path = artifactStorePath(artifact.relative_path);
|
|
32767
|
-
if (!
|
|
33552
|
+
if (!existsSync10(path)) {
|
|
32768
33553
|
report.warnings.push(`stored artifact already missing: ${artifact.relative_path}`);
|
|
32769
33554
|
continue;
|
|
32770
33555
|
}
|
|
@@ -32791,8 +33576,8 @@ var init_retention_cleanup = __esm(() => {
|
|
|
32791
33576
|
});
|
|
32792
33577
|
|
|
32793
33578
|
// src/lib/mention-resolver.ts
|
|
32794
|
-
import { existsSync as
|
|
32795
|
-
import { basename as basename4, isAbsolute, join as
|
|
33579
|
+
import { existsSync as existsSync11, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync5 } from "fs";
|
|
33580
|
+
import { basename as basename4, isAbsolute, join as join9, relative as relative3, resolve as resolve9, sep as sep2 } from "path";
|
|
32796
33581
|
function blankResolution(parsed) {
|
|
32797
33582
|
return {
|
|
32798
33583
|
input: parsed.input,
|
|
@@ -32890,7 +33675,7 @@ function resolveFile(parsed, workspace) {
|
|
|
32890
33675
|
return resolution;
|
|
32891
33676
|
}
|
|
32892
33677
|
resolution.path = relPath;
|
|
32893
|
-
if (!
|
|
33678
|
+
if (!existsSync11(absolutePath)) {
|
|
32894
33679
|
resolution.warnings.push("file does not exist in the local workspace");
|
|
32895
33680
|
return resolution;
|
|
32896
33681
|
}
|
|
@@ -32923,7 +33708,7 @@ function walkSourceFiles(root, current = root, files = []) {
|
|
|
32923
33708
|
if (SKIP_DIRS.has(entry.name))
|
|
32924
33709
|
continue;
|
|
32925
33710
|
}
|
|
32926
|
-
const absolutePath =
|
|
33711
|
+
const absolutePath = join9(current, entry.name);
|
|
32927
33712
|
if (entry.isDirectory()) {
|
|
32928
33713
|
if (!SKIP_DIRS.has(entry.name))
|
|
32929
33714
|
walkSourceFiles(root, absolutePath, files);
|
|
@@ -34190,8 +34975,8 @@ var init_local_notifications = __esm(() => {
|
|
|
34190
34975
|
});
|
|
34191
34976
|
|
|
34192
34977
|
// src/lib/local-encryption.ts
|
|
34193
|
-
import { createCipheriv, createDecipheriv, createHash as createHash4, randomBytes as randomBytes2, scryptSync, timingSafeEqual as
|
|
34194
|
-
function
|
|
34978
|
+
import { createCipheriv, createDecipheriv, createHash as createHash4, randomBytes as randomBytes2, scryptSync, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
34979
|
+
function now3() {
|
|
34195
34980
|
return new Date().toISOString();
|
|
34196
34981
|
}
|
|
34197
34982
|
function sha2562(value) {
|
|
@@ -34228,7 +35013,7 @@ function upsertEncryptionProfile(input) {
|
|
|
34228
35013
|
const name = normalizeProfileName(input.name);
|
|
34229
35014
|
const config2 = loadConfig();
|
|
34230
35015
|
const existing = config2.encryption_profiles?.[name];
|
|
34231
|
-
const timestamp =
|
|
35016
|
+
const timestamp = now3();
|
|
34232
35017
|
const profile = {
|
|
34233
35018
|
name,
|
|
34234
35019
|
algorithm: "aes-256-gcm",
|
|
@@ -34285,7 +35070,7 @@ function encryptString(plaintext, options = {}) {
|
|
|
34285
35070
|
return {
|
|
34286
35071
|
schemaVersion: TODOS_ENCRYPTION_SCHEMA_VERSION,
|
|
34287
35072
|
kind: TODOS_ENCRYPTED_VALUE_KIND,
|
|
34288
|
-
encryptedAt: options.encryptedAt ??
|
|
35073
|
+
encryptedAt: options.encryptedAt ?? now3(),
|
|
34289
35074
|
profile: profile.name,
|
|
34290
35075
|
key_env: profile.key_env,
|
|
34291
35076
|
algorithm: "aes-256-gcm",
|
|
@@ -34320,7 +35105,7 @@ function decryptString(envelope, env = process.env) {
|
|
|
34320
35105
|
]).toString("utf8");
|
|
34321
35106
|
const expected = Buffer.from(envelope.plaintext_sha256, "hex");
|
|
34322
35107
|
const actual = Buffer.from(sha2562(plaintext), "hex");
|
|
34323
|
-
if (expected.length !== actual.length || !
|
|
35108
|
+
if (expected.length !== actual.length || !timingSafeEqual3(expected, actual)) {
|
|
34324
35109
|
throw new EncryptedPayloadError("decrypted payload checksum mismatch");
|
|
34325
35110
|
}
|
|
34326
35111
|
return plaintext;
|
|
@@ -35843,7 +36628,7 @@ function createRoadmap(input) {
|
|
|
35843
36628
|
if (!name)
|
|
35844
36629
|
throw new Error("Roadmap name is required");
|
|
35845
36630
|
const store = readStore();
|
|
35846
|
-
const
|
|
36631
|
+
const now4 = timestamp();
|
|
35847
36632
|
const roadmap = {
|
|
35848
36633
|
id: newId("roadmap"),
|
|
35849
36634
|
name,
|
|
@@ -35854,8 +36639,8 @@ function createRoadmap(input) {
|
|
|
35854
36639
|
agent_id: cleanString(input.agent_id),
|
|
35855
36640
|
release: cleanString(input.release),
|
|
35856
36641
|
milestone_ids: [],
|
|
35857
|
-
created_at:
|
|
35858
|
-
updated_at:
|
|
36642
|
+
created_at: now4,
|
|
36643
|
+
updated_at: now4
|
|
35859
36644
|
};
|
|
35860
36645
|
store.roadmaps[roadmap.id] = roadmap;
|
|
35861
36646
|
writeStore(store);
|
|
@@ -35907,7 +36692,7 @@ function createMilestone(input) {
|
|
|
35907
36692
|
const title = input.title.trim();
|
|
35908
36693
|
if (!title)
|
|
35909
36694
|
throw new Error("Milestone title is required");
|
|
35910
|
-
const
|
|
36695
|
+
const now4 = timestamp();
|
|
35911
36696
|
const milestone = {
|
|
35912
36697
|
id: newId("milestone"),
|
|
35913
36698
|
roadmap_id: roadmapId,
|
|
@@ -35922,11 +36707,11 @@ function createMilestone(input) {
|
|
|
35922
36707
|
run_ids: cleanList(input.run_ids),
|
|
35923
36708
|
release: cleanString(input.release ?? roadmap.release ?? undefined),
|
|
35924
36709
|
tags: cleanList(input.tags),
|
|
35925
|
-
created_at:
|
|
35926
|
-
updated_at:
|
|
36710
|
+
created_at: now4,
|
|
36711
|
+
updated_at: now4
|
|
35927
36712
|
};
|
|
35928
36713
|
store.milestones[milestone.id] = milestone;
|
|
35929
|
-
store.roadmaps[roadmapId] = { ...roadmap, milestone_ids: cleanList([...roadmap.milestone_ids, milestone.id]), updated_at:
|
|
36714
|
+
store.roadmaps[roadmapId] = { ...roadmap, milestone_ids: cleanList([...roadmap.milestone_ids, milestone.id]), updated_at: now4 };
|
|
35930
36715
|
writeStore(store);
|
|
35931
36716
|
return milestone;
|
|
35932
36717
|
}
|
|
@@ -35981,7 +36766,7 @@ function upsertReleaseGroup(input) {
|
|
|
35981
36766
|
throw new Error("Release group name is required");
|
|
35982
36767
|
const key = releaseKey(roadmapId, name);
|
|
35983
36768
|
const existing = store.releases[key];
|
|
35984
|
-
const
|
|
36769
|
+
const now4 = timestamp();
|
|
35985
36770
|
const release = {
|
|
35986
36771
|
name,
|
|
35987
36772
|
version: input.version === undefined ? existing?.version ?? null : cleanString(input.version),
|
|
@@ -35992,8 +36777,8 @@ function upsertReleaseGroup(input) {
|
|
|
35992
36777
|
plan_ids: input.plan_ids === undefined ? existing?.plan_ids ?? [] : cleanList(input.plan_ids),
|
|
35993
36778
|
run_ids: input.run_ids === undefined ? existing?.run_ids ?? [] : cleanList(input.run_ids),
|
|
35994
36779
|
notes: input.notes === undefined ? existing?.notes ?? null : cleanString(input.notes),
|
|
35995
|
-
created_at: existing?.created_at ??
|
|
35996
|
-
updated_at:
|
|
36780
|
+
created_at: existing?.created_at ?? now4,
|
|
36781
|
+
updated_at: now4
|
|
35997
36782
|
};
|
|
35998
36783
|
store.releases[key] = release;
|
|
35999
36784
|
writeStore(store);
|
|
@@ -36192,7 +36977,7 @@ function upsertCapacityProfile(input) {
|
|
|
36192
36977
|
const store = readStore2();
|
|
36193
36978
|
const key = profileKey(agentId, projectId);
|
|
36194
36979
|
const existing = store.profiles[key];
|
|
36195
|
-
const
|
|
36980
|
+
const now4 = timestamp2();
|
|
36196
36981
|
const profile = {
|
|
36197
36982
|
id: existing?.id ?? key,
|
|
36198
36983
|
agent_id: agentId,
|
|
@@ -36200,8 +36985,8 @@ function upsertCapacityProfile(input) {
|
|
|
36200
36985
|
minutes_per_day: assertMinutes(input.minutes_per_day),
|
|
36201
36986
|
working_days: normalizeWorkingDays(input.working_days),
|
|
36202
36987
|
effective_from: cleanString2(input.effective_from),
|
|
36203
|
-
created_at: existing?.created_at ??
|
|
36204
|
-
updated_at:
|
|
36988
|
+
created_at: existing?.created_at ?? now4,
|
|
36989
|
+
updated_at: now4
|
|
36205
36990
|
};
|
|
36206
36991
|
store.profiles[key] = profile;
|
|
36207
36992
|
writeStore2(store);
|
|
@@ -36610,7 +37395,7 @@ var init_audit_ledger = __esm(() => {
|
|
|
36610
37395
|
|
|
36611
37396
|
// src/lib/release-compatibility.ts
|
|
36612
37397
|
import { readFileSync as readFileSync6 } from "fs";
|
|
36613
|
-
import { join as
|
|
37398
|
+
import { join as join10, resolve as resolve11 } from "path";
|
|
36614
37399
|
import { Database as Database2 } from "bun:sqlite";
|
|
36615
37400
|
function pass(id, message, details) {
|
|
36616
37401
|
return { id, status: "passed", message, details };
|
|
@@ -36622,7 +37407,7 @@ function warn(id, message, details) {
|
|
|
36622
37407
|
return { id, status: "warning", message, details };
|
|
36623
37408
|
}
|
|
36624
37409
|
function readPackageJson2(root) {
|
|
36625
|
-
return JSON.parse(readFileSync6(
|
|
37410
|
+
return JSON.parse(readFileSync6(join10(root, "package.json"), "utf8"));
|
|
36626
37411
|
}
|
|
36627
37412
|
function sortedKeys(value) {
|
|
36628
37413
|
return Object.keys(value ?? {}).sort((left, right) => left.localeCompare(right));
|
|
@@ -36891,6 +37676,9 @@ function hasFts(db) {
|
|
|
36891
37676
|
function escapeFtsQuery(q) {
|
|
36892
37677
|
return q.replace(/["*^()]/g, " ").trim().split(/\s+/).filter(Boolean).map((token) => `"${token}"*`).join(" ");
|
|
36893
37678
|
}
|
|
37679
|
+
function shouldUseFts(q) {
|
|
37680
|
+
return /^[\p{L}\p{N}_\-\s]+$/u.test(q);
|
|
37681
|
+
}
|
|
36894
37682
|
function searchTasks(options, projectId, taskListId, db) {
|
|
36895
37683
|
const opts = typeof options === "string" ? { query: options || undefined, project_id: projectId, task_list_id: taskListId } : options;
|
|
36896
37684
|
const d = db || getDatabase();
|
|
@@ -36899,7 +37687,8 @@ function searchTasks(options, projectId, taskListId, db) {
|
|
|
36899
37687
|
let sql;
|
|
36900
37688
|
const raw = opts.query?.trim() ?? "";
|
|
36901
37689
|
const q = raw === "*" ? "" : raw;
|
|
36902
|
-
|
|
37690
|
+
const useFts = hasFts(d) && q && shouldUseFts(q);
|
|
37691
|
+
if (useFts) {
|
|
36903
37692
|
const ftsQuery = escapeFtsQuery(q);
|
|
36904
37693
|
sql = `SELECT t.* FROM tasks t
|
|
36905
37694
|
INNER JOIN tasks_fts fts ON fts.rowid = t.rowid
|
|
@@ -36907,8 +37696,16 @@ function searchTasks(options, projectId, taskListId, db) {
|
|
|
36907
37696
|
params.push(ftsQuery);
|
|
36908
37697
|
} else if (q) {
|
|
36909
37698
|
const pattern = `%${q}%`;
|
|
36910
|
-
sql = `SELECT * FROM tasks t WHERE (
|
|
36911
|
-
|
|
37699
|
+
sql = `SELECT * FROM tasks t WHERE (
|
|
37700
|
+
t.id LIKE ?
|
|
37701
|
+
OR t.short_id LIKE ?
|
|
37702
|
+
OR t.title LIKE ?
|
|
37703
|
+
OR t.description LIKE ?
|
|
37704
|
+
OR t.working_dir LIKE ?
|
|
37705
|
+
OR t.metadata LIKE ?
|
|
37706
|
+
OR EXISTS (SELECT 1 FROM task_tags WHERE task_tags.task_id = t.id AND tag LIKE ?)
|
|
37707
|
+
)`;
|
|
37708
|
+
params.push(pattern, pattern, pattern, pattern, pattern, pattern, pattern);
|
|
36912
37709
|
} else {
|
|
36913
37710
|
sql = `SELECT * FROM tasks t WHERE 1=1`;
|
|
36914
37711
|
}
|
|
@@ -36964,7 +37761,7 @@ function searchTasks(options, projectId, taskListId, db) {
|
|
|
36964
37761
|
} else if (opts.is_blocked === false) {
|
|
36965
37762
|
sql += " AND t.id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed')";
|
|
36966
37763
|
}
|
|
36967
|
-
if (
|
|
37764
|
+
if (useFts) {
|
|
36968
37765
|
sql += ` ORDER BY bm25(tasks_fts),
|
|
36969
37766
|
CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
|
|
36970
37767
|
t.created_at DESC`;
|
|
@@ -40059,7 +40856,7 @@ function registerTaskWorkflowTools(server, ctx) {
|
|
|
40059
40856
|
const globalRole = n.agent.role ? ` [${n.agent.role}]` : "";
|
|
40060
40857
|
const lead = n.is_lead ? " \u2605" : "";
|
|
40061
40858
|
const lastSeen = new Date(n.agent.last_seen_at).getTime();
|
|
40062
|
-
const active =
|
|
40859
|
+
const active = now4 - lastSeen < ACTIVE_MS ? " \u25CF" : " \u25CB";
|
|
40063
40860
|
const line = `${prefix}${active} ${n.agent.name}${title}${globalRole}${lead}`;
|
|
40064
40861
|
const children = n.reports.length > 0 ? `
|
|
40065
40862
|
` + render(n.reports, indent + 1) : "";
|
|
@@ -40072,7 +40869,7 @@ function registerTaskWorkflowTools(server, ctx) {
|
|
|
40072
40869
|
if (format === "json") {
|
|
40073
40870
|
return { content: [{ type: "text", text: JSON.stringify(tree, null, 2) }] };
|
|
40074
40871
|
}
|
|
40075
|
-
const
|
|
40872
|
+
const now4 = Date.now();
|
|
40076
40873
|
const ACTIVE_MS = 30 * 60 * 1000;
|
|
40077
40874
|
const text = tree.length > 0 ? render(tree) : "No agents in org chart.";
|
|
40078
40875
|
return { content: [{ type: "text", text }] };
|
|
@@ -40268,14 +41065,14 @@ function registerTaskAutoTools(server, ctx) {
|
|
|
40268
41065
|
return { content: [{ type: "text", text: "No agent_id provided and no agent focus is active." }], isError: true };
|
|
40269
41066
|
}
|
|
40270
41067
|
const assigned = listTasks3({ assigned_to: effectiveAgentId, limit: 500 }, undefined);
|
|
40271
|
-
const
|
|
40272
|
-
const dueSoonCutoff =
|
|
41068
|
+
const now4 = Date.now();
|
|
41069
|
+
const dueSoonCutoff = now4 + 24 * 60 * 60 * 1000;
|
|
40273
41070
|
const blocked = getBlockedTasks2().filter((t) => t.assigned_to === effectiveAgentId);
|
|
40274
41071
|
const workload = {
|
|
40275
41072
|
in_progress: assigned.filter((t) => t.status === "in_progress").length,
|
|
40276
41073
|
pending: assigned.filter((t) => t.status === "pending").length,
|
|
40277
|
-
completed_recent: assigned.filter((t) => t.status === "completed" && t.completed_at &&
|
|
40278
|
-
due_soon: assigned.filter((t) => t.due_at && new Date(t.due_at).getTime() <= dueSoonCutoff && new Date(t.due_at).getTime() >=
|
|
41074
|
+
completed_recent: assigned.filter((t) => t.status === "completed" && t.completed_at && now4 - new Date(t.completed_at).getTime() <= 7 * 24 * 60 * 60 * 1000).length,
|
|
41075
|
+
due_soon: assigned.filter((t) => t.due_at && new Date(t.due_at).getTime() <= dueSoonCutoff && new Date(t.due_at).getTime() >= now4 && !["completed", "cancelled", "failed"].includes(t.status)).length,
|
|
40279
41076
|
blocked: blocked.length
|
|
40280
41077
|
};
|
|
40281
41078
|
const lines = [
|
|
@@ -40502,7 +41299,7 @@ function limits(input) {
|
|
|
40502
41299
|
stale_after_hours: clamp(input.stale_after_hours, DEFAULT_LIMITS.stale_after_hours, 24 * 365)
|
|
40503
41300
|
};
|
|
40504
41301
|
}
|
|
40505
|
-
function
|
|
41302
|
+
function truncate2(value, max) {
|
|
40506
41303
|
if (!value)
|
|
40507
41304
|
return value ?? null;
|
|
40508
41305
|
const redacted = redactEvidenceText(value);
|
|
@@ -40523,9 +41320,9 @@ function acceptanceCriteria(task, maxText) {
|
|
|
40523
41320
|
const metadata = task.metadata || {};
|
|
40524
41321
|
const raw = metadata["acceptance_criteria"] ?? metadata["acceptanceCriteria"] ?? metadata["criteria"];
|
|
40525
41322
|
if (Array.isArray(raw))
|
|
40526
|
-
return raw.map((item) =>
|
|
41323
|
+
return raw.map((item) => truncate2(String(item), maxText)).filter((item) => Boolean(item));
|
|
40527
41324
|
if (typeof raw === "string") {
|
|
40528
|
-
return raw.split(/\r?\n/).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).map((line) =>
|
|
41325
|
+
return raw.split(/\r?\n/).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).map((line) => truncate2(line, maxText)).filter((item) => Boolean(item));
|
|
40529
41326
|
}
|
|
40530
41327
|
return [];
|
|
40531
41328
|
}
|
|
@@ -40548,7 +41345,7 @@ function addFile(files, path, source, base) {
|
|
|
40548
41345
|
path,
|
|
40549
41346
|
status: base?.status || "active",
|
|
40550
41347
|
agent_id: base?.agent_id ?? null,
|
|
40551
|
-
note:
|
|
41348
|
+
note: truncate2(base?.note, 240),
|
|
40552
41349
|
updated_at: base?.updated_at || "",
|
|
40553
41350
|
sources: [source]
|
|
40554
41351
|
});
|
|
@@ -40606,7 +41403,7 @@ function estimateTokens(value) {
|
|
|
40606
41403
|
return Math.max(1, Math.ceil((text || "").length / 4));
|
|
40607
41404
|
}
|
|
40608
41405
|
function summarizeStrings(values, maxChars) {
|
|
40609
|
-
return
|
|
41406
|
+
return truncate2(values.filter(Boolean).join("; "), maxChars) || "No local details were available before this section was omitted.";
|
|
40610
41407
|
}
|
|
40611
41408
|
function summarizeSection(pack, section, maxChars) {
|
|
40612
41409
|
if (section === "project")
|
|
@@ -40797,8 +41594,8 @@ function createAgentContextPack(input, db) {
|
|
|
40797
41594
|
...taskFiles.map((file) => file.updated_at)
|
|
40798
41595
|
], task.updated_at);
|
|
40799
41596
|
const warnings = [];
|
|
40800
|
-
const
|
|
40801
|
-
if (Date.parse(task.updated_at) <
|
|
41597
|
+
const now4 = input.now ? new Date(input.now) : new Date;
|
|
41598
|
+
if (Date.parse(task.updated_at) < now4.getTime() - limit.stale_after_hours * 60 * 60 * 1000) {
|
|
40802
41599
|
warnings.push(`task state is older than ${limit.stale_after_hours} hours`);
|
|
40803
41600
|
}
|
|
40804
41601
|
if (comments.length > recentComments.length)
|
|
@@ -40813,7 +41610,7 @@ function createAgentContextPack(input, db) {
|
|
|
40813
41610
|
id: task.id,
|
|
40814
41611
|
short_id: task.short_id,
|
|
40815
41612
|
title: redactEvidenceText(task.title),
|
|
40816
|
-
description:
|
|
41613
|
+
description: truncate2(task.description, limit.max_text_chars),
|
|
40817
41614
|
status: task.status,
|
|
40818
41615
|
priority: task.priority,
|
|
40819
41616
|
assigned_to: task.assigned_to,
|
|
@@ -40837,7 +41634,7 @@ function createAgentContextPack(input, db) {
|
|
|
40837
41634
|
plan: plan ? {
|
|
40838
41635
|
id: plan.id,
|
|
40839
41636
|
name: plan.name,
|
|
40840
|
-
description:
|
|
41637
|
+
description: truncate2(plan.description, limit.max_text_chars),
|
|
40841
41638
|
status: plan.status,
|
|
40842
41639
|
agent_id: plan.agent_id,
|
|
40843
41640
|
tasks: planTasks.slice(0, limit.plan_task_limit).map(taskSummary).filter((item) => Boolean(item)),
|
|
@@ -40856,7 +41653,7 @@ function createAgentContextPack(input, db) {
|
|
|
40856
41653
|
type: comment.type,
|
|
40857
41654
|
progress_pct: comment.progress_pct,
|
|
40858
41655
|
created_at: comment.created_at,
|
|
40859
|
-
content:
|
|
41656
|
+
content: truncate2(comment.content, limit.max_text_chars) || ""
|
|
40860
41657
|
})),
|
|
40861
41658
|
omitted: Math.max(0, comments.length - recentComments.length)
|
|
40862
41659
|
},
|
|
@@ -40864,7 +41661,7 @@ function createAgentContextPack(input, db) {
|
|
|
40864
41661
|
traceability: {
|
|
40865
41662
|
commits: traceability.commits.map((commit) => ({
|
|
40866
41663
|
sha: commit.sha,
|
|
40867
|
-
message:
|
|
41664
|
+
message: truncate2(commit.message, 240),
|
|
40868
41665
|
files_changed: commit.files_changed,
|
|
40869
41666
|
committed_at: commit.committed_at
|
|
40870
41667
|
})),
|
|
@@ -40872,7 +41669,7 @@ function createAgentContextPack(input, db) {
|
|
|
40872
41669
|
verifications: verifications.map((verification) => ({
|
|
40873
41670
|
command: verification.command,
|
|
40874
41671
|
status: verification.status,
|
|
40875
|
-
output_summary:
|
|
41672
|
+
output_summary: truncate2(verification.output_summary, limit.max_text_chars),
|
|
40876
41673
|
artifact_path: verification.artifact_path,
|
|
40877
41674
|
run_at: verification.run_at
|
|
40878
41675
|
})),
|
|
@@ -40883,14 +41680,14 @@ function createAgentContextPack(input, db) {
|
|
|
40883
41680
|
id: ledger.run.id,
|
|
40884
41681
|
title: ledger.run.title,
|
|
40885
41682
|
status: ledger.run.status,
|
|
40886
|
-
summary:
|
|
41683
|
+
summary: truncate2(ledger.run.summary, limit.max_text_chars),
|
|
40887
41684
|
agent_id: ledger.run.agent_id,
|
|
40888
41685
|
started_at: ledger.run.started_at,
|
|
40889
41686
|
completed_at: ledger.run.completed_at,
|
|
40890
|
-
events: ledger.events.map((event) => ({ event_type: event.event_type, message:
|
|
40891
|
-
commands: ledger.commands.map((command) => ({ command: command.command, status: command.status, output_summary:
|
|
40892
|
-
files: ledger.files.map((file) => ({ path: file.path, status: file.status, note:
|
|
40893
|
-
artifacts: ledger.artifacts.map((artifact) => ({ path: artifact.path, artifact_type: artifact.artifact_type, description:
|
|
41687
|
+
events: ledger.events.map((event) => ({ event_type: event.event_type, message: truncate2(event.message, 500), created_at: event.created_at })),
|
|
41688
|
+
commands: ledger.commands.map((command) => ({ command: command.command, status: command.status, output_summary: truncate2(command.output_summary, limit.max_text_chars), artifact_path: command.artifact_path })),
|
|
41689
|
+
files: ledger.files.map((file) => ({ path: file.path, status: file.status, note: truncate2(file.note, 240) })),
|
|
41690
|
+
artifacts: ledger.artifacts.map((artifact) => ({ path: artifact.path, artifact_type: artifact.artifact_type, description: truncate2(artifact.description, 240), sha256: artifact.sha256 }))
|
|
40894
41691
|
})),
|
|
40895
41692
|
omitted: Math.max(0, runs.length - selectedRuns.length)
|
|
40896
41693
|
},
|
|
@@ -40995,7 +41792,7 @@ function renderAgentContextPackCompactMarkdown(pack) {
|
|
|
40995
41792
|
const lines = [
|
|
40996
41793
|
`# Context: ${pack.task.title}`,
|
|
40997
41794
|
`${pack.task.status} | ${pack.task.priority} | ${pack.task.short_id || pack.task.id.slice(0, 8)}`,
|
|
40998
|
-
pack.task.description ?
|
|
41795
|
+
pack.task.description ? truncate2(pack.task.description, Math.min(pack.limits.summary_char_limit, 700)) : null,
|
|
40999
41796
|
"",
|
|
41000
41797
|
"## Must Know",
|
|
41001
41798
|
bullet([
|
|
@@ -56983,7 +57780,7 @@ var require_to_json_schema = __commonJS((exports) => {
|
|
|
56983
57780
|
if (val === undefined) {
|
|
56984
57781
|
if (this.unrepresentable === "throw") {
|
|
56985
57782
|
throw new Error("Literal `undefined` cannot be represented in JSON Schema");
|
|
56986
|
-
}
|
|
57783
|
+
} else {}
|
|
56987
57784
|
} else if (typeof val === "bigint") {
|
|
56988
57785
|
if (this.unrepresentable === "throw") {
|
|
56989
57786
|
throw new Error("BigInt literals cannot be represented in JSON Schema");
|
|
@@ -66019,7 +66816,7 @@ var init_agent_run_dispatcher = __esm(() => {
|
|
|
66019
66816
|
});
|
|
66020
66817
|
|
|
66021
66818
|
// src/lib/verification-providers.ts
|
|
66022
|
-
import { existsSync as
|
|
66819
|
+
import { existsSync as existsSync12, readFileSync as readFileSync7 } from "fs";
|
|
66023
66820
|
function normalizeName5(name) {
|
|
66024
66821
|
const normalized = name.trim().toLowerCase();
|
|
66025
66822
|
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(normalized)) {
|
|
@@ -66171,7 +66968,7 @@ Timed out after ${provider.timeout_ms}ms`);
|
|
|
66171
66968
|
};
|
|
66172
66969
|
}
|
|
66173
66970
|
function runCiLogProvider(input) {
|
|
66174
|
-
const text = input.log_text ?? (input.log_path &&
|
|
66971
|
+
const text = input.log_text ?? (input.log_path && existsSync12(input.log_path) ? readFileSync7(input.log_path, "utf-8") : "");
|
|
66175
66972
|
return {
|
|
66176
66973
|
status: classifyLog(text),
|
|
66177
66974
|
attempts: 1,
|
|
@@ -66183,7 +66980,7 @@ function runBrowserProvider(input) {
|
|
|
66183
66980
|
if (!input.artifact_path) {
|
|
66184
66981
|
return { status: "unknown", attempts: 1, exit_code: null, output_summary: "browser provider needs a screenshot or artifact path" };
|
|
66185
66982
|
}
|
|
66186
|
-
if (!
|
|
66983
|
+
if (!existsSync12(input.artifact_path)) {
|
|
66187
66984
|
return { status: "failed", attempts: 1, exit_code: null, output_summary: `artifact not found: ${input.artifact_path}` };
|
|
66188
66985
|
}
|
|
66189
66986
|
return {
|
|
@@ -67973,12 +68770,12 @@ function summarizeTask(task) {
|
|
|
67973
68770
|
};
|
|
67974
68771
|
}
|
|
67975
68772
|
function overdueTasks(tasks, nowIso) {
|
|
67976
|
-
const
|
|
68773
|
+
const now4 = Date.parse(nowIso);
|
|
67977
68774
|
return tasks.filter((task) => {
|
|
67978
68775
|
if (isTerminal2(task) || !task.due_at)
|
|
67979
68776
|
return false;
|
|
67980
68777
|
const due = Date.parse(task.due_at);
|
|
67981
|
-
return Number.isFinite(due) && due <
|
|
68778
|
+
return Number.isFinite(due) && due < now4;
|
|
67982
68779
|
});
|
|
67983
68780
|
}
|
|
67984
68781
|
function isReady(task, db) {
|
|
@@ -70403,8 +71200,8 @@ __export(exports_local_extensions, {
|
|
|
70403
71200
|
discoverLocalExtensions: () => discoverLocalExtensions
|
|
70404
71201
|
});
|
|
70405
71202
|
import { createHash as createHash9, createVerify } from "crypto";
|
|
70406
|
-
import { existsSync as
|
|
70407
|
-
import { basename as basename5, join as
|
|
71203
|
+
import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
|
|
71204
|
+
import { basename as basename5, join as join11, resolve as resolve13 } from "path";
|
|
70408
71205
|
function isObject3(value) {
|
|
70409
71206
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
70410
71207
|
}
|
|
@@ -70663,10 +71460,10 @@ function verifyExtensionSignature(input) {
|
|
|
70663
71460
|
}
|
|
70664
71461
|
function inspectExtensionSource(source3) {
|
|
70665
71462
|
const resolved = resolve13(source3);
|
|
70666
|
-
if (!
|
|
71463
|
+
if (!existsSync13(resolved))
|
|
70667
71464
|
throw new Error(`extension source not found: ${source3}`);
|
|
70668
71465
|
const stat = statSync6(resolved);
|
|
70669
|
-
const manifestPath = stat.isDirectory() ? [
|
|
71466
|
+
const manifestPath = stat.isDirectory() ? [join11(resolved, "todos.extension.json"), join11(resolved, "extension.json")].find(existsSync13) : resolved;
|
|
70670
71467
|
if (!manifestPath)
|
|
70671
71468
|
throw new Error(`extension directory ${source3} is missing todos.extension.json`);
|
|
70672
71469
|
const raw = readFileSync9(manifestPath);
|
|
@@ -70762,20 +71559,20 @@ function projectExtensionSources(projectPath) {
|
|
|
70762
71559
|
return [];
|
|
70763
71560
|
const root = resolve13(projectPath);
|
|
70764
71561
|
const candidates = [
|
|
70765
|
-
|
|
70766
|
-
|
|
71562
|
+
join11(root, "todos.extension.json"),
|
|
71563
|
+
join11(root, ".todos", "todos.extension.json")
|
|
70767
71564
|
];
|
|
70768
|
-
const extensionDir =
|
|
70769
|
-
if (
|
|
71565
|
+
const extensionDir = join11(root, ".todos", "extensions");
|
|
71566
|
+
if (existsSync13(extensionDir)) {
|
|
70770
71567
|
for (const entry of readdirSync3(extensionDir)) {
|
|
70771
71568
|
if (entry.startsWith("."))
|
|
70772
71569
|
continue;
|
|
70773
|
-
const full =
|
|
71570
|
+
const full = join11(extensionDir, entry);
|
|
70774
71571
|
if (statSync6(full).isDirectory() || entry.endsWith(".json"))
|
|
70775
71572
|
candidates.push(full);
|
|
70776
71573
|
}
|
|
70777
71574
|
}
|
|
70778
|
-
return candidates.filter(
|
|
71575
|
+
return candidates.filter(existsSync13);
|
|
70779
71576
|
}
|
|
70780
71577
|
function discoverLocalExtensions(options = {}) {
|
|
70781
71578
|
const config2 = loadConfig();
|
|
@@ -74297,7 +75094,7 @@ ${lines.join(`
|
|
|
74297
75094
|
const projectRoles = n.project_roles.length > 0 ? ` <${n.project_roles.join(", ")}>` : "";
|
|
74298
75095
|
const lead = n.is_project_lead ? " \u2605" : "";
|
|
74299
75096
|
const lastSeen = new Date(n.agent.last_seen_at).getTime();
|
|
74300
|
-
const active =
|
|
75097
|
+
const active = now4 - lastSeen < ACTIVE_MS ? " \u25CF" : " \u25CB";
|
|
74301
75098
|
const line = `${prefix}${active} ${n.agent.name}${title}${globalRole}${projectRoles}${lead}`;
|
|
74302
75099
|
const children = n.reports.length > 0 ? `
|
|
74303
75100
|
` + render(n.reports, indent + 1) : "";
|
|
@@ -74311,7 +75108,7 @@ ${lines.join(`
|
|
|
74311
75108
|
if (format === "json") {
|
|
74312
75109
|
return { content: [{ type: "text", text: JSON.stringify(tree, null, 2) }] };
|
|
74313
75110
|
}
|
|
74314
|
-
const
|
|
75111
|
+
const now4 = Date.now();
|
|
74315
75112
|
const ACTIVE_MS = 30 * 60 * 1000;
|
|
74316
75113
|
const text2 = tree.length > 0 ? render(tree) : "No agents in this project's org chart.";
|
|
74317
75114
|
return { content: [{ type: "text", text: text2 }] };
|
|
@@ -74861,10 +75658,10 @@ ${lines.join(`
|
|
|
74861
75658
|
});
|
|
74862
75659
|
}
|
|
74863
75660
|
if (shouldRegisterTool("get_idle_focus_prompts")) {
|
|
74864
|
-
server.tool("get_idle_focus_prompts", "Return local idle prompts for active focus sessions.", { agent_id: exports_external.string().optional(), now: exports_external.string().optional() }, async ({ agent_id, now:
|
|
75661
|
+
server.tool("get_idle_focus_prompts", "Return local idle prompts for active focus sessions.", { agent_id: exports_external.string().optional(), now: exports_external.string().optional() }, async ({ agent_id, now: now4 }) => {
|
|
74865
75662
|
try {
|
|
74866
75663
|
const { getIdleFocusSessionPrompts: getIdleFocusSessionPrompts2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
74867
|
-
return { content: [{ type: "text", text: JSON.stringify(getIdleFocusSessionPrompts2({ agent_id, now:
|
|
75664
|
+
return { content: [{ type: "text", text: JSON.stringify(getIdleFocusSessionPrompts2({ agent_id, now: now4 }), null, 2) }] };
|
|
74868
75665
|
} catch (e) {
|
|
74869
75666
|
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
74870
75667
|
}
|
|
@@ -75059,9 +75856,9 @@ __export(exports_extract, {
|
|
|
75059
75856
|
buildCodebaseIndex: () => buildCodebaseIndex,
|
|
75060
75857
|
EXTRACT_TAGS: () => EXTRACT_TAGS
|
|
75061
75858
|
});
|
|
75062
|
-
import { existsSync as
|
|
75859
|
+
import { existsSync as existsSync14, readFileSync as readFileSync10, statSync as statSync7 } from "fs";
|
|
75063
75860
|
import { createHash as createHash11 } from "crypto";
|
|
75064
|
-
import { relative as relative5, resolve as resolve14, join as
|
|
75861
|
+
import { relative as relative5, resolve as resolve14, join as join12 } from "path";
|
|
75065
75862
|
function stableHash(value) {
|
|
75066
75863
|
return createHash11("sha256").update(value).digest("hex");
|
|
75067
75864
|
}
|
|
@@ -75070,8 +75867,8 @@ function normalizePathForMatch(value) {
|
|
|
75070
75867
|
}
|
|
75071
75868
|
function readGitignorePatterns(basePath) {
|
|
75072
75869
|
const root = statSync7(basePath).isFile() ? resolve14(basePath, "..") : basePath;
|
|
75073
|
-
const gitignorePath =
|
|
75074
|
-
if (!
|
|
75870
|
+
const gitignorePath = join12(root, ".gitignore");
|
|
75871
|
+
if (!existsSync14(gitignorePath))
|
|
75075
75872
|
return [];
|
|
75076
75873
|
try {
|
|
75077
75874
|
return readFileSync10(gitignorePath, "utf-8").split(`
|
|
@@ -75213,7 +76010,7 @@ function buildCodebaseIndex(options) {
|
|
|
75213
76010
|
const files = collectFiles(basePath, extensions, excludes, respectGitignore);
|
|
75214
76011
|
const indexed = [];
|
|
75215
76012
|
for (const file of files) {
|
|
75216
|
-
const fullPath = statSync7(basePath).isFile() ? basePath :
|
|
76013
|
+
const fullPath = statSync7(basePath).isFile() ? basePath : join12(basePath, file);
|
|
75217
76014
|
try {
|
|
75218
76015
|
const source3 = readFileSync10(fullPath, "utf-8");
|
|
75219
76016
|
const relPath = statSync7(basePath).isFile() ? relative5(resolve14(basePath, ".."), fullPath) : file;
|
|
@@ -75244,7 +76041,7 @@ function extractTodos(options, db) {
|
|
|
75244
76041
|
const files = collectFiles(basePath, extensions, excludes, respectGitignore);
|
|
75245
76042
|
const allComments = [];
|
|
75246
76043
|
for (const file of files) {
|
|
75247
|
-
const fullPath = statSync7(basePath).isFile() ? basePath :
|
|
76044
|
+
const fullPath = statSync7(basePath).isFile() ? basePath : join12(basePath, file);
|
|
75248
76045
|
try {
|
|
75249
76046
|
const source3 = readFileSync10(fullPath, "utf-8");
|
|
75250
76047
|
const relPath = statSync7(basePath).isFile() ? relative5(resolve14(basePath, ".."), fullPath) : file;
|
|
@@ -76137,7 +76934,7 @@ __export(exports_builtin_templates, {
|
|
|
76137
76934
|
BUILTIN_TEMPLATES: () => BUILTIN_TEMPLATES
|
|
76138
76935
|
});
|
|
76139
76936
|
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
76140
|
-
import { join as
|
|
76937
|
+
import { join as join13 } from "path";
|
|
76141
76938
|
function templateMetadata(template) {
|
|
76142
76939
|
return {
|
|
76143
76940
|
source: BUILTIN_TEMPLATE_LIBRARY_SOURCE,
|
|
@@ -76196,7 +76993,7 @@ function writeBuiltinTemplateFiles(directory) {
|
|
|
76196
76993
|
mkdirSync7(directory, { recursive: true });
|
|
76197
76994
|
const files = [];
|
|
76198
76995
|
for (const entry of exportBuiltinTemplateFiles()) {
|
|
76199
|
-
const path =
|
|
76996
|
+
const path = join13(directory, entry.filename);
|
|
76200
76997
|
writeFileSync4(path, `${JSON.stringify(entry.template, null, 2)}
|
|
76201
76998
|
`, "utf-8");
|
|
76202
76999
|
files.push(path);
|
|
@@ -76722,16 +77519,16 @@ __export(exports_environment_snapshots, {
|
|
|
76722
77519
|
captureEnvironmentSnapshot: () => captureEnvironmentSnapshot
|
|
76723
77520
|
});
|
|
76724
77521
|
import { createHash as createHash12 } from "crypto";
|
|
76725
|
-
import { existsSync as
|
|
77522
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11, statSync as statSync8 } from "fs";
|
|
76726
77523
|
import { hostname as hostname3, platform, arch } from "os";
|
|
76727
|
-
import { dirname as dirname9, join as
|
|
77524
|
+
import { dirname as dirname9, join as join14, resolve as resolve15 } from "path";
|
|
76728
77525
|
import { tmpdir as tmpdir2 } from "os";
|
|
76729
77526
|
function sha2566(value) {
|
|
76730
77527
|
return createHash12("sha256").update(value).digest("hex");
|
|
76731
77528
|
}
|
|
76732
77529
|
function fileRecord(root, relativePath) {
|
|
76733
|
-
const path =
|
|
76734
|
-
if (!
|
|
77530
|
+
const path = join14(root, relativePath);
|
|
77531
|
+
if (!existsSync15(path))
|
|
76735
77532
|
return null;
|
|
76736
77533
|
const stat = statSync8(path);
|
|
76737
77534
|
if (!stat.isFile())
|
|
@@ -76743,7 +77540,7 @@ function manifestRecord(root, relativePath) {
|
|
|
76743
77540
|
const base = fileRecord(root, relativePath);
|
|
76744
77541
|
if (!base)
|
|
76745
77542
|
return null;
|
|
76746
|
-
const parsed = readJsonFile(
|
|
77543
|
+
const parsed = readJsonFile(join14(root, relativePath));
|
|
76747
77544
|
if (!parsed)
|
|
76748
77545
|
return { ...base, redacted: {} };
|
|
76749
77546
|
const redacted = redactValue({
|
|
@@ -76838,8 +77635,8 @@ function commandEnv(env, includeValues) {
|
|
|
76838
77635
|
function defaultSnapshotDir() {
|
|
76839
77636
|
const dbPath = getDatabasePath();
|
|
76840
77637
|
if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
|
|
76841
|
-
return
|
|
76842
|
-
return
|
|
77638
|
+
return join14(tmpdir2(), "hasna-todos", "environment-snapshots");
|
|
77639
|
+
return join14(dirname9(resolve15(dbPath)), "environment-snapshots");
|
|
76843
77640
|
}
|
|
76844
77641
|
function snapshotWithId(snapshot) {
|
|
76845
77642
|
const digest = sha2566(JSON.stringify(snapshot)).slice(0, 24);
|
|
@@ -76886,7 +77683,7 @@ function captureEnvironmentSnapshot(input = {}) {
|
|
|
76886
77683
|
});
|
|
76887
77684
|
}
|
|
76888
77685
|
function writeEnvironmentSnapshot(snapshot, outputPath) {
|
|
76889
|
-
const path = outputPath ? resolve15(outputPath) :
|
|
77686
|
+
const path = outputPath ? resolve15(outputPath) : join14(defaultSnapshotDir(), `${snapshot.id}.json`);
|
|
76890
77687
|
ensureDir2(dirname9(path));
|
|
76891
77688
|
writeJsonFile(path, snapshot);
|
|
76892
77689
|
return path;
|
|
@@ -77532,27 +78329,27 @@ __export(exports_serve, {
|
|
|
77532
78329
|
SECURITY_HEADERS: () => SECURITY_HEADERS,
|
|
77533
78330
|
MIME_TYPES: () => MIME_TYPES
|
|
77534
78331
|
});
|
|
77535
|
-
import { existsSync as
|
|
77536
|
-
import { join as
|
|
78332
|
+
import { existsSync as existsSync16 } from "fs";
|
|
78333
|
+
import { join as join15, dirname as dirname10, extname } from "path";
|
|
77537
78334
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
77538
78335
|
function resolveDashboardDir() {
|
|
77539
78336
|
const candidates = [];
|
|
77540
78337
|
try {
|
|
77541
78338
|
const scriptDir = dirname10(fileURLToPath2(import.meta.url));
|
|
77542
|
-
candidates.push(
|
|
77543
|
-
candidates.push(
|
|
78339
|
+
candidates.push(join15(scriptDir, "..", "dashboard", "dist"));
|
|
78340
|
+
candidates.push(join15(scriptDir, "..", "..", "dashboard", "dist"));
|
|
77544
78341
|
} catch {}
|
|
77545
78342
|
if (process.argv[1]) {
|
|
77546
78343
|
const mainDir = dirname10(process.argv[1]);
|
|
77547
|
-
candidates.push(
|
|
77548
|
-
candidates.push(
|
|
78344
|
+
candidates.push(join15(mainDir, "..", "dashboard", "dist"));
|
|
78345
|
+
candidates.push(join15(mainDir, "..", "..", "dashboard", "dist"));
|
|
77549
78346
|
}
|
|
77550
|
-
candidates.push(
|
|
78347
|
+
candidates.push(join15(process.cwd(), "dashboard", "dist"));
|
|
77551
78348
|
for (const candidate of candidates) {
|
|
77552
|
-
if (
|
|
78349
|
+
if (existsSync16(candidate))
|
|
77553
78350
|
return candidate;
|
|
77554
78351
|
}
|
|
77555
|
-
return
|
|
78352
|
+
return join15(process.cwd(), "dashboard", "dist");
|
|
77556
78353
|
}
|
|
77557
78354
|
function getProvidedApiKey(req) {
|
|
77558
78355
|
const headerKey = req.headers.get("x-api-key");
|
|
@@ -77579,15 +78376,15 @@ function checkAuth(req, apiKey) {
|
|
|
77579
78376
|
return null;
|
|
77580
78377
|
}
|
|
77581
78378
|
function checkRateLimit(ip) {
|
|
77582
|
-
const
|
|
78379
|
+
const now4 = Date.now();
|
|
77583
78380
|
const entry = rateLimitMap.get(ip);
|
|
77584
|
-
if (!entry ||
|
|
77585
|
-
rateLimitMap.set(ip, { count: 1, resetAt:
|
|
78381
|
+
if (!entry || now4 > entry.resetAt) {
|
|
78382
|
+
rateLimitMap.set(ip, { count: 1, resetAt: now4 + RATE_LIMIT_WINDOW_MS });
|
|
77586
78383
|
return { allowed: true };
|
|
77587
78384
|
}
|
|
77588
78385
|
entry.count++;
|
|
77589
78386
|
if (entry.count > RATE_LIMIT_MAX) {
|
|
77590
|
-
return { allowed: false, retryAfter: Math.ceil((entry.resetAt -
|
|
78387
|
+
return { allowed: false, retryAfter: Math.ceil((entry.resetAt - now4) / 1000) };
|
|
77591
78388
|
}
|
|
77592
78389
|
return { allowed: true };
|
|
77593
78390
|
}
|
|
@@ -77602,7 +78399,7 @@ function json(data, status = 200, headers) {
|
|
|
77602
78399
|
});
|
|
77603
78400
|
}
|
|
77604
78401
|
function serveStaticFile(filePath) {
|
|
77605
|
-
if (!
|
|
78402
|
+
if (!existsSync16(filePath))
|
|
77606
78403
|
return null;
|
|
77607
78404
|
const ext = extname(filePath);
|
|
77608
78405
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
@@ -77681,7 +78478,7 @@ data: ${data}
|
|
|
77681
78478
|
filteredSseClients.delete(client);
|
|
77682
78479
|
}
|
|
77683
78480
|
const dashboardDir = resolveDashboardDir();
|
|
77684
|
-
const dashboardExists =
|
|
78481
|
+
const dashboardExists = existsSync16(dashboardDir);
|
|
77685
78482
|
if (!dashboardExists) {
|
|
77686
78483
|
console.error(`
|
|
77687
78484
|
Dashboard not found at: ${dashboardDir}`);
|