@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/dist/mcp/index.js CHANGED
@@ -8628,6 +8628,7 @@ var init_event_hooks = __esm(() => {
8628
8628
  init_runner_sandbox();
8629
8629
  init_config();
8630
8630
  LOCAL_EVENT_TYPES = [
8631
+ "task.created",
8631
8632
  "task.assigned",
8632
8633
  "task.blocked",
8633
8634
  "task.started",
@@ -8652,6 +8653,760 @@ var init_event_hooks = __esm(() => {
8652
8653
  VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
8653
8654
  });
8654
8655
 
8656
+ // node_modules/.bun/@hasna+events@0.1.9/node_modules/@hasna/events/dist/index.js
8657
+ import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
8658
+ import { existsSync as existsSync5 } from "fs";
8659
+ import { homedir } from "os";
8660
+ import { join as join4 } from "path";
8661
+ import { createHmac, timingSafeEqual } from "crypto";
8662
+ import { randomUUID as randomUUID2 } from "crypto";
8663
+ import { spawn } from "child_process";
8664
+ import { randomUUID as randomUUID22 } from "crypto";
8665
+ function getPathValue(input, path) {
8666
+ return path.split(".").reduce((value, part) => {
8667
+ if (value && typeof value === "object" && part in value) {
8668
+ return value[part];
8669
+ }
8670
+ return;
8671
+ }, input);
8672
+ }
8673
+ function wildcardToRegExp(pattern, options = {}) {
8674
+ let body = "";
8675
+ for (let index = 0;index < pattern.length; index += 1) {
8676
+ const char = pattern[index];
8677
+ if (char === "*") {
8678
+ if (pattern[index + 1] === "*") {
8679
+ body += ".*";
8680
+ index += 1;
8681
+ } else {
8682
+ body += options.segmentSafe ? "[^/]*" : ".*";
8683
+ }
8684
+ } else {
8685
+ body += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
8686
+ }
8687
+ }
8688
+ return new RegExp(`^${body}$`);
8689
+ }
8690
+ function matchString(value, matcher, options = {}) {
8691
+ if (matcher === undefined)
8692
+ return true;
8693
+ if (value === undefined)
8694
+ return false;
8695
+ const matchers = Array.isArray(matcher) ? matcher : [matcher];
8696
+ return matchers.some((item) => wildcardToRegExp(item, options).test(value));
8697
+ }
8698
+ function matchRecord(input, matcher) {
8699
+ if (!matcher)
8700
+ return true;
8701
+ return Object.entries(matcher).every(([path, expected]) => {
8702
+ const actual = getPathValue(input, path);
8703
+ if (typeof expected === "string" || Array.isArray(expected)) {
8704
+ return matchString(actual === undefined ? undefined : String(actual), expected, {
8705
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
8706
+ });
8707
+ }
8708
+ return actual === expected;
8709
+ });
8710
+ }
8711
+ function eventMatchesFilter(event, filter) {
8712
+ 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);
8713
+ }
8714
+ function channelMatchesEvent(channel, event) {
8715
+ if (!channel.enabled)
8716
+ return false;
8717
+ if (!channel.filters || channel.filters.length === 0)
8718
+ return true;
8719
+ return channel.filters.some((filter) => eventMatchesFilter(event, filter));
8720
+ }
8721
+ function getEventsDataDir(override) {
8722
+ return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join4(homedir(), ".hasna", "events");
8723
+ }
8724
+
8725
+ class JsonEventsStore {
8726
+ dataDir;
8727
+ channelsPath;
8728
+ eventsPath;
8729
+ deliveriesPath;
8730
+ constructor(dataDir = getEventsDataDir()) {
8731
+ this.dataDir = dataDir;
8732
+ this.channelsPath = join4(dataDir, "channels.json");
8733
+ this.eventsPath = join4(dataDir, "events.json");
8734
+ this.deliveriesPath = join4(dataDir, "deliveries.json");
8735
+ }
8736
+ async init() {
8737
+ await mkdir(this.dataDir, { recursive: true, mode: 448 });
8738
+ await chmod(this.dataDir, 448).catch(() => {
8739
+ return;
8740
+ });
8741
+ await this.ensureArrayFile(this.channelsPath);
8742
+ await this.ensureArrayFile(this.eventsPath);
8743
+ await this.ensureArrayFile(this.deliveriesPath);
8744
+ }
8745
+ async addChannel(channel) {
8746
+ await this.init();
8747
+ const channels = await this.readJson(this.channelsPath, []);
8748
+ const index = channels.findIndex((item) => item.id === channel.id);
8749
+ if (index >= 0) {
8750
+ channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
8751
+ } else {
8752
+ channels.push(channel);
8753
+ }
8754
+ await this.writeJson(this.channelsPath, channels);
8755
+ return index >= 0 ? channels[index] : channel;
8756
+ }
8757
+ async listChannels() {
8758
+ await this.init();
8759
+ return this.readJson(this.channelsPath, []);
8760
+ }
8761
+ async getChannel(id) {
8762
+ const channels = await this.listChannels();
8763
+ return channels.find((channel) => channel.id === id);
8764
+ }
8765
+ async removeChannel(id) {
8766
+ await this.init();
8767
+ const channels = await this.readJson(this.channelsPath, []);
8768
+ const next = channels.filter((channel) => channel.id !== id);
8769
+ await this.writeJson(this.channelsPath, next);
8770
+ return next.length !== channels.length;
8771
+ }
8772
+ async appendEvent(event) {
8773
+ await this.init();
8774
+ const events = await this.readJson(this.eventsPath, []);
8775
+ events.push(event);
8776
+ await this.writeJson(this.eventsPath, events);
8777
+ return event;
8778
+ }
8779
+ async listEvents() {
8780
+ await this.init();
8781
+ return this.readJson(this.eventsPath, []);
8782
+ }
8783
+ async findEventByIdentity(identity) {
8784
+ const events = await this.listEvents();
8785
+ return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
8786
+ }
8787
+ async appendDelivery(result) {
8788
+ await this.init();
8789
+ const deliveries = await this.readJson(this.deliveriesPath, []);
8790
+ deliveries.push(result);
8791
+ await this.writeJson(this.deliveriesPath, deliveries);
8792
+ return result;
8793
+ }
8794
+ async listDeliveries() {
8795
+ await this.init();
8796
+ return this.readJson(this.deliveriesPath, []);
8797
+ }
8798
+ async exportData() {
8799
+ return {
8800
+ channels: await this.listChannels(),
8801
+ events: await this.listEvents(),
8802
+ deliveries: await this.listDeliveries()
8803
+ };
8804
+ }
8805
+ async ensureArrayFile(path) {
8806
+ if (!existsSync5(path)) {
8807
+ await writeFile(path, `[]
8808
+ `, { encoding: "utf-8", mode: 384 });
8809
+ }
8810
+ await chmod(path, 384).catch(() => {
8811
+ return;
8812
+ });
8813
+ }
8814
+ async readJson(path, fallback) {
8815
+ try {
8816
+ const raw = await readFile(path, "utf-8");
8817
+ if (!raw.trim())
8818
+ return fallback;
8819
+ return JSON.parse(raw);
8820
+ } catch (error) {
8821
+ if (error.code === "ENOENT")
8822
+ return fallback;
8823
+ throw error;
8824
+ }
8825
+ }
8826
+ async writeJson(path, value) {
8827
+ const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
8828
+ await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
8829
+ `, { encoding: "utf-8", mode: 384 });
8830
+ await rename(tempPath, path);
8831
+ await chmod(path, 384).catch(() => {
8832
+ return;
8833
+ });
8834
+ }
8835
+ }
8836
+ function buildSignatureBase(timestamp, body) {
8837
+ return `${timestamp}.${body}`;
8838
+ }
8839
+ function signPayload(secret, timestamp, body) {
8840
+ const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
8841
+ return `sha256=${digest}`;
8842
+ }
8843
+ function now2() {
8844
+ return new Date().toISOString();
8845
+ }
8846
+ function truncate(value, max = 4096) {
8847
+ return value.length > max ? `${value.slice(0, max)}...` : value;
8848
+ }
8849
+ function buildWebhookRequest(event, channel) {
8850
+ if (!channel.webhook)
8851
+ throw new Error(`Channel ${channel.id} has no webhook config`);
8852
+ const body = JSON.stringify(event);
8853
+ const timestamp = event.time;
8854
+ const headers = {
8855
+ "Content-Type": "application/json",
8856
+ "User-Agent": "@hasna/events",
8857
+ "X-Hasna-Event-Id": event.id,
8858
+ "X-Hasna-Event-Type": event.type,
8859
+ "X-Hasna-Timestamp": timestamp,
8860
+ ...channel.webhook.headers
8861
+ };
8862
+ if (channel.webhook.secret) {
8863
+ headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
8864
+ }
8865
+ return { body, headers };
8866
+ }
8867
+ async function dispatchWebhook(event, channel, options = {}) {
8868
+ if (!channel.webhook)
8869
+ throw new Error(`Channel ${channel.id} has no webhook config`);
8870
+ const startedAt = now2();
8871
+ const { body, headers } = buildWebhookRequest(event, channel);
8872
+ const controller = new AbortController;
8873
+ const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
8874
+ try {
8875
+ const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
8876
+ method: "POST",
8877
+ headers,
8878
+ body,
8879
+ signal: controller.signal
8880
+ });
8881
+ const responseBody = truncate(await response.text());
8882
+ return {
8883
+ attempt: 1,
8884
+ status: response.ok ? "success" : "failed",
8885
+ startedAt,
8886
+ completedAt: now2(),
8887
+ responseStatus: response.status,
8888
+ responseBody,
8889
+ error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
8890
+ };
8891
+ } catch (error) {
8892
+ return {
8893
+ attempt: 1,
8894
+ status: "failed",
8895
+ startedAt,
8896
+ completedAt: now2(),
8897
+ error: error instanceof Error ? error.message : String(error)
8898
+ };
8899
+ } finally {
8900
+ clearTimeout(timeout);
8901
+ }
8902
+ }
8903
+ async function dispatchCommand(event, channel) {
8904
+ if (!channel.command)
8905
+ throw new Error(`Channel ${channel.id} has no command config`);
8906
+ const startedAt = now2();
8907
+ const eventJson = JSON.stringify(event);
8908
+ const env = {
8909
+ ...process.env,
8910
+ ...channel.command.env,
8911
+ HASNA_CHANNEL_ID: channel.id,
8912
+ HASNA_EVENT_ID: event.id,
8913
+ HASNA_EVENT_TYPE: event.type,
8914
+ HASNA_EVENT_SOURCE: event.source,
8915
+ HASNA_EVENT_SUBJECT: event.subject ?? "",
8916
+ HASNA_EVENT_SEVERITY: event.severity,
8917
+ HASNA_EVENT_TIME: event.time,
8918
+ HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
8919
+ HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
8920
+ HASNA_EVENT_JSON: eventJson
8921
+ };
8922
+ return new Promise((resolve6) => {
8923
+ const child = spawn(channel.command.command, channel.command.args ?? [], {
8924
+ cwd: channel.command.cwd,
8925
+ env,
8926
+ stdio: ["pipe", "pipe", "pipe"]
8927
+ });
8928
+ let stdout = "";
8929
+ let stderr = "";
8930
+ const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
8931
+ child.stdin.end(eventJson);
8932
+ child.stdout.on("data", (chunk) => {
8933
+ stdout += chunk.toString();
8934
+ });
8935
+ child.stderr.on("data", (chunk) => {
8936
+ stderr += chunk.toString();
8937
+ });
8938
+ child.on("error", (error) => {
8939
+ clearTimeout(timeout);
8940
+ resolve6({
8941
+ attempt: 1,
8942
+ status: "failed",
8943
+ startedAt,
8944
+ completedAt: now2(),
8945
+ stdout: truncate(stdout),
8946
+ stderr: truncate(stderr),
8947
+ error: error.message
8948
+ });
8949
+ });
8950
+ child.on("close", (code, signal) => {
8951
+ clearTimeout(timeout);
8952
+ const success = code === 0;
8953
+ resolve6({
8954
+ attempt: 1,
8955
+ status: success ? "success" : "failed",
8956
+ startedAt,
8957
+ completedAt: now2(),
8958
+ stdout: truncate(stdout),
8959
+ stderr: truncate(stderr),
8960
+ error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
8961
+ });
8962
+ });
8963
+ });
8964
+ }
8965
+ async function dispatchChannel(event, channel, options = {}) {
8966
+ if (channel.transport === "webhook")
8967
+ return dispatchWebhook(event, channel, options);
8968
+ if (channel.transport === "command")
8969
+ return dispatchCommand(event, channel);
8970
+ return {
8971
+ attempt: 1,
8972
+ status: "skipped",
8973
+ startedAt: now2(),
8974
+ completedAt: now2(),
8975
+ error: `Unsupported transport: ${channel.transport}`
8976
+ };
8977
+ }
8978
+ function createDeliveryResult(event, channel, attempts) {
8979
+ const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
8980
+ return {
8981
+ id: randomUUID2(),
8982
+ eventId: event.id,
8983
+ channelId: channel.id,
8984
+ transport: channel.transport,
8985
+ status,
8986
+ attempts,
8987
+ createdAt: attempts[0]?.startedAt ?? now2(),
8988
+ completedAt: attempts.at(-1)?.completedAt ?? now2()
8989
+ };
8990
+ }
8991
+ function createEvent(input) {
8992
+ return {
8993
+ id: input.id ?? randomUUID22(),
8994
+ source: input.source,
8995
+ type: input.type,
8996
+ time: normalizeTime(input.time),
8997
+ subject: input.subject,
8998
+ severity: input.severity ?? "info",
8999
+ data: input.data ?? {},
9000
+ message: input.message,
9001
+ dedupeKey: input.dedupeKey,
9002
+ schemaVersion: input.schemaVersion ?? "1.0",
9003
+ metadata: input.metadata ?? {}
9004
+ };
9005
+ }
9006
+
9007
+ class EventsClient {
9008
+ store;
9009
+ redactors;
9010
+ transportOptions;
9011
+ constructor(options = {}) {
9012
+ this.store = options.store ?? new JsonEventsStore(options.dataDir);
9013
+ this.redactors = options.redactors ?? [];
9014
+ this.transportOptions = { fetchImpl: options.fetchImpl };
9015
+ }
9016
+ async addChannel(input) {
9017
+ const timestamp = new Date().toISOString();
9018
+ return this.store.addChannel({
9019
+ ...input,
9020
+ createdAt: input.createdAt ?? timestamp,
9021
+ updatedAt: input.updatedAt ?? timestamp
9022
+ });
9023
+ }
9024
+ async listChannels() {
9025
+ return this.store.listChannels();
9026
+ }
9027
+ async removeChannel(id) {
9028
+ return this.store.removeChannel(id);
9029
+ }
9030
+ async emit(input, options = {}) {
9031
+ const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
9032
+ if (options.dedupe !== false) {
9033
+ const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
9034
+ if (existing) {
9035
+ return { event: existing, deliveries: [], deduped: true };
9036
+ }
9037
+ }
9038
+ await this.store.appendEvent(event);
9039
+ const deliveries = options.deliver === false ? [] : await this.deliver(event);
9040
+ return { event, deliveries, deduped: false };
9041
+ }
9042
+ async listEvents() {
9043
+ return this.store.listEvents();
9044
+ }
9045
+ async listDeliveries() {
9046
+ return this.store.listDeliveries();
9047
+ }
9048
+ async deliver(event) {
9049
+ const channels = await this.store.listChannels();
9050
+ const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
9051
+ const deliveries = [];
9052
+ for (const channel of selected) {
9053
+ const eventForChannel = await this.applyRedaction(event, channel);
9054
+ const result = await this.deliverWithRetry(eventForChannel, channel);
9055
+ await this.store.appendDelivery(result);
9056
+ deliveries.push(result);
9057
+ }
9058
+ return deliveries;
9059
+ }
9060
+ async matchChannel(id, input = {}) {
9061
+ const channel = await this.store.getChannel(id);
9062
+ if (!channel)
9063
+ throw new Error(`Channel not found: ${id}`);
9064
+ const event = createEvent({
9065
+ source: input.source ?? "hasna.events",
9066
+ type: input.type ?? "events.test",
9067
+ subject: input.subject ?? id,
9068
+ severity: input.severity ?? "info",
9069
+ data: input.data ?? { test: true },
9070
+ message: input.message ?? "Hasna events test delivery",
9071
+ dedupeKey: input.dedupeKey,
9072
+ schemaVersion: input.schemaVersion,
9073
+ metadata: input.metadata,
9074
+ time: input.time,
9075
+ id: input.id
9076
+ });
9077
+ const matched = channelMatchesEvent(channel, event);
9078
+ return {
9079
+ channelId: channel.id,
9080
+ matched,
9081
+ event,
9082
+ filters: channel.filters,
9083
+ reason: matched ? undefined : channel.enabled ? "event did not match channel filters" : "channel is disabled"
9084
+ };
9085
+ }
9086
+ async testChannel(id, input = {}, options = {}) {
9087
+ const channel = await this.store.getChannel(id);
9088
+ if (!channel)
9089
+ throw new Error(`Channel not found: ${id}`);
9090
+ const match = await this.matchChannel(id, input);
9091
+ const event = match.event;
9092
+ if (options.honorFilters && !match.matched) {
9093
+ const timestamp = new Date().toISOString();
9094
+ const result2 = createDeliveryResult(event, channel, [{
9095
+ attempt: 1,
9096
+ status: "skipped",
9097
+ startedAt: timestamp,
9098
+ completedAt: timestamp,
9099
+ error: match.reason
9100
+ }]);
9101
+ result2.metadata = { reason: "filter_mismatch" };
9102
+ await this.store.appendDelivery(result2);
9103
+ return result2;
9104
+ }
9105
+ const eventForChannel = await this.applyRedaction(event, channel);
9106
+ const result = await this.deliverWithRetry(eventForChannel, channel);
9107
+ await this.store.appendDelivery(result);
9108
+ return result;
9109
+ }
9110
+ async replay(options = {}) {
9111
+ const events = (await this.store.listEvents()).filter((event) => {
9112
+ if (options.eventId && event.id !== options.eventId)
9113
+ return false;
9114
+ if (options.source && event.source !== options.source)
9115
+ return false;
9116
+ if (options.type && event.type !== options.type)
9117
+ return false;
9118
+ return true;
9119
+ });
9120
+ if (options.dryRun)
9121
+ return { events, deliveries: [] };
9122
+ const deliveries = [];
9123
+ for (const event of events) {
9124
+ deliveries.push(...await this.deliver(event));
9125
+ }
9126
+ return { events, deliveries };
9127
+ }
9128
+ async applyRedaction(event, channel) {
9129
+ let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
9130
+ for (const redactor of this.redactors) {
9131
+ next = await redactor(next, channel);
9132
+ }
9133
+ return next;
9134
+ }
9135
+ async deliverWithRetry(event, channel) {
9136
+ const policy = normalizeRetryPolicy(channel.retry);
9137
+ const attempts = [];
9138
+ for (let index = 0;index < policy.maxAttempts; index += 1) {
9139
+ const attempt = await dispatchChannel(event, channel, this.transportOptions);
9140
+ attempt.attempt = index + 1;
9141
+ if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
9142
+ attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
9143
+ }
9144
+ attempts.push(attempt);
9145
+ if (attempt.status !== "failed")
9146
+ break;
9147
+ if (attempt.nextBackoffMs)
9148
+ await Bun.sleep(attempt.nextBackoffMs);
9149
+ }
9150
+ return createDeliveryResult(event, channel, attempts);
9151
+ }
9152
+ }
9153
+ function redactPaths(event, paths, replacement = "[REDACTED]") {
9154
+ if (paths.length === 0)
9155
+ return event;
9156
+ const copy = structuredClone(event);
9157
+ for (const path of paths) {
9158
+ setPath(copy, path, replacement);
9159
+ }
9160
+ return copy;
9161
+ }
9162
+ function redactSensitiveKeys(event, replacement = "[REDACTED]") {
9163
+ return redactValue2(event, replacement);
9164
+ }
9165
+ function shouldRedactKey(key) {
9166
+ return /secret|token|password|api[_-]?key|authorization/i.test(key);
9167
+ }
9168
+ function redactValue2(value, replacement) {
9169
+ if (Array.isArray(value))
9170
+ return value.map((item) => redactValue2(item, replacement));
9171
+ if (!value || typeof value !== "object")
9172
+ return value;
9173
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
9174
+ key,
9175
+ shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
9176
+ ]));
9177
+ }
9178
+ function setPath(input, path, replacement) {
9179
+ const parts = path.split(".");
9180
+ let cursor = input;
9181
+ for (const part of parts.slice(0, -1)) {
9182
+ const next = cursor[part];
9183
+ if (!next || typeof next !== "object")
9184
+ return;
9185
+ cursor = next;
9186
+ }
9187
+ const last = parts.at(-1);
9188
+ if (last && last in cursor)
9189
+ cursor[last] = replacement;
9190
+ }
9191
+ function normalizeTime(value) {
9192
+ if (!value)
9193
+ return new Date().toISOString();
9194
+ return value instanceof Date ? value.toISOString() : value;
9195
+ }
9196
+ function normalizeRetryPolicy(policy) {
9197
+ return {
9198
+ maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
9199
+ backoffMs: Math.max(0, policy?.backoffMs ?? 250),
9200
+ multiplier: Math.max(1, policy?.multiplier ?? 2)
9201
+ };
9202
+ }
9203
+ var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", DEFAULT_SIGNATURE_TOLERANCE_MS;
9204
+ var init_dist = __esm(() => {
9205
+ DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
9206
+ });
9207
+
9208
+ // src/db/task-lists.ts
9209
+ function rowToTaskList(row) {
9210
+ return {
9211
+ ...row,
9212
+ metadata: JSON.parse(row.metadata || "{}")
9213
+ };
9214
+ }
9215
+ function createTaskList(input, db) {
9216
+ const d = db || getDatabase();
9217
+ const id = uuid();
9218
+ const timestamp = now();
9219
+ const slug = input.slug || slugify(input.name);
9220
+ if (!input.project_id) {
9221
+ const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
9222
+ if (existing) {
9223
+ throw new Error(`Standalone task list with slug "${slug}" already exists`);
9224
+ }
9225
+ }
9226
+ d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
9227
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
9228
+ return getTaskList(id, d);
9229
+ }
9230
+ function getTaskList(id, db) {
9231
+ const d = db || getDatabase();
9232
+ const row = d.query("SELECT * FROM task_lists WHERE id = ?").get(id);
9233
+ return row ? rowToTaskList(row) : null;
9234
+ }
9235
+ function getTaskListBySlug(slug, projectId, db) {
9236
+ const d = db || getDatabase();
9237
+ let row;
9238
+ if (projectId) {
9239
+ row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id = ?").get(slug, projectId);
9240
+ } else {
9241
+ row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id IS NULL").get(slug);
9242
+ }
9243
+ return row ? rowToTaskList(row) : null;
9244
+ }
9245
+ function listTaskLists(projectId, db) {
9246
+ const d = db || getDatabase();
9247
+ if (projectId) {
9248
+ return d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(projectId).map(rowToTaskList);
9249
+ }
9250
+ return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
9251
+ }
9252
+ function updateTaskList(id, input, db) {
9253
+ const d = db || getDatabase();
9254
+ const existing = getTaskList(id, d);
9255
+ if (!existing)
9256
+ throw new TaskListNotFoundError(id);
9257
+ const sets = ["updated_at = ?"];
9258
+ const params = [now()];
9259
+ if (input.name !== undefined) {
9260
+ sets.push("name = ?");
9261
+ params.push(input.name);
9262
+ }
9263
+ if (input.description !== undefined) {
9264
+ sets.push("description = ?");
9265
+ params.push(input.description);
9266
+ }
9267
+ if (input.metadata !== undefined) {
9268
+ sets.push("metadata = ?");
9269
+ params.push(JSON.stringify(input.metadata));
9270
+ }
9271
+ params.push(id);
9272
+ d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
9273
+ return getTaskList(id, d);
9274
+ }
9275
+ function deleteTaskList(id, db) {
9276
+ const d = db || getDatabase();
9277
+ return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
9278
+ }
9279
+ function ensureTaskList(name, slug, projectId, db) {
9280
+ const d = db || getDatabase();
9281
+ const existing = getTaskListBySlug(slug, projectId, d);
9282
+ if (existing)
9283
+ return existing;
9284
+ return createTaskList({ name, slug, project_id: projectId }, d);
9285
+ }
9286
+ var init_task_lists = __esm(() => {
9287
+ init_types();
9288
+ init_database();
9289
+ init_projects();
9290
+ });
9291
+
9292
+ // src/lib/shared-events.ts
9293
+ function taskEventData(task, extra = {}) {
9294
+ return {
9295
+ id: task.id,
9296
+ task_id: task.id,
9297
+ short_id: task.short_id,
9298
+ title: task.title,
9299
+ description: task.description,
9300
+ status: task.status,
9301
+ priority: task.priority,
9302
+ project_id: task.project_id,
9303
+ parent_id: task.parent_id,
9304
+ plan_id: task.plan_id,
9305
+ task_list_id: task.task_list_id,
9306
+ agent_id: task.agent_id,
9307
+ assigned_to: task.assigned_to,
9308
+ session_id: task.session_id,
9309
+ working_dir: task.working_dir,
9310
+ tags: task.tags,
9311
+ metadata: task.metadata,
9312
+ version: task.version,
9313
+ created_at: task.created_at,
9314
+ updated_at: task.updated_at,
9315
+ started_at: task.started_at,
9316
+ completed_at: task.completed_at,
9317
+ due_at: task.due_at,
9318
+ ...extra
9319
+ };
9320
+ }
9321
+ function taskEventMetadata(task) {
9322
+ const metadata = {
9323
+ package: "@hasna/todos",
9324
+ todos_event_schema_version: 1,
9325
+ task_id: task.id,
9326
+ task_short_id: task.short_id,
9327
+ project_id: task.project_id,
9328
+ task_list_id: task.task_list_id,
9329
+ working_dir: task.working_dir
9330
+ };
9331
+ try {
9332
+ const project = task.project_id ? getProject(task.project_id) : null;
9333
+ const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
9334
+ if (project) {
9335
+ metadata.project_id = project.id;
9336
+ metadata.project_name = project.name;
9337
+ metadata.project_path = projectPath;
9338
+ metadata.project_canonical_path = project.path;
9339
+ metadata.project_default_task_list_slug = project.task_list_id;
9340
+ metadata.root_project_id = inferRootProjectId(project);
9341
+ } else if (projectPath) {
9342
+ metadata.project_path = projectPath;
9343
+ metadata.project_canonical_path = projectPath;
9344
+ }
9345
+ if (projectPath) {
9346
+ metadata.project_kind = classifyProjectKind(projectPath);
9347
+ metadata.project_is_worktree = isWorktreePath(projectPath);
9348
+ if (typeof task.metadata.route_enabled === "boolean") {
9349
+ metadata.route_enabled = task.metadata.route_enabled;
9350
+ }
9351
+ metadata.working_dir = task.working_dir ?? projectPath;
9352
+ }
9353
+ 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;
9354
+ if (taskList) {
9355
+ metadata.task_list_id = taskList.id;
9356
+ metadata.task_list_slug = taskList.slug;
9357
+ metadata.task_list_name = taskList.name;
9358
+ metadata.task_list_project_id = taskList.project_id;
9359
+ metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
9360
+ }
9361
+ } catch {}
9362
+ return metadata;
9363
+ }
9364
+ function classifyProjectKind(path) {
9365
+ return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
9366
+ }
9367
+ function isWorktreePath(path) {
9368
+ return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
9369
+ }
9370
+ function inferRootProjectId(project) {
9371
+ return isWorktreePath(project.path) ? null : project.id;
9372
+ }
9373
+ function readMachineLocalPath(project) {
9374
+ const machineId = process.env["TODOS_MACHINE_ID"];
9375
+ if (!machineId)
9376
+ return null;
9377
+ try {
9378
+ const row = getDatabase().query("SELECT path FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(project.id, machineId);
9379
+ return row?.path ?? null;
9380
+ } catch {
9381
+ return null;
9382
+ }
9383
+ }
9384
+ async function emitSharedTaskEvent(input) {
9385
+ const data = taskEventData(input.task, input.data);
9386
+ await new EventsClient().emit({
9387
+ source: SOURCE,
9388
+ type: input.type,
9389
+ subject: input.task.id,
9390
+ severity: input.severity ?? "info",
9391
+ message: input.message ?? `${input.type}: ${input.task.title}`,
9392
+ data,
9393
+ dedupeKey: input.dedupeKey ?? `${input.type}:${input.task.id}:${input.task.version}`,
9394
+ metadata: taskEventMetadata(input.task)
9395
+ }, { deliver: true, dedupe: true });
9396
+ }
9397
+ function emitSharedTaskEventQuiet(input) {
9398
+ emitSharedTaskEvent(input).catch(() => {
9399
+ return;
9400
+ });
9401
+ }
9402
+ var SOURCE = "todos";
9403
+ var init_shared_events = __esm(() => {
9404
+ init_dist();
9405
+ init_database();
9406
+ init_projects();
9407
+ init_task_lists();
9408
+ });
9409
+
8655
9410
  // src/lib/secret-redaction.ts
8656
9411
  function isAllowlisted(text, match, allowlist) {
8657
9412
  const context = text.slice(Math.max(0, text.indexOf(match) - 20), text.indexOf(match) + match.length + 20);
@@ -9119,7 +9874,7 @@ async function deliverWebhook(wh, event, body, attempt, db) {
9119
9874
  activeDeliveries--;
9120
9875
  }
9121
9876
  }
9122
- async function dispatchWebhook(event, payload, db) {
9877
+ async function dispatchWebhook2(event, payload, db) {
9123
9878
  const d = db || getDatabase();
9124
9879
  const webhooks = listWebhooks(d).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
9125
9880
  const payloadObj = typeof payload === "object" && payload !== null ? payload : {};
@@ -9236,7 +9991,10 @@ function createTask(input, db) {
9236
9991
  insertTaskTags(id, tags, d);
9237
9992
  }
9238
9993
  const task = getTask(id, d);
9239
- dispatchWebhook("task.created", { id: task.id, short_id: task.short_id, title: task.title, status: task.status, priority: task.priority, project_id: task.project_id, assigned_to: task.assigned_to }, d).catch(() => {});
9994
+ const payload = taskEventData(task);
9995
+ dispatchWebhook2("task.created", payload, d).catch(() => {});
9996
+ emitLocalEventHooksQuiet({ type: "task.created", payload });
9997
+ emitSharedTaskEventQuiet({ type: "task.created", task });
9240
9998
  return task;
9241
9999
  }
9242
10000
  function getTask(id, db) {
@@ -9580,18 +10338,7 @@ function updateTask(id, input, db) {
9580
10338
  logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
9581
10339
  if (input.approved_by !== undefined)
9582
10340
  logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
9583
- if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
9584
- dispatchWebhook("task.assigned", { id, assigned_to: input.assigned_to, title: task.title }, d).catch(() => {});
9585
- emitLocalEventHooksQuiet({ type: "task.assigned", payload: { id, assigned_to: input.assigned_to, title: task.title } });
9586
- }
9587
- if (input.status !== undefined && input.status !== task.status) {
9588
- dispatchWebhook("task.status_changed", { id, old_status: task.status, new_status: input.status, title: task.title }, d).catch(() => {});
9589
- emitLocalEventHooksQuiet({ type: "task.status_changed", payload: { id, old_status: task.status, new_status: input.status, title: task.title } });
9590
- }
9591
- if (input.approved_by !== undefined) {
9592
- emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
9593
- }
9594
- return {
10341
+ const updatedTask = {
9595
10342
  ...task,
9596
10343
  ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
9597
10344
  tags: input.tags ?? task.tags,
@@ -9609,6 +10356,22 @@ function updateTask(id, input, db) {
9609
10356
  approved_by: input.approved_by ?? task.approved_by,
9610
10357
  approved_at: input.approved_by ? timestamp : task.approved_at
9611
10358
  };
10359
+ if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
10360
+ const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
10361
+ dispatchWebhook2("task.assigned", payload, d).catch(() => {});
10362
+ emitLocalEventHooksQuiet({ type: "task.assigned", payload });
10363
+ emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to } });
10364
+ }
10365
+ if (input.status !== undefined && input.status !== task.status) {
10366
+ const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
10367
+ dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
10368
+ emitLocalEventHooksQuiet({ type: "task.status_changed", payload });
10369
+ emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status } });
10370
+ }
10371
+ if (input.approved_by !== undefined) {
10372
+ emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
10373
+ }
10374
+ return updatedTask;
9612
10375
  }
9613
10376
  function deleteTask(id, db) {
9614
10377
  const d = db || getDatabase();
@@ -9620,6 +10383,7 @@ var init_task_crud = __esm(() => {
9620
10383
  init_database();
9621
10384
  init_completion_guard();
9622
10385
  init_event_hooks();
10386
+ init_shared_events();
9623
10387
  init_audit();
9624
10388
  init_webhooks();
9625
10389
  init_checklists();
@@ -10375,9 +11139,12 @@ function startTask(id, agentId, db) {
10375
11139
  throw new Error(`Task ${id} could not be started because it changed during claim`);
10376
11140
  }
10377
11141
  logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
10378
- dispatchWebhook("task.started", { id, agent_id: agentId, title: task.title }, d).catch(() => {});
10379
- emitLocalEventHooksQuiet({ type: "task.started", payload: { id, agent_id: agentId, title: task.title } });
10380
- return { ...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 };
11142
+ 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 };
11143
+ const payload = taskEventData(startedTask, { agent_id: agentId });
11144
+ dispatchWebhook2("task.started", payload, d).catch(() => {});
11145
+ emitLocalEventHooksQuiet({ type: "task.started", payload });
11146
+ emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId } });
11147
+ return startedTask;
10381
11148
  }
10382
11149
  function completeTask(id, agentId, db, options) {
10383
11150
  const d = db || getDatabase();
@@ -10413,8 +11180,21 @@ function completeTask(id, agentId, db, options) {
10413
11180
  });
10414
11181
  tx();
10415
11182
  logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
10416
- dispatchWebhook("task.completed", { id, agent_id: agentId, title: task.title, completed_at: timestamp }, d).catch(() => {});
10417
- emitLocalEventHooksQuiet({ type: "task.completed", payload: { id, agent_id: agentId, title: task.title, completed_at: timestamp } });
11183
+ const completedTaskForEvent = {
11184
+ ...task,
11185
+ status: "completed",
11186
+ locked_by: null,
11187
+ locked_at: null,
11188
+ completed_at: timestamp,
11189
+ confidence,
11190
+ version: task.version + 1,
11191
+ updated_at: timestamp,
11192
+ metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
11193
+ };
11194
+ const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
11195
+ dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
11196
+ emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload });
11197
+ emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp } });
10418
11198
  let spawnedTask = null;
10419
11199
  if (task.recurrence_rule && !options?.skip_recurrence) {
10420
11200
  spawnedTask = spawnNextRecurrence(task, d, timestamp);
@@ -10455,8 +11235,12 @@ function completeTask(id, agentId, db, options) {
10455
11235
  if (unblockedDeps.length > 0) {
10456
11236
  meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
10457
11237
  for (const dep of unblockedDeps) {
10458
- dispatchWebhook("task.unblocked", { id: dep.id, unblocked_by: id, title: dep.title }, d).catch(() => {});
10459
- emitLocalEventHooksQuiet({ type: "task.unblocked", payload: { id: dep.id, unblocked_by: id, title: dep.title } });
11238
+ const depTask = getTask(dep.id, d);
11239
+ const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
11240
+ dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
11241
+ emitLocalEventHooksQuiet({ type: "task.unblocked", payload });
11242
+ if (depTask)
11243
+ emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id } });
10460
11244
  }
10461
11245
  }
10462
11246
  return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
@@ -10642,9 +11426,6 @@ function failTask(id, agentId, reason, options, db) {
10642
11426
  const timestamp = now();
10643
11427
  d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
10644
11428
  WHERE id = ?`, [JSON.stringify(meta), timestamp, id]);
10645
- logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
10646
- dispatchWebhook("task.failed", { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title }, d).catch(() => {});
10647
- emitLocalEventHooksQuiet({ type: "task.failed", payload: { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title } });
10648
11429
  const failedTask = {
10649
11430
  ...task,
10650
11431
  status: "failed",
@@ -10654,6 +11435,11 @@ function failTask(id, agentId, reason, options, db) {
10654
11435
  version: task.version + 1,
10655
11436
  updated_at: timestamp
10656
11437
  };
11438
+ logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
11439
+ const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
11440
+ dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
11441
+ emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload });
11442
+ emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning" });
10657
11443
  let retryTask;
10658
11444
  if (options?.retry) {
10659
11445
  const retryCount = (task.retry_count || 0) + 1;
@@ -10728,9 +11514,12 @@ function stealTask(agentId, opts, db) {
10728
11514
  return null;
10729
11515
  logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
10730
11516
  logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
10731
- dispatchWebhook("task.assigned", { id: target.id, agent_id: agentId, title: target.title, stolen_from: target.assigned_to }, d).catch(() => {});
10732
- emitLocalEventHooksQuiet({ type: "task.assigned", payload: { id: target.id, agent_id: agentId, title: target.title, stolen_from: target.assigned_to } });
10733
- return { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
11517
+ const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
11518
+ const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
11519
+ dispatchWebhook2("task.assigned", payload, d).catch(() => {});
11520
+ emitLocalEventHooksQuiet({ type: "task.assigned", payload });
11521
+ emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to } });
11522
+ return stolenTask;
10734
11523
  }
10735
11524
  function claimOrSteal(agentId, filters, db) {
10736
11525
  const d = db || getDatabase();
@@ -10778,6 +11567,7 @@ var init_task_lifecycle = __esm(() => {
10778
11567
  init_database();
10779
11568
  init_completion_guard();
10780
11569
  init_event_hooks();
11570
+ init_shared_events();
10781
11571
  init_audit();
10782
11572
  init_recurrence();
10783
11573
  init_webhooks();
@@ -11465,7 +12255,7 @@ function getTaskWatchers(taskId, db) {
11465
12255
  }
11466
12256
  function notifyWatchers(taskId, event, data, db) {
11467
12257
  const watchers = getTaskWatchers(taskId, db);
11468
- dispatchWebhook(`task.watcher.${event}`, { task_id: taskId, watchers: watchers.map((w) => w.agent_id), ...data }, db).catch(() => {});
12258
+ dispatchWebhook2(`task.watcher.${event}`, { task_id: taskId, watchers: watchers.map((w) => w.agent_id), ...data }, db).catch(() => {});
11469
12259
  }
11470
12260
  function logCost(taskId, tokens, usd, db) {
11471
12261
  const d = db || getDatabase();
@@ -11959,8 +12749,8 @@ var init_boards = __esm(() => {
11959
12749
 
11960
12750
  // src/lib/artifact-store.ts
11961
12751
  import { createHash as createHash2 } from "crypto";
11962
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync2, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
11963
- import { basename, dirname as dirname4, join as join4, resolve as resolve6 } from "path";
12752
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync2, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
12753
+ import { basename, dirname as dirname4, join as join5, resolve as resolve6 } from "path";
11964
12754
  import { tmpdir } from "os";
11965
12755
  function isInMemoryDb2(path) {
11966
12756
  return path === ":memory:" || path.startsWith("file::memory:");
@@ -11972,15 +12762,15 @@ function artifactStoreRoot() {
11972
12762
  return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
11973
12763
  const dbPath = getDatabasePath();
11974
12764
  if (isInMemoryDb2(dbPath))
11975
- return join4(tmpdir(), "hasna-todos-artifacts");
11976
- return join4(dirname4(resolve6(dbPath)), "artifacts");
12765
+ return join5(tmpdir(), "hasna-todos-artifacts");
12766
+ return join5(dirname4(resolve6(dbPath)), "artifacts");
11977
12767
  }
11978
12768
  function artifactStorePath(relativePath) {
11979
12769
  const normalized = relativePath.replace(/\\/g, "/");
11980
12770
  if (normalized.includes("..") || normalized.startsWith("/") || normalized.length === 0) {
11981
12771
  throw new Error("Invalid artifact store path");
11982
12772
  }
11983
- return join4(artifactStoreRoot(), normalized);
12773
+ return join5(artifactStoreRoot(), normalized);
11984
12774
  }
11985
12775
  function sha256(buffer) {
11986
12776
  return createHash2("sha256").update(buffer).digest("hex");
@@ -12021,7 +12811,7 @@ function mediaTypeFor(path, textLike) {
12021
12811
  }
12022
12812
  function storeArtifactContent(input) {
12023
12813
  const sourcePath = resolve6(input.path);
12024
- if (!existsSync5(sourcePath))
12814
+ if (!existsSync6(sourcePath))
12025
12815
  return null;
12026
12816
  const sourceStat = statSync2(sourcePath);
12027
12817
  if (!sourceStat.isFile())
@@ -12038,9 +12828,9 @@ function storeArtifactContent(input) {
12038
12828
  redactionStatus = "redacted";
12039
12829
  }
12040
12830
  const storedSha = sha256(storedBuffer);
12041
- const relativePath = join4("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
12831
+ const relativePath = join5("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
12042
12832
  const destination = artifactStorePath(relativePath);
12043
- if (!existsSync5(destination)) {
12833
+ if (!existsSync6(destination)) {
12044
12834
  mkdirSync4(dirname4(destination), { recursive: true });
12045
12835
  writeFileSync2(destination, storedBuffer);
12046
12836
  }
@@ -12100,7 +12890,7 @@ function verifyStoredArtifact(input) {
12100
12890
  };
12101
12891
  }
12102
12892
  const storedPath = artifactStorePath(store.relative_path);
12103
- if (!existsSync5(storedPath)) {
12893
+ if (!existsSync6(storedPath)) {
12104
12894
  return {
12105
12895
  id: input.id,
12106
12896
  path: input.path,
@@ -13385,17 +14175,73 @@ var init_dispatch_formatter = __esm(() => {
13385
14175
  });
13386
14176
 
13387
14177
  // src/lib/tmux.ts
14178
+ async function inspectTmuxPane(target) {
14179
+ const proc = Bun.spawn([
14180
+ "tmux",
14181
+ "display-message",
14182
+ "-p",
14183
+ "-t",
14184
+ target,
14185
+ "#{pane_id}\t#{pane_current_command}\t#{pane_dead}\t#{pane_input_off}\t#{pane_in_mode}"
14186
+ ], {
14187
+ stdout: "pipe",
14188
+ stderr: "pipe"
14189
+ });
14190
+ const exitCode = await proc.exited;
14191
+ if (exitCode !== 0) {
14192
+ const stderr = await new Response(proc.stderr).text();
14193
+ throw new Error(`tmux target "${target}" not found: ${stderr.trim() || "unknown error"}`);
14194
+ }
14195
+ const stdout = (await new Response(proc.stdout).text()).trim();
14196
+ const [paneId, currentCommand = "", paneDead = "0", inputOff = "0", inMode = "0"] = stdout.split("\t");
14197
+ if (!paneId) {
14198
+ throw new Error(`tmux target "${target}" did not resolve to a pane`);
14199
+ }
14200
+ return {
14201
+ target,
14202
+ paneId,
14203
+ currentCommand,
14204
+ paneDead: paneDead === "1",
14205
+ inputOff: inputOff === "1",
14206
+ inMode: inMode === "1"
14207
+ };
14208
+ }
14209
+ function tmuxPaneBusyStatus(pane) {
14210
+ if (pane.paneDead) {
14211
+ return { busy: true, reason: "pane is dead" };
14212
+ }
14213
+ if (pane.inputOff) {
14214
+ return { busy: true, reason: "pane input is disabled" };
14215
+ }
14216
+ if (pane.inMode) {
14217
+ return { busy: true, reason: "pane is in copy or alternate mode" };
14218
+ }
14219
+ const currentCommand = pane.currentCommand.trim();
14220
+ if (currentCommand && !IDLE_TMUX_COMMANDS.has(currentCommand)) {
14221
+ return { busy: true, reason: `pane is running ${currentCommand}` };
14222
+ }
14223
+ return { busy: false, reason: null };
14224
+ }
13388
14225
  function calculateDelay(message) {
13389
14226
  const len = message.length;
13390
14227
  const extra = Math.floor(len / 100 * 40);
13391
14228
  return Math.min(DELAY_MIN + extra, DELAY_MAX);
13392
14229
  }
13393
- async function sendToTmux(target, message, delayMs, dryRun = false) {
14230
+ async function sendToTmux(target, message, delayMs, options = false) {
14231
+ const opts = typeof options === "boolean" ? { dryRun: options } : options;
14232
+ const dryRun = opts.dryRun ?? false;
13394
14233
  if (dryRun) {
13395
14234
  console.log(`[dry-run] sendToTmux target=${target} delay=${delayMs}ms`);
13396
14235
  console.log(`[dry-run] message: ${message.slice(0, 200)}`);
13397
14236
  return;
13398
14237
  }
14238
+ if (!opts.confirmBusy) {
14239
+ const pane = await inspectTmuxPane(target);
14240
+ const status = tmuxPaneBusyStatus(pane);
14241
+ if (status.busy) {
14242
+ throw new Error(`tmux target "${target}" appears busy (${status.reason}). Re-run with --confirm-busy to send anyway.`);
14243
+ }
14244
+ }
13399
14245
  const sendProc = Bun.spawn(["tmux", "send-keys", "-t", target, message, ""], {
13400
14246
  stdout: "pipe",
13401
14247
  stderr: "pipe"
@@ -13416,7 +14262,21 @@ async function sendToTmux(target, message, delayMs, dryRun = false) {
13416
14262
  throw new Error(`tmux send-keys Enter failed for target "${target}": ${stderr.trim()}`);
13417
14263
  }
13418
14264
  }
13419
- var DELAY_MIN = 3000, DELAY_MAX = 5000;
14265
+ var DELAY_MIN = 3000, DELAY_MAX = 5000, IDLE_TMUX_COMMANDS;
14266
+ var init_tmux = __esm(() => {
14267
+ IDLE_TMUX_COMMANDS = new Set([
14268
+ "bash",
14269
+ "dash",
14270
+ "elvish",
14271
+ "fish",
14272
+ "ksh",
14273
+ "nu",
14274
+ "pwsh",
14275
+ "sh",
14276
+ "tmux",
14277
+ "zsh"
14278
+ ]);
14279
+ });
13420
14280
 
13421
14281
  // src/lib/dispatch.ts
13422
14282
  var exports_dispatch = {};
@@ -13434,7 +14294,10 @@ async function executeDispatch(dispatch, opts = {}, db) {
13434
14294
  }
13435
14295
  const delayMs = dispatch.delay_ms ?? calculateDelay(message);
13436
14296
  try {
13437
- await sendToTmux(dispatch.target_window, message, delayMs, opts.dryRun ?? false);
14297
+ await sendToTmux(dispatch.target_window, message, delayMs, {
14298
+ dryRun: opts.dryRun ?? false,
14299
+ confirmBusy: opts.confirmBusy ?? false
14300
+ });
13438
14301
  createDispatchLog({
13439
14302
  dispatch_id: dispatch.id,
13440
14303
  target_window: dispatch.target_window,
@@ -13493,90 +14356,7 @@ var init_dispatch = __esm(() => {
13493
14356
  init_tasks();
13494
14357
  init_database();
13495
14358
  init_dispatch_formatter();
13496
- });
13497
-
13498
- // src/db/task-lists.ts
13499
- function rowToTaskList(row) {
13500
- return {
13501
- ...row,
13502
- metadata: JSON.parse(row.metadata || "{}")
13503
- };
13504
- }
13505
- function createTaskList(input, db) {
13506
- const d = db || getDatabase();
13507
- const id = uuid();
13508
- const timestamp = now();
13509
- const slug = input.slug || slugify(input.name);
13510
- if (!input.project_id) {
13511
- const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
13512
- if (existing) {
13513
- throw new Error(`Standalone task list with slug "${slug}" already exists`);
13514
- }
13515
- }
13516
- d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
13517
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
13518
- return getTaskList(id, d);
13519
- }
13520
- function getTaskList(id, db) {
13521
- const d = db || getDatabase();
13522
- const row = d.query("SELECT * FROM task_lists WHERE id = ?").get(id);
13523
- return row ? rowToTaskList(row) : null;
13524
- }
13525
- function getTaskListBySlug(slug, projectId, db) {
13526
- const d = db || getDatabase();
13527
- let row;
13528
- if (projectId) {
13529
- row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id = ?").get(slug, projectId);
13530
- } else {
13531
- row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id IS NULL").get(slug);
13532
- }
13533
- return row ? rowToTaskList(row) : null;
13534
- }
13535
- function listTaskLists(projectId, db) {
13536
- const d = db || getDatabase();
13537
- if (projectId) {
13538
- return d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(projectId).map(rowToTaskList);
13539
- }
13540
- return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
13541
- }
13542
- function updateTaskList(id, input, db) {
13543
- const d = db || getDatabase();
13544
- const existing = getTaskList(id, d);
13545
- if (!existing)
13546
- throw new TaskListNotFoundError(id);
13547
- const sets = ["updated_at = ?"];
13548
- const params = [now()];
13549
- if (input.name !== undefined) {
13550
- sets.push("name = ?");
13551
- params.push(input.name);
13552
- }
13553
- if (input.description !== undefined) {
13554
- sets.push("description = ?");
13555
- params.push(input.description);
13556
- }
13557
- if (input.metadata !== undefined) {
13558
- sets.push("metadata = ?");
13559
- params.push(JSON.stringify(input.metadata));
13560
- }
13561
- params.push(id);
13562
- d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
13563
- return getTaskList(id, d);
13564
- }
13565
- function deleteTaskList(id, db) {
13566
- const d = db || getDatabase();
13567
- return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
13568
- }
13569
- function ensureTaskList(name, slug, projectId, db) {
13570
- const d = db || getDatabase();
13571
- const existing = getTaskListBySlug(slug, projectId, d);
13572
- if (existing)
13573
- return existing;
13574
- return createTaskList({ name, slug, project_id: projectId }, d);
13575
- }
13576
- var init_task_lists = __esm(() => {
13577
- init_types();
13578
- init_database();
13579
- init_projects();
14359
+ init_tmux();
13580
14360
  });
13581
14361
 
13582
14362
  // src/mcp/tools/dispatch.ts
@@ -13587,8 +14367,9 @@ function registerDispatchTools(server, { shouldRegisterTool, resolveId, formatEr
13587
14367
  target: exports_external.string().describe("tmux target \u2014 window name, session:window, or session:window.pane"),
13588
14368
  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."),
13589
14369
  scheduled_at: exports_external.string().optional().describe("ISO datetime to schedule the dispatch for. Fires immediately if omitted."),
14370
+ confirm_busy: exports_external.boolean().optional().describe("Send even if the target tmux pane appears busy. Default: false."),
13590
14371
  dry_run: exports_external.boolean().optional().describe("Preview the formatted message without sending. Default: false.")
13591
- }, async ({ task_ids, target, delay_ms, scheduled_at, dry_run }) => {
14372
+ }, async ({ task_ids, target, delay_ms, scheduled_at, confirm_busy, dry_run }) => {
13592
14373
  try {
13593
14374
  const db = getDatabase();
13594
14375
  const resolvedIds = task_ids.map((id) => resolveId(id));
@@ -13604,7 +14385,7 @@ ${message}` }]
13604
14385
  }
13605
14386
  const dispatch = createDispatch({ task_ids: resolvedIds, target_window: target, message, delay_ms: effectiveDelay, scheduled_at }, db);
13606
14387
  if (!scheduled_at)
13607
- await executeDispatch(dispatch, {}, db);
14388
+ await executeDispatch(dispatch, { confirmBusy: confirm_busy ?? false }, db);
13608
14389
  return {
13609
14390
  content: [{
13610
14391
  type: "text",
@@ -13630,8 +14411,9 @@ ${message}`
13630
14411
  filter_status: exports_external.array(exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"])).optional().describe("Only include tasks with these statuses. Default: pending."),
13631
14412
  delay_ms: exports_external.number().optional().describe("Delay in ms between sending and Enter. Auto-calculated if omitted."),
13632
14413
  scheduled_at: exports_external.string().optional().describe("ISO datetime to schedule. Fires immediately if omitted."),
14414
+ confirm_busy: exports_external.boolean().optional().describe("Send even if the target tmux pane appears busy. Default: false."),
13633
14415
  dry_run: exports_external.boolean().optional().describe("Preview without sending. Default: false.")
13634
- }, async ({ task_list_id, target, filter_status, delay_ms, scheduled_at, dry_run }) => {
14416
+ }, async ({ task_list_id, target, filter_status, delay_ms, scheduled_at, confirm_busy, dry_run }) => {
13635
14417
  try {
13636
14418
  const db = getDatabase();
13637
14419
  const resolvedListId = resolveId(task_list_id, "task_lists");
@@ -13651,7 +14433,7 @@ ${message}` }]
13651
14433
  }
13652
14434
  const dispatch = createDispatch({ title: `Task list: ${taskList.name}`, task_list_id: resolvedListId, target_window: target, message, delay_ms: effectiveDelay, scheduled_at }, db);
13653
14435
  if (!scheduled_at)
13654
- await executeDispatch(dispatch, {}, db);
14436
+ await executeDispatch(dispatch, { confirmBusy: confirm_busy ?? false }, db);
13655
14437
  return {
13656
14438
  content: [{
13657
14439
  type: "text",
@@ -13678,15 +14460,16 @@ ${message}`
13678
14460
  task_list_id: exports_external.string().optional().describe("Task list ID to dispatch (use this or task_ids)"),
13679
14461
  stagger_ms: exports_external.number().optional().describe("Delay between each window dispatch. Default: 500ms."),
13680
14462
  delay_ms: exports_external.number().optional().describe("Delay between message send and Enter. Auto-calculated if omitted."),
14463
+ confirm_busy: exports_external.boolean().optional().describe("Send even if target tmux panes appear busy. Default: false."),
13681
14464
  dry_run: exports_external.boolean().optional().describe("Preview without sending. Default: false.")
13682
- }, async ({ targets, task_ids, task_list_id, stagger_ms, delay_ms, dry_run }) => {
14465
+ }, async ({ targets, task_ids, task_list_id, stagger_ms, delay_ms, confirm_busy, dry_run }) => {
13683
14466
  try {
13684
14467
  if (!task_ids && !task_list_id)
13685
14468
  throw new Error("Either task_ids or task_list_id is required");
13686
14469
  const db = getDatabase();
13687
14470
  const resolvedTaskIds = task_ids ? task_ids.map((id) => resolveId(id)) : undefined;
13688
14471
  const resolvedListId = task_list_id ? resolveId(task_list_id, "task_lists") : undefined;
13689
- const dispatches = await dispatchToMultiple({ targets, task_ids: resolvedTaskIds, task_list_id: resolvedListId, delay_ms, stagger_ms }, { dryRun: dry_run }, db);
14472
+ const dispatches = await dispatchToMultiple({ targets, task_ids: resolvedTaskIds, task_list_id: resolvedListId, delay_ms, stagger_ms }, { dryRun: dry_run, confirmBusy: confirm_busy ?? false }, db);
13690
14473
  const lines = dispatches.map((d) => `${d.target_window}: ${d.id} [${d.status}]`);
13691
14474
  return { content: [{ type: "text", text: `Dispatched to ${dispatches.length} target(s):
13692
14475
  ${lines.join(`
@@ -13731,11 +14514,12 @@ ${lines.join(`
13731
14514
  if (shouldRegisterTool("run_due_dispatches")) {
13732
14515
  server.tool("run_due_dispatches", "Manually trigger all pending dispatches that are due (scheduled_at <= now). Returns the count fired.", {
13733
14516
  dry_run: exports_external.boolean().optional().describe("Preview without sending. Default: false."),
14517
+ confirm_busy: exports_external.boolean().optional().describe("Send even if target tmux panes appear busy. Default: false."),
13734
14518
  all: exports_external.boolean().optional().describe("Ignore scheduled_at and fire all pending dispatches immediately.")
13735
- }, async ({ dry_run }) => {
14519
+ }, async ({ dry_run, confirm_busy }) => {
13736
14520
  try {
13737
14521
  const { runDueDispatches: runDueDispatches2 } = await Promise.resolve().then(() => (init_dispatch(), exports_dispatch));
13738
- const count = await runDueDispatches2({ dryRun: dry_run });
14522
+ const count = await runDueDispatches2({ dryRun: dry_run, confirmBusy: confirm_busy ?? false });
13739
14523
  return { content: [{ type: "text", text: `Fired ${count} dispatch(es).` }] };
13740
14524
  } catch (e) {
13741
14525
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
@@ -13748,6 +14532,7 @@ var init_dispatch2 = __esm(() => {
13748
14532
  init_dispatches();
13749
14533
  init_dispatch();
13750
14534
  init_dispatch_formatter();
14535
+ init_tmux();
13751
14536
  init_tasks();
13752
14537
  init_task_lists();
13753
14538
  init_database();
@@ -14555,7 +15340,7 @@ var init_task_crud2 = __esm(() => {
14555
15340
  });
14556
15341
 
14557
15342
  // src/lib/project-bootstrap.ts
14558
- import { existsSync as existsSync6, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
15343
+ import { existsSync as existsSync7, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
14559
15344
  import { basename as basename2, dirname as dirname5, resolve as resolve7 } from "path";
14560
15345
  function safeStat(path) {
14561
15346
  try {
@@ -14574,7 +15359,7 @@ function canonicalPath(input) {
14574
15359
  function findUp(start, marker) {
14575
15360
  let current = canonicalPath(start);
14576
15361
  while (true) {
14577
- if (existsSync6(resolve7(current, marker)))
15362
+ if (existsSync7(resolve7(current, marker)))
14578
15363
  return current;
14579
15364
  const parent = dirname5(current);
14580
15365
  if (parent === current)
@@ -14586,7 +15371,7 @@ function readPackageJson(path) {
14586
15371
  if (!path)
14587
15372
  return null;
14588
15373
  const file = resolve7(path, "package.json");
14589
- if (!existsSync6(file))
15374
+ if (!existsSync7(file))
14590
15375
  return null;
14591
15376
  try {
14592
15377
  const parsed = JSON.parse(readFileSync3(file, "utf-8"));
@@ -14608,7 +15393,7 @@ function workspaceMarker(root, rootPackage) {
14608
15393
  if (rootPackage?.workspaces)
14609
15394
  markers.push("package.json#workspaces");
14610
15395
  for (const marker of ["pnpm-workspace.yaml", "turbo.json", "nx.json", "lerna.json", "rush.json", "bun.lock", "bun.lockb"]) {
14611
- if (existsSync6(resolve7(root, marker)))
15396
+ if (existsSync7(resolve7(root, marker)))
14612
15397
  markers.push(marker);
14613
15398
  }
14614
15399
  const kind = markers.find((marker) => marker !== "bun.lock" && marker !== "bun.lockb") ?? null;
@@ -14909,7 +15694,7 @@ var init_tags = __esm(() => {
14909
15694
  });
14910
15695
 
14911
15696
  // src/lib/retention-cleanup.ts
14912
- import { existsSync as existsSync7, unlinkSync } from "fs";
15697
+ import { existsSync as existsSync8, unlinkSync } from "fs";
14913
15698
  function normalizeScopes(scopes) {
14914
15699
  if (!scopes || scopes.length === 0)
14915
15700
  return [...ALL_SCOPES];
@@ -15112,7 +15897,7 @@ function applyRetentionCleanup(input, db) {
15112
15897
  for (const artifact of report.candidates.artifact_files) {
15113
15898
  try {
15114
15899
  const path = artifactStorePath(artifact.relative_path);
15115
- if (!existsSync7(path)) {
15900
+ if (!existsSync8(path)) {
15116
15901
  report.warnings.push(`stored artifact already missing: ${artifact.relative_path}`);
15117
15902
  continue;
15118
15903
  }
@@ -15139,8 +15924,8 @@ var init_retention_cleanup = __esm(() => {
15139
15924
  });
15140
15925
 
15141
15926
  // src/lib/mention-resolver.ts
15142
- import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
15143
- import { basename as basename3, isAbsolute, join as join5, relative as relative3, resolve as resolve8, sep } from "path";
15927
+ import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
15928
+ import { basename as basename3, isAbsolute, join as join6, relative as relative3, resolve as resolve8, sep } from "path";
15144
15929
  function blankResolution(parsed) {
15145
15930
  return {
15146
15931
  input: parsed.input,
@@ -15238,7 +16023,7 @@ function resolveFile(parsed, workspace) {
15238
16023
  return resolution;
15239
16024
  }
15240
16025
  resolution.path = relPath;
15241
- if (!existsSync8(absolutePath)) {
16026
+ if (!existsSync9(absolutePath)) {
15242
16027
  resolution.warnings.push("file does not exist in the local workspace");
15243
16028
  return resolution;
15244
16029
  }
@@ -15271,7 +16056,7 @@ function walkSourceFiles(root, current = root, files = []) {
15271
16056
  if (SKIP_DIRS.has(entry.name))
15272
16057
  continue;
15273
16058
  }
15274
- const absolutePath = join5(current, entry.name);
16059
+ const absolutePath = join6(current, entry.name);
15275
16060
  if (entry.isDirectory()) {
15276
16061
  if (!SKIP_DIRS.has(entry.name))
15277
16062
  walkSourceFiles(root, absolutePath, files);
@@ -16538,8 +17323,8 @@ var init_local_notifications = __esm(() => {
16538
17323
  });
16539
17324
 
16540
17325
  // src/lib/local-encryption.ts
16541
- import { createCipheriv, createDecipheriv, createHash as createHash3, randomBytes, scryptSync, timingSafeEqual } from "crypto";
16542
- function now2() {
17326
+ import { createCipheriv, createDecipheriv, createHash as createHash3, randomBytes, scryptSync, timingSafeEqual as timingSafeEqual2 } from "crypto";
17327
+ function now3() {
16543
17328
  return new Date().toISOString();
16544
17329
  }
16545
17330
  function sha2562(value) {
@@ -16576,7 +17361,7 @@ function upsertEncryptionProfile(input) {
16576
17361
  const name = normalizeProfileName(input.name);
16577
17362
  const config = loadConfig();
16578
17363
  const existing = config.encryption_profiles?.[name];
16579
- const timestamp = now2();
17364
+ const timestamp = now3();
16580
17365
  const profile = {
16581
17366
  name,
16582
17367
  algorithm: "aes-256-gcm",
@@ -16633,7 +17418,7 @@ function encryptString(plaintext, options = {}) {
16633
17418
  return {
16634
17419
  schemaVersion: TODOS_ENCRYPTION_SCHEMA_VERSION,
16635
17420
  kind: TODOS_ENCRYPTED_VALUE_KIND,
16636
- encryptedAt: options.encryptedAt ?? now2(),
17421
+ encryptedAt: options.encryptedAt ?? now3(),
16637
17422
  profile: profile.name,
16638
17423
  key_env: profile.key_env,
16639
17424
  algorithm: "aes-256-gcm",
@@ -16668,7 +17453,7 @@ function decryptString(envelope, env = process.env) {
16668
17453
  ]).toString("utf8");
16669
17454
  const expected = Buffer.from(envelope.plaintext_sha256, "hex");
16670
17455
  const actual = Buffer.from(sha2562(plaintext), "hex");
16671
- if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
17456
+ if (expected.length !== actual.length || !timingSafeEqual2(expected, actual)) {
16672
17457
  throw new EncryptedPayloadError("decrypted payload checksum mismatch");
16673
17458
  }
16674
17459
  return plaintext;
@@ -18191,7 +18976,7 @@ function createRoadmap(input) {
18191
18976
  if (!name)
18192
18977
  throw new Error("Roadmap name is required");
18193
18978
  const store = readStore();
18194
- const now3 = timestamp();
18979
+ const now4 = timestamp();
18195
18980
  const roadmap = {
18196
18981
  id: newId("roadmap"),
18197
18982
  name,
@@ -18202,8 +18987,8 @@ function createRoadmap(input) {
18202
18987
  agent_id: cleanString(input.agent_id),
18203
18988
  release: cleanString(input.release),
18204
18989
  milestone_ids: [],
18205
- created_at: now3,
18206
- updated_at: now3
18990
+ created_at: now4,
18991
+ updated_at: now4
18207
18992
  };
18208
18993
  store.roadmaps[roadmap.id] = roadmap;
18209
18994
  writeStore(store);
@@ -18255,7 +19040,7 @@ function createMilestone(input) {
18255
19040
  const title = input.title.trim();
18256
19041
  if (!title)
18257
19042
  throw new Error("Milestone title is required");
18258
- const now3 = timestamp();
19043
+ const now4 = timestamp();
18259
19044
  const milestone = {
18260
19045
  id: newId("milestone"),
18261
19046
  roadmap_id: roadmapId,
@@ -18270,11 +19055,11 @@ function createMilestone(input) {
18270
19055
  run_ids: cleanList(input.run_ids),
18271
19056
  release: cleanString(input.release ?? roadmap.release ?? undefined),
18272
19057
  tags: cleanList(input.tags),
18273
- created_at: now3,
18274
- updated_at: now3
19058
+ created_at: now4,
19059
+ updated_at: now4
18275
19060
  };
18276
19061
  store.milestones[milestone.id] = milestone;
18277
- store.roadmaps[roadmapId] = { ...roadmap, milestone_ids: cleanList([...roadmap.milestone_ids, milestone.id]), updated_at: now3 };
19062
+ store.roadmaps[roadmapId] = { ...roadmap, milestone_ids: cleanList([...roadmap.milestone_ids, milestone.id]), updated_at: now4 };
18278
19063
  writeStore(store);
18279
19064
  return milestone;
18280
19065
  }
@@ -18329,7 +19114,7 @@ function upsertReleaseGroup(input) {
18329
19114
  throw new Error("Release group name is required");
18330
19115
  const key = releaseKey(roadmapId, name);
18331
19116
  const existing = store.releases[key];
18332
- const now3 = timestamp();
19117
+ const now4 = timestamp();
18333
19118
  const release = {
18334
19119
  name,
18335
19120
  version: input.version === undefined ? existing?.version ?? null : cleanString(input.version),
@@ -18340,8 +19125,8 @@ function upsertReleaseGroup(input) {
18340
19125
  plan_ids: input.plan_ids === undefined ? existing?.plan_ids ?? [] : cleanList(input.plan_ids),
18341
19126
  run_ids: input.run_ids === undefined ? existing?.run_ids ?? [] : cleanList(input.run_ids),
18342
19127
  notes: input.notes === undefined ? existing?.notes ?? null : cleanString(input.notes),
18343
- created_at: existing?.created_at ?? now3,
18344
- updated_at: now3
19128
+ created_at: existing?.created_at ?? now4,
19129
+ updated_at: now4
18345
19130
  };
18346
19131
  store.releases[key] = release;
18347
19132
  writeStore(store);
@@ -18540,7 +19325,7 @@ function upsertCapacityProfile(input) {
18540
19325
  const store = readStore2();
18541
19326
  const key = profileKey(agentId, projectId);
18542
19327
  const existing = store.profiles[key];
18543
- const now3 = timestamp2();
19328
+ const now4 = timestamp2();
18544
19329
  const profile = {
18545
19330
  id: existing?.id ?? key,
18546
19331
  agent_id: agentId,
@@ -18548,8 +19333,8 @@ function upsertCapacityProfile(input) {
18548
19333
  minutes_per_day: assertMinutes(input.minutes_per_day),
18549
19334
  working_days: normalizeWorkingDays(input.working_days),
18550
19335
  effective_from: cleanString2(input.effective_from),
18551
- created_at: existing?.created_at ?? now3,
18552
- updated_at: now3
19336
+ created_at: existing?.created_at ?? now4,
19337
+ updated_at: now4
18553
19338
  };
18554
19339
  store.profiles[key] = profile;
18555
19340
  writeStore2(store);
@@ -18958,7 +19743,7 @@ var init_audit_ledger = __esm(() => {
18958
19743
 
18959
19744
  // src/lib/release-compatibility.ts
18960
19745
  import { readFileSync as readFileSync5 } from "fs";
18961
- import { join as join6, resolve as resolve10 } from "path";
19746
+ import { join as join7, resolve as resolve10 } from "path";
18962
19747
  import { Database as Database2 } from "bun:sqlite";
18963
19748
  function pass(id, message, details) {
18964
19749
  return { id, status: "passed", message, details };
@@ -18970,7 +19755,7 @@ function warn(id, message, details) {
18970
19755
  return { id, status: "warning", message, details };
18971
19756
  }
18972
19757
  function readPackageJson2(root) {
18973
- return JSON.parse(readFileSync5(join6(root, "package.json"), "utf8"));
19758
+ return JSON.parse(readFileSync5(join7(root, "package.json"), "utf8"));
18974
19759
  }
18975
19760
  function sortedKeys(value) {
18976
19761
  return Object.keys(value ?? {}).sort((left, right) => left.localeCompare(right));
@@ -19239,6 +20024,9 @@ function hasFts(db) {
19239
20024
  function escapeFtsQuery(q) {
19240
20025
  return q.replace(/["*^()]/g, " ").trim().split(/\s+/).filter(Boolean).map((token) => `"${token}"*`).join(" ");
19241
20026
  }
20027
+ function shouldUseFts(q) {
20028
+ return /^[\p{L}\p{N}_\-\s]+$/u.test(q);
20029
+ }
19242
20030
  function searchTasks(options, projectId, taskListId, db) {
19243
20031
  const opts = typeof options === "string" ? { query: options || undefined, project_id: projectId, task_list_id: taskListId } : options;
19244
20032
  const d = db || getDatabase();
@@ -19247,7 +20035,8 @@ function searchTasks(options, projectId, taskListId, db) {
19247
20035
  let sql;
19248
20036
  const raw = opts.query?.trim() ?? "";
19249
20037
  const q = raw === "*" ? "" : raw;
19250
- if (hasFts(d) && q) {
20038
+ const useFts = hasFts(d) && q && shouldUseFts(q);
20039
+ if (useFts) {
19251
20040
  const ftsQuery = escapeFtsQuery(q);
19252
20041
  sql = `SELECT t.* FROM tasks t
19253
20042
  INNER JOIN tasks_fts fts ON fts.rowid = t.rowid
@@ -19255,8 +20044,16 @@ function searchTasks(options, projectId, taskListId, db) {
19255
20044
  params.push(ftsQuery);
19256
20045
  } else if (q) {
19257
20046
  const pattern = `%${q}%`;
19258
- sql = `SELECT * FROM tasks t WHERE (t.title LIKE ? OR t.description LIKE ? OR EXISTS (SELECT 1 FROM task_tags WHERE task_tags.task_id = t.id AND tag LIKE ?))`;
19259
- params.push(pattern, pattern, pattern);
20047
+ sql = `SELECT * FROM tasks t WHERE (
20048
+ t.id LIKE ?
20049
+ OR t.short_id LIKE ?
20050
+ OR t.title LIKE ?
20051
+ OR t.description LIKE ?
20052
+ OR t.working_dir LIKE ?
20053
+ OR t.metadata LIKE ?
20054
+ OR EXISTS (SELECT 1 FROM task_tags WHERE task_tags.task_id = t.id AND tag LIKE ?)
20055
+ )`;
20056
+ params.push(pattern, pattern, pattern, pattern, pattern, pattern, pattern);
19260
20057
  } else {
19261
20058
  sql = `SELECT * FROM tasks t WHERE 1=1`;
19262
20059
  }
@@ -19312,7 +20109,7 @@ function searchTasks(options, projectId, taskListId, db) {
19312
20109
  } else if (opts.is_blocked === false) {
19313
20110
  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')";
19314
20111
  }
19315
- if (hasFts(d) && q) {
20112
+ if (useFts) {
19316
20113
  sql += ` ORDER BY bm25(tasks_fts),
19317
20114
  CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
19318
20115
  t.created_at DESC`;
@@ -22407,7 +23204,7 @@ function registerTaskWorkflowTools(server, ctx) {
22407
23204
  const globalRole = n.agent.role ? ` [${n.agent.role}]` : "";
22408
23205
  const lead = n.is_lead ? " \u2605" : "";
22409
23206
  const lastSeen = new Date(n.agent.last_seen_at).getTime();
22410
- const active = now3 - lastSeen < ACTIVE_MS ? " \u25CF" : " \u25CB";
23207
+ const active = now4 - lastSeen < ACTIVE_MS ? " \u25CF" : " \u25CB";
22411
23208
  const line = `${prefix}${active} ${n.agent.name}${title}${globalRole}${lead}`;
22412
23209
  const children = n.reports.length > 0 ? `
22413
23210
  ` + render(n.reports, indent + 1) : "";
@@ -22420,7 +23217,7 @@ function registerTaskWorkflowTools(server, ctx) {
22420
23217
  if (format === "json") {
22421
23218
  return { content: [{ type: "text", text: JSON.stringify(tree, null, 2) }] };
22422
23219
  }
22423
- const now3 = Date.now();
23220
+ const now4 = Date.now();
22424
23221
  const ACTIVE_MS = 30 * 60 * 1000;
22425
23222
  const text = tree.length > 0 ? render(tree) : "No agents in org chart.";
22426
23223
  return { content: [{ type: "text", text }] };
@@ -22542,8 +23339,8 @@ var exports_doctor = {};
22542
23339
  __export(exports_doctor, {
22543
23340
  runTodosDoctor: () => runTodosDoctor
22544
23341
  });
22545
- import { chmodSync, copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync5, statSync as statSync5 } from "fs";
22546
- import { basename as basename4, dirname as dirname6, join as join7 } from "path";
23342
+ import { chmodSync, copyFileSync, existsSync as existsSync10, mkdirSync as mkdirSync5, statSync as statSync5 } from "fs";
23343
+ import { basename as basename4, dirname as dirname6, join as join8 } from "path";
22547
23344
  function tableExists(db, table) {
22548
23345
  return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
22549
23346
  }
@@ -22637,7 +23434,7 @@ function findMissingProjectRoots(db) {
22637
23434
  continue;
22638
23435
  if (!row.path.startsWith("/"))
22639
23436
  continue;
22640
- if (!existsSync9(row.path))
23437
+ if (!existsSync10(row.path))
22641
23438
  missing++;
22642
23439
  }
22643
23440
  return missing;
@@ -22697,16 +23494,16 @@ function databasePermissionsAreUnsafe(dbPath) {
22697
23494
  function createBackup(dbPath) {
22698
23495
  if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
22699
23496
  return;
22700
- if (!existsSync9(dbPath))
23497
+ if (!existsSync10(dbPath))
22701
23498
  return;
22702
23499
  const stamp = now().replace(/[:.]/g, "-");
22703
- const backupDir = join7(dirname6(dbPath), `${basename4(dbPath)}.backup-${stamp}`);
23500
+ const backupDir = join8(dirname6(dbPath), `${basename4(dbPath)}.backup-${stamp}`);
22704
23501
  const files = [];
22705
23502
  mkdirSync5(backupDir, { recursive: true });
22706
23503
  for (const source of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
22707
- if (!existsSync9(source))
23504
+ if (!existsSync10(source))
22708
23505
  continue;
22709
- const target = join7(backupDir, basename4(source));
23506
+ const target = join8(backupDir, basename4(source));
22710
23507
  copyFileSync(source, target);
22711
23508
  files.push(target);
22712
23509
  }
@@ -23010,14 +23807,14 @@ function registerTaskAutoTools(server, ctx) {
23010
23807
  return { content: [{ type: "text", text: "No agent_id provided and no agent focus is active." }], isError: true };
23011
23808
  }
23012
23809
  const assigned = listTasks3({ assigned_to: effectiveAgentId, limit: 500 }, undefined);
23013
- const now3 = Date.now();
23014
- const dueSoonCutoff = now3 + 24 * 60 * 60 * 1000;
23810
+ const now4 = Date.now();
23811
+ const dueSoonCutoff = now4 + 24 * 60 * 60 * 1000;
23015
23812
  const blocked = getBlockedTasks2().filter((t) => t.assigned_to === effectiveAgentId);
23016
23813
  const workload = {
23017
23814
  in_progress: assigned.filter((t) => t.status === "in_progress").length,
23018
23815
  pending: assigned.filter((t) => t.status === "pending").length,
23019
- completed_recent: assigned.filter((t) => t.status === "completed" && t.completed_at && now3 - new Date(t.completed_at).getTime() <= 7 * 24 * 60 * 60 * 1000).length,
23020
- due_soon: assigned.filter((t) => t.due_at && new Date(t.due_at).getTime() <= dueSoonCutoff && new Date(t.due_at).getTime() >= now3 && !["completed", "cancelled", "failed"].includes(t.status)).length,
23816
+ completed_recent: assigned.filter((t) => t.status === "completed" && t.completed_at && now4 - new Date(t.completed_at).getTime() <= 7 * 24 * 60 * 60 * 1000).length,
23817
+ 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,
23021
23818
  blocked: blocked.length
23022
23819
  };
23023
23820
  const lines = [
@@ -23244,7 +24041,7 @@ function limits(input) {
23244
24041
  stale_after_hours: clamp(input.stale_after_hours, DEFAULT_LIMITS.stale_after_hours, 24 * 365)
23245
24042
  };
23246
24043
  }
23247
- function truncate(value, max) {
24044
+ function truncate2(value, max) {
23248
24045
  if (!value)
23249
24046
  return value ?? null;
23250
24047
  const redacted = redactEvidenceText(value);
@@ -23265,9 +24062,9 @@ function acceptanceCriteria(task, maxText) {
23265
24062
  const metadata = task.metadata || {};
23266
24063
  const raw = metadata["acceptance_criteria"] ?? metadata["acceptanceCriteria"] ?? metadata["criteria"];
23267
24064
  if (Array.isArray(raw))
23268
- return raw.map((item) => truncate(String(item), maxText)).filter((item) => Boolean(item));
24065
+ return raw.map((item) => truncate2(String(item), maxText)).filter((item) => Boolean(item));
23269
24066
  if (typeof raw === "string") {
23270
- return raw.split(/\r?\n/).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).map((line) => truncate(line, maxText)).filter((item) => Boolean(item));
24067
+ return raw.split(/\r?\n/).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).map((line) => truncate2(line, maxText)).filter((item) => Boolean(item));
23271
24068
  }
23272
24069
  return [];
23273
24070
  }
@@ -23290,7 +24087,7 @@ function addFile(files, path, source, base) {
23290
24087
  path,
23291
24088
  status: base?.status || "active",
23292
24089
  agent_id: base?.agent_id ?? null,
23293
- note: truncate(base?.note, 240),
24090
+ note: truncate2(base?.note, 240),
23294
24091
  updated_at: base?.updated_at || "",
23295
24092
  sources: [source]
23296
24093
  });
@@ -23348,7 +24145,7 @@ function estimateTokens(value) {
23348
24145
  return Math.max(1, Math.ceil((text || "").length / 4));
23349
24146
  }
23350
24147
  function summarizeStrings(values, maxChars) {
23351
- return truncate(values.filter(Boolean).join("; "), maxChars) || "No local details were available before this section was omitted.";
24148
+ return truncate2(values.filter(Boolean).join("; "), maxChars) || "No local details were available before this section was omitted.";
23352
24149
  }
23353
24150
  function summarizeSection(pack, section, maxChars) {
23354
24151
  if (section === "project")
@@ -23539,8 +24336,8 @@ function createAgentContextPack(input, db) {
23539
24336
  ...taskFiles.map((file) => file.updated_at)
23540
24337
  ], task.updated_at);
23541
24338
  const warnings = [];
23542
- const now3 = input.now ? new Date(input.now) : new Date;
23543
- if (Date.parse(task.updated_at) < now3.getTime() - limit.stale_after_hours * 60 * 60 * 1000) {
24339
+ const now4 = input.now ? new Date(input.now) : new Date;
24340
+ if (Date.parse(task.updated_at) < now4.getTime() - limit.stale_after_hours * 60 * 60 * 1000) {
23544
24341
  warnings.push(`task state is older than ${limit.stale_after_hours} hours`);
23545
24342
  }
23546
24343
  if (comments.length > recentComments.length)
@@ -23555,7 +24352,7 @@ function createAgentContextPack(input, db) {
23555
24352
  id: task.id,
23556
24353
  short_id: task.short_id,
23557
24354
  title: redactEvidenceText(task.title),
23558
- description: truncate(task.description, limit.max_text_chars),
24355
+ description: truncate2(task.description, limit.max_text_chars),
23559
24356
  status: task.status,
23560
24357
  priority: task.priority,
23561
24358
  assigned_to: task.assigned_to,
@@ -23579,7 +24376,7 @@ function createAgentContextPack(input, db) {
23579
24376
  plan: plan ? {
23580
24377
  id: plan.id,
23581
24378
  name: plan.name,
23582
- description: truncate(plan.description, limit.max_text_chars),
24379
+ description: truncate2(plan.description, limit.max_text_chars),
23583
24380
  status: plan.status,
23584
24381
  agent_id: plan.agent_id,
23585
24382
  tasks: planTasks.slice(0, limit.plan_task_limit).map(taskSummary).filter((item) => Boolean(item)),
@@ -23598,7 +24395,7 @@ function createAgentContextPack(input, db) {
23598
24395
  type: comment.type,
23599
24396
  progress_pct: comment.progress_pct,
23600
24397
  created_at: comment.created_at,
23601
- content: truncate(comment.content, limit.max_text_chars) || ""
24398
+ content: truncate2(comment.content, limit.max_text_chars) || ""
23602
24399
  })),
23603
24400
  omitted: Math.max(0, comments.length - recentComments.length)
23604
24401
  },
@@ -23606,7 +24403,7 @@ function createAgentContextPack(input, db) {
23606
24403
  traceability: {
23607
24404
  commits: traceability.commits.map((commit) => ({
23608
24405
  sha: commit.sha,
23609
- message: truncate(commit.message, 240),
24406
+ message: truncate2(commit.message, 240),
23610
24407
  files_changed: commit.files_changed,
23611
24408
  committed_at: commit.committed_at
23612
24409
  })),
@@ -23614,7 +24411,7 @@ function createAgentContextPack(input, db) {
23614
24411
  verifications: verifications.map((verification) => ({
23615
24412
  command: verification.command,
23616
24413
  status: verification.status,
23617
- output_summary: truncate(verification.output_summary, limit.max_text_chars),
24414
+ output_summary: truncate2(verification.output_summary, limit.max_text_chars),
23618
24415
  artifact_path: verification.artifact_path,
23619
24416
  run_at: verification.run_at
23620
24417
  })),
@@ -23625,14 +24422,14 @@ function createAgentContextPack(input, db) {
23625
24422
  id: ledger.run.id,
23626
24423
  title: ledger.run.title,
23627
24424
  status: ledger.run.status,
23628
- summary: truncate(ledger.run.summary, limit.max_text_chars),
24425
+ summary: truncate2(ledger.run.summary, limit.max_text_chars),
23629
24426
  agent_id: ledger.run.agent_id,
23630
24427
  started_at: ledger.run.started_at,
23631
24428
  completed_at: ledger.run.completed_at,
23632
- events: ledger.events.map((event) => ({ event_type: event.event_type, message: truncate(event.message, 500), created_at: event.created_at })),
23633
- commands: ledger.commands.map((command) => ({ command: command.command, status: command.status, output_summary: truncate(command.output_summary, limit.max_text_chars), artifact_path: command.artifact_path })),
23634
- files: ledger.files.map((file) => ({ path: file.path, status: file.status, note: truncate(file.note, 240) })),
23635
- artifacts: ledger.artifacts.map((artifact) => ({ path: artifact.path, artifact_type: artifact.artifact_type, description: truncate(artifact.description, 240), sha256: artifact.sha256 }))
24429
+ events: ledger.events.map((event) => ({ event_type: event.event_type, message: truncate2(event.message, 500), created_at: event.created_at })),
24430
+ 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 })),
24431
+ files: ledger.files.map((file) => ({ path: file.path, status: file.status, note: truncate2(file.note, 240) })),
24432
+ artifacts: ledger.artifacts.map((artifact) => ({ path: artifact.path, artifact_type: artifact.artifact_type, description: truncate2(artifact.description, 240), sha256: artifact.sha256 }))
23636
24433
  })),
23637
24434
  omitted: Math.max(0, runs.length - selectedRuns.length)
23638
24435
  },
@@ -23737,7 +24534,7 @@ function renderAgentContextPackCompactMarkdown(pack) {
23737
24534
  const lines = [
23738
24535
  `# Context: ${pack.task.title}`,
23739
24536
  `${pack.task.status} | ${pack.task.priority} | ${pack.task.short_id || pack.task.id.slice(0, 8)}`,
23740
- pack.task.description ? truncate(pack.task.description, Math.min(pack.limits.summary_char_limit, 700)) : null,
24537
+ pack.task.description ? truncate2(pack.task.description, Math.min(pack.limits.summary_char_limit, 700)) : null,
23741
24538
  "",
23742
24539
  "## Must Know",
23743
24540
  bullet([
@@ -24987,7 +25784,7 @@ var init_agent_run_dispatcher = __esm(() => {
24987
25784
  });
24988
25785
 
24989
25786
  // src/lib/verification-providers.ts
24990
- import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
25787
+ import { existsSync as existsSync11, readFileSync as readFileSync6 } from "fs";
24991
25788
  function normalizeName5(name) {
24992
25789
  const normalized = name.trim().toLowerCase();
24993
25790
  if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(normalized)) {
@@ -25139,7 +25936,7 @@ Timed out after ${provider.timeout_ms}ms`);
25139
25936
  };
25140
25937
  }
25141
25938
  function runCiLogProvider(input) {
25142
- const text = input.log_text ?? (input.log_path && existsSync10(input.log_path) ? readFileSync6(input.log_path, "utf-8") : "");
25939
+ const text = input.log_text ?? (input.log_path && existsSync11(input.log_path) ? readFileSync6(input.log_path, "utf-8") : "");
25143
25940
  return {
25144
25941
  status: classifyLog(text),
25145
25942
  attempts: 1,
@@ -25151,7 +25948,7 @@ function runBrowserProvider(input) {
25151
25948
  if (!input.artifact_path) {
25152
25949
  return { status: "unknown", attempts: 1, exit_code: null, output_summary: "browser provider needs a screenshot or artifact path" };
25153
25950
  }
25154
- if (!existsSync10(input.artifact_path)) {
25951
+ if (!existsSync11(input.artifact_path)) {
25155
25952
  return { status: "failed", attempts: 1, exit_code: null, output_summary: `artifact not found: ${input.artifact_path}` };
25156
25953
  }
25157
25954
  return {
@@ -26941,12 +27738,12 @@ function summarizeTask(task) {
26941
27738
  };
26942
27739
  }
26943
27740
  function overdueTasks(tasks, nowIso) {
26944
- const now3 = Date.parse(nowIso);
27741
+ const now4 = Date.parse(nowIso);
26945
27742
  return tasks.filter((task) => {
26946
27743
  if (isTerminal(task) || !task.due_at)
26947
27744
  return false;
26948
27745
  const due = Date.parse(task.due_at);
26949
- return Number.isFinite(due) && due < now3;
27746
+ return Number.isFinite(due) && due < now4;
26950
27747
  });
26951
27748
  }
26952
27749
  function isReady(task, db) {
@@ -27296,15 +28093,15 @@ var init_local_reports = __esm(() => {
27296
28093
  });
27297
28094
 
27298
28095
  // src/lib/package-version.ts
27299
- import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
27300
- import { dirname as dirname7, join as join8 } from "path";
28096
+ import { existsSync as existsSync12, readFileSync as readFileSync7 } from "fs";
28097
+ import { dirname as dirname7, join as join9 } from "path";
27301
28098
  import { fileURLToPath } from "url";
27302
28099
  function getPackageVersion(fromUrl = import.meta.url) {
27303
28100
  try {
27304
28101
  let dir = dirname7(fileURLToPath(fromUrl));
27305
28102
  for (let i = 0;i < 5; i++) {
27306
- const pkgPath = join8(dir, "package.json");
27307
- if (existsSync11(pkgPath)) {
28103
+ const pkgPath = join9(dir, "package.json");
28104
+ if (existsSync12(pkgPath)) {
27308
28105
  return JSON.parse(readFileSync7(pkgPath, "utf-8")).version || "0.0.0";
27309
28106
  }
27310
28107
  const parent = dirname7(dir);
@@ -29395,8 +30192,8 @@ __export(exports_local_extensions, {
29395
30192
  discoverLocalExtensions: () => discoverLocalExtensions
29396
30193
  });
29397
30194
  import { createHash as createHash8, createVerify } from "crypto";
29398
- import { existsSync as existsSync12, readdirSync as readdirSync3, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
29399
- import { basename as basename5, join as join9, resolve as resolve12 } from "path";
30195
+ import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
30196
+ import { basename as basename5, join as join10, resolve as resolve12 } from "path";
29400
30197
  function isObject2(value) {
29401
30198
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
29402
30199
  }
@@ -29655,10 +30452,10 @@ function verifyExtensionSignature(input) {
29655
30452
  }
29656
30453
  function inspectExtensionSource(source3) {
29657
30454
  const resolved = resolve12(source3);
29658
- if (!existsSync12(resolved))
30455
+ if (!existsSync13(resolved))
29659
30456
  throw new Error(`extension source not found: ${source3}`);
29660
30457
  const stat = statSync6(resolved);
29661
- const manifestPath = stat.isDirectory() ? [join9(resolved, "todos.extension.json"), join9(resolved, "extension.json")].find(existsSync12) : resolved;
30458
+ const manifestPath = stat.isDirectory() ? [join10(resolved, "todos.extension.json"), join10(resolved, "extension.json")].find(existsSync13) : resolved;
29662
30459
  if (!manifestPath)
29663
30460
  throw new Error(`extension directory ${source3} is missing todos.extension.json`);
29664
30461
  const raw = readFileSync9(manifestPath);
@@ -29754,20 +30551,20 @@ function projectExtensionSources(projectPath) {
29754
30551
  return [];
29755
30552
  const root = resolve12(projectPath);
29756
30553
  const candidates = [
29757
- join9(root, "todos.extension.json"),
29758
- join9(root, ".todos", "todos.extension.json")
30554
+ join10(root, "todos.extension.json"),
30555
+ join10(root, ".todos", "todos.extension.json")
29759
30556
  ];
29760
- const extensionDir = join9(root, ".todos", "extensions");
29761
- if (existsSync12(extensionDir)) {
30557
+ const extensionDir = join10(root, ".todos", "extensions");
30558
+ if (existsSync13(extensionDir)) {
29762
30559
  for (const entry of readdirSync3(extensionDir)) {
29763
30560
  if (entry.startsWith("."))
29764
30561
  continue;
29765
- const full = join9(extensionDir, entry);
30562
+ const full = join10(extensionDir, entry);
29766
30563
  if (statSync6(full).isDirectory() || entry.endsWith(".json"))
29767
30564
  candidates.push(full);
29768
30565
  }
29769
30566
  }
29770
- return candidates.filter(existsSync12);
30567
+ return candidates.filter(existsSync13);
29771
30568
  }
29772
30569
  function discoverLocalExtensions(options = {}) {
29773
30570
  const config = loadConfig();
@@ -33289,7 +34086,7 @@ ${lines.join(`
33289
34086
  const projectRoles = n.project_roles.length > 0 ? ` <${n.project_roles.join(", ")}>` : "";
33290
34087
  const lead = n.is_project_lead ? " \u2605" : "";
33291
34088
  const lastSeen = new Date(n.agent.last_seen_at).getTime();
33292
- const active = now3 - lastSeen < ACTIVE_MS ? " \u25CF" : " \u25CB";
34089
+ const active = now4 - lastSeen < ACTIVE_MS ? " \u25CF" : " \u25CB";
33293
34090
  const line = `${prefix}${active} ${n.agent.name}${title}${globalRole}${projectRoles}${lead}`;
33294
34091
  const children = n.reports.length > 0 ? `
33295
34092
  ` + render(n.reports, indent + 1) : "";
@@ -33303,7 +34100,7 @@ ${lines.join(`
33303
34100
  if (format === "json") {
33304
34101
  return { content: [{ type: "text", text: JSON.stringify(tree, null, 2) }] };
33305
34102
  }
33306
- const now3 = Date.now();
34103
+ const now4 = Date.now();
33307
34104
  const ACTIVE_MS = 30 * 60 * 1000;
33308
34105
  const text2 = tree.length > 0 ? render(tree) : "No agents in this project's org chart.";
33309
34106
  return { content: [{ type: "text", text: text2 }] };
@@ -33853,10 +34650,10 @@ ${lines.join(`
33853
34650
  });
33854
34651
  }
33855
34652
  if (shouldRegisterTool("get_idle_focus_prompts")) {
33856
- 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: now3 }) => {
34653
+ 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 }) => {
33857
34654
  try {
33858
34655
  const { getIdleFocusSessionPrompts: getIdleFocusSessionPrompts2 } = (init_tasks(), __toCommonJS(exports_tasks));
33859
- return { content: [{ type: "text", text: JSON.stringify(getIdleFocusSessionPrompts2({ agent_id, now: now3 }), null, 2) }] };
34656
+ return { content: [{ type: "text", text: JSON.stringify(getIdleFocusSessionPrompts2({ agent_id, now: now4 }), null, 2) }] };
33860
34657
  } catch (e) {
33861
34658
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
33862
34659
  }
@@ -34051,9 +34848,9 @@ __export(exports_extract, {
34051
34848
  buildCodebaseIndex: () => buildCodebaseIndex,
34052
34849
  EXTRACT_TAGS: () => EXTRACT_TAGS
34053
34850
  });
34054
- import { existsSync as existsSync13, readFileSync as readFileSync10, statSync as statSync7 } from "fs";
34851
+ import { existsSync as existsSync14, readFileSync as readFileSync10, statSync as statSync7 } from "fs";
34055
34852
  import { createHash as createHash10 } from "crypto";
34056
- import { relative as relative5, resolve as resolve13, join as join10 } from "path";
34853
+ import { relative as relative5, resolve as resolve13, join as join11 } from "path";
34057
34854
  function stableHash(value) {
34058
34855
  return createHash10("sha256").update(value).digest("hex");
34059
34856
  }
@@ -34062,8 +34859,8 @@ function normalizePathForMatch(value) {
34062
34859
  }
34063
34860
  function readGitignorePatterns(basePath) {
34064
34861
  const root = statSync7(basePath).isFile() ? resolve13(basePath, "..") : basePath;
34065
- const gitignorePath = join10(root, ".gitignore");
34066
- if (!existsSync13(gitignorePath))
34862
+ const gitignorePath = join11(root, ".gitignore");
34863
+ if (!existsSync14(gitignorePath))
34067
34864
  return [];
34068
34865
  try {
34069
34866
  return readFileSync10(gitignorePath, "utf-8").split(`
@@ -34205,7 +35002,7 @@ function buildCodebaseIndex(options) {
34205
35002
  const files = collectFiles(basePath, extensions, excludes, respectGitignore);
34206
35003
  const indexed = [];
34207
35004
  for (const file of files) {
34208
- const fullPath = statSync7(basePath).isFile() ? basePath : join10(basePath, file);
35005
+ const fullPath = statSync7(basePath).isFile() ? basePath : join11(basePath, file);
34209
35006
  try {
34210
35007
  const source3 = readFileSync10(fullPath, "utf-8");
34211
35008
  const relPath = statSync7(basePath).isFile() ? relative5(resolve13(basePath, ".."), fullPath) : file;
@@ -34236,7 +35033,7 @@ function extractTodos(options, db) {
34236
35033
  const files = collectFiles(basePath, extensions, excludes, respectGitignore);
34237
35034
  const allComments = [];
34238
35035
  for (const file of files) {
34239
- const fullPath = statSync7(basePath).isFile() ? basePath : join10(basePath, file);
35036
+ const fullPath = statSync7(basePath).isFile() ? basePath : join11(basePath, file);
34240
35037
  try {
34241
35038
  const source3 = readFileSync10(fullPath, "utf-8");
34242
35039
  const relPath = statSync7(basePath).isFile() ? relative5(resolve13(basePath, ".."), fullPath) : file;
@@ -35129,7 +35926,7 @@ __export(exports_builtin_templates, {
35129
35926
  BUILTIN_TEMPLATES: () => BUILTIN_TEMPLATES
35130
35927
  });
35131
35928
  import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync4 } from "fs";
35132
- import { join as join11 } from "path";
35929
+ import { join as join12 } from "path";
35133
35930
  function templateMetadata(template) {
35134
35931
  return {
35135
35932
  source: BUILTIN_TEMPLATE_LIBRARY_SOURCE,
@@ -35188,7 +35985,7 @@ function writeBuiltinTemplateFiles(directory) {
35188
35985
  mkdirSync7(directory, { recursive: true });
35189
35986
  const files = [];
35190
35987
  for (const entry of exportBuiltinTemplateFiles()) {
35191
- const path = join11(directory, entry.filename);
35988
+ const path = join12(directory, entry.filename);
35192
35989
  writeFileSync4(path, `${JSON.stringify(entry.template, null, 2)}
35193
35990
  `, "utf-8");
35194
35991
  files.push(path);
@@ -35714,16 +36511,16 @@ __export(exports_environment_snapshots, {
35714
36511
  captureEnvironmentSnapshot: () => captureEnvironmentSnapshot
35715
36512
  });
35716
36513
  import { createHash as createHash11 } from "crypto";
35717
- import { existsSync as existsSync14, readFileSync as readFileSync11, statSync as statSync8 } from "fs";
36514
+ import { existsSync as existsSync15, readFileSync as readFileSync11, statSync as statSync8 } from "fs";
35718
36515
  import { hostname as hostname2, platform, arch } from "os";
35719
- import { dirname as dirname9, join as join12, resolve as resolve14 } from "path";
36516
+ import { dirname as dirname9, join as join13, resolve as resolve14 } from "path";
35720
36517
  import { tmpdir as tmpdir2 } from "os";
35721
36518
  function sha2566(value) {
35722
36519
  return createHash11("sha256").update(value).digest("hex");
35723
36520
  }
35724
36521
  function fileRecord(root, relativePath) {
35725
- const path = join12(root, relativePath);
35726
- if (!existsSync14(path))
36522
+ const path = join13(root, relativePath);
36523
+ if (!existsSync15(path))
35727
36524
  return null;
35728
36525
  const stat = statSync8(path);
35729
36526
  if (!stat.isFile())
@@ -35735,7 +36532,7 @@ function manifestRecord(root, relativePath) {
35735
36532
  const base = fileRecord(root, relativePath);
35736
36533
  if (!base)
35737
36534
  return null;
35738
- const parsed = readJsonFile(join12(root, relativePath));
36535
+ const parsed = readJsonFile(join13(root, relativePath));
35739
36536
  if (!parsed)
35740
36537
  return { ...base, redacted: {} };
35741
36538
  const redacted = redactValue({
@@ -35830,8 +36627,8 @@ function commandEnv(env, includeValues) {
35830
36627
  function defaultSnapshotDir() {
35831
36628
  const dbPath = getDatabasePath();
35832
36629
  if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
35833
- return join12(tmpdir2(), "hasna-todos", "environment-snapshots");
35834
- return join12(dirname9(resolve14(dbPath)), "environment-snapshots");
36630
+ return join13(tmpdir2(), "hasna-todos", "environment-snapshots");
36631
+ return join13(dirname9(resolve14(dbPath)), "environment-snapshots");
35835
36632
  }
35836
36633
  function snapshotWithId(snapshot) {
35837
36634
  const digest = sha2566(JSON.stringify(snapshot)).slice(0, 24);
@@ -35878,7 +36675,7 @@ function captureEnvironmentSnapshot(input = {}) {
35878
36675
  });
35879
36676
  }
35880
36677
  function writeEnvironmentSnapshot(snapshot, outputPath) {
35881
- const path = outputPath ? resolve14(outputPath) : join12(defaultSnapshotDir(), `${snapshot.id}.json`);
36678
+ const path = outputPath ? resolve14(outputPath) : join13(defaultSnapshotDir(), `${snapshot.id}.json`);
35882
36679
  ensureDir2(dirname9(path));
35883
36680
  writeJsonFile(path, snapshot);
35884
36681
  return path;
@@ -36250,7 +37047,7 @@ var init_http = __esm(() => {
36250
37047
  });
36251
37048
 
36252
37049
  // src/db/api-keys.ts
36253
- import { createHash as createHash12, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "crypto";
37050
+ import { createHash as createHash12, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual3 } from "crypto";
36254
37051
  function rowToRecord(row) {
36255
37052
  return {
36256
37053
  id: row.id,
@@ -36269,7 +37066,7 @@ function hashApiKey(key) {
36269
37066
  function safeEqualHex(a, b) {
36270
37067
  if (a.length !== b.length)
36271
37068
  return false;
36272
- return timingSafeEqual2(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
37069
+ return timingSafeEqual3(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
36273
37070
  }
36274
37071
  function hasActiveApiKeys(db) {
36275
37072
  const d = db || getDatabase();
@@ -36430,7 +37227,7 @@ var init_headless_boundaries = __esm(() => {
36430
37227
  });
36431
37228
 
36432
37229
  // src/server/routes.ts
36433
- import { join as join13, resolve as resolve15, sep as sep2 } from "path";
37230
+ import { join as join14, resolve as resolve15, sep as sep2 } from "path";
36434
37231
  function parseFieldsParam(url) {
36435
37232
  const fieldsParam = url.searchParams.get("fields");
36436
37233
  return fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
@@ -37134,7 +37931,7 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
37134
37931
  if (!ctx.dashboardExists || method !== "GET" && method !== "HEAD")
37135
37932
  return null;
37136
37933
  if (path !== "/") {
37137
- const filePath = join13(ctx.dashboardDir, path);
37934
+ const filePath = join14(ctx.dashboardDir, path);
37138
37935
  const resolvedFile = resolve15(filePath);
37139
37936
  const resolvedBase = resolve15(ctx.dashboardDir);
37140
37937
  if (!resolvedFile.startsWith(resolvedBase + sep2) && resolvedFile !== resolvedBase) {
@@ -37144,7 +37941,7 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
37144
37941
  if (res2)
37145
37942
  return res2;
37146
37943
  }
37147
- const indexPath = join13(ctx.dashboardDir, "index.html");
37944
+ const indexPath = join14(ctx.dashboardDir, "index.html");
37148
37945
  const res = serveStaticFile2(indexPath);
37149
37946
  if (res)
37150
37947
  return res;
@@ -37173,27 +37970,27 @@ __export(exports_serve, {
37173
37970
  SECURITY_HEADERS: () => SECURITY_HEADERS,
37174
37971
  MIME_TYPES: () => MIME_TYPES
37175
37972
  });
37176
- import { existsSync as existsSync15 } from "fs";
37177
- import { join as join14, dirname as dirname10, extname } from "path";
37973
+ import { existsSync as existsSync16 } from "fs";
37974
+ import { join as join15, dirname as dirname10, extname } from "path";
37178
37975
  import { fileURLToPath as fileURLToPath2 } from "url";
37179
37976
  function resolveDashboardDir() {
37180
37977
  const candidates = [];
37181
37978
  try {
37182
37979
  const scriptDir = dirname10(fileURLToPath2(import.meta.url));
37183
- candidates.push(join14(scriptDir, "..", "dashboard", "dist"));
37184
- candidates.push(join14(scriptDir, "..", "..", "dashboard", "dist"));
37980
+ candidates.push(join15(scriptDir, "..", "dashboard", "dist"));
37981
+ candidates.push(join15(scriptDir, "..", "..", "dashboard", "dist"));
37185
37982
  } catch {}
37186
37983
  if (process.argv[1]) {
37187
37984
  const mainDir = dirname10(process.argv[1]);
37188
- candidates.push(join14(mainDir, "..", "dashboard", "dist"));
37189
- candidates.push(join14(mainDir, "..", "..", "dashboard", "dist"));
37985
+ candidates.push(join15(mainDir, "..", "dashboard", "dist"));
37986
+ candidates.push(join15(mainDir, "..", "..", "dashboard", "dist"));
37190
37987
  }
37191
- candidates.push(join14(process.cwd(), "dashboard", "dist"));
37988
+ candidates.push(join15(process.cwd(), "dashboard", "dist"));
37192
37989
  for (const candidate of candidates) {
37193
- if (existsSync15(candidate))
37990
+ if (existsSync16(candidate))
37194
37991
  return candidate;
37195
37992
  }
37196
- return join14(process.cwd(), "dashboard", "dist");
37993
+ return join15(process.cwd(), "dashboard", "dist");
37197
37994
  }
37198
37995
  function getProvidedApiKey(req) {
37199
37996
  const headerKey = req.headers.get("x-api-key");
@@ -37220,15 +38017,15 @@ function checkAuth(req, apiKey) {
37220
38017
  return null;
37221
38018
  }
37222
38019
  function checkRateLimit(ip) {
37223
- const now3 = Date.now();
38020
+ const now4 = Date.now();
37224
38021
  const entry = rateLimitMap.get(ip);
37225
- if (!entry || now3 > entry.resetAt) {
37226
- rateLimitMap.set(ip, { count: 1, resetAt: now3 + RATE_LIMIT_WINDOW_MS });
38022
+ if (!entry || now4 > entry.resetAt) {
38023
+ rateLimitMap.set(ip, { count: 1, resetAt: now4 + RATE_LIMIT_WINDOW_MS });
37227
38024
  return { allowed: true };
37228
38025
  }
37229
38026
  entry.count++;
37230
38027
  if (entry.count > RATE_LIMIT_MAX) {
37231
- return { allowed: false, retryAfter: Math.ceil((entry.resetAt - now3) / 1000) };
38028
+ return { allowed: false, retryAfter: Math.ceil((entry.resetAt - now4) / 1000) };
37232
38029
  }
37233
38030
  return { allowed: true };
37234
38031
  }
@@ -37243,7 +38040,7 @@ function json(data, status = 200, headers) {
37243
38040
  });
37244
38041
  }
37245
38042
  function serveStaticFile(filePath) {
37246
- if (!existsSync15(filePath))
38043
+ if (!existsSync16(filePath))
37247
38044
  return null;
37248
38045
  const ext = extname(filePath);
37249
38046
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
@@ -37322,7 +38119,7 @@ data: ${data}
37322
38119
  filteredSseClients.delete(client);
37323
38120
  }
37324
38121
  const dashboardDir = resolveDashboardDir();
37325
- const dashboardExists = existsSync15(dashboardDir);
38122
+ const dashboardExists = existsSync16(dashboardDir);
37326
38123
  if (!dashboardExists) {
37327
38124
  console.error(`
37328
38125
  Dashboard not found at: ${dashboardDir}`);