@hasna/todos 0.11.56 → 0.11.57

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/index.js CHANGED
@@ -3050,7 +3050,7 @@ var init_redaction = __esm(() => {
3050
3050
  });
3051
3051
 
3052
3052
  // src/lib/secret-redaction.ts
3053
- import { readFileSync as readFileSync3, existsSync as existsSync6 } from "fs";
3053
+ import { readFileSync as readFileSync3, existsSync as existsSync7 } from "fs";
3054
3054
  function registerCustomRedactor(fn) {
3055
3055
  customRedactors.push(fn);
3056
3056
  }
@@ -3115,7 +3115,7 @@ function scanAndRedactText(text, options = {}) {
3115
3115
  };
3116
3116
  }
3117
3117
  function scanFileForSecrets(path, options = {}) {
3118
- if (!existsSync6(path))
3118
+ if (!existsSync7(path))
3119
3119
  throw new Error(`File not found: ${path}`);
3120
3120
  const content = readFileSync3(path, "utf8");
3121
3121
  return scanAndRedactText(content, options);
@@ -4874,6 +4874,67 @@ var TODOS_JSON_CONTRACTS = [
4874
4874
  },
4875
4875
  optional: {}
4876
4876
  }),
4877
+ contract({
4878
+ id: "tester_issue_report",
4879
+ name: "Tester Issue Report",
4880
+ description: "Generic issue report payload emitted by @hasna/testers or compatible test runners for conversion into local tasks.",
4881
+ surfaces: ["cli", "sdk"],
4882
+ stability: "stable",
4883
+ required: {
4884
+ schema_version: field("string", "Must be testers.issue_report.v1."),
4885
+ title: field("string", "Short human-readable issue title.")
4886
+ },
4887
+ optional: {
4888
+ kind: field("string", "Issue kind such as assertion_failure, console_error, network_error, accessibility, or unknown."),
4889
+ severity: field("string", "Issue severity mapped to task priority: low, medium, high, or critical."),
4890
+ id: idField,
4891
+ fingerprint: field("string", "Stable tester-side fingerprint. If omitted, @hasna/todos derives one from issue fields."),
4892
+ summary: field(["string", "null"], "Short issue summary.", true),
4893
+ source: field("object", "Run, result, scenario, URL, commit, and branch source metadata."),
4894
+ target: field("object", "URL, route, selector, component, browser, and viewport metadata."),
4895
+ failure: field("object", "Failure message, expected/actual values, stack, reasoning, and reproduction steps."),
4896
+ evidence: field("object", "Logs, screenshots, and artifact references."),
4897
+ labels: tagsField,
4898
+ metadata: metadataField,
4899
+ occurred_at: isoDateField
4900
+ }
4901
+ }),
4902
+ contract({
4903
+ id: "tester_issue_report_result",
4904
+ name: "Tester Issue Report Result",
4905
+ description: "Local-only dry-run or applied result from mapping a tester issue report to a task.",
4906
+ surfaces: ["cli", "sdk"],
4907
+ stability: "stable",
4908
+ required: {
4909
+ schema_version: field("string", "Result schema version."),
4910
+ local_only: field("boolean", "Always true; issue reports are applied to local todos state."),
4911
+ dry_run: field("boolean", "True when no task was created or updated."),
4912
+ processed_at: isoDateField,
4913
+ action: field("string", "preview, matched, created, updated, or regressed."),
4914
+ fingerprint: field("string", "Computed or supplied tester issue fingerprint."),
4915
+ report: field("object", "Normalized tester issue report."),
4916
+ task: field(["object", "null"], "Created, updated, matched, or null task.", true),
4917
+ warnings: field("array", "Non-fatal warnings."),
4918
+ commands: field("array", "Follow-up CLI commands for operators and agents.")
4919
+ },
4920
+ optional: {}
4921
+ }),
4922
+ contract({
4923
+ id: "tester_issue_report_batch_result",
4924
+ name: "Tester Issue Report Batch Result",
4925
+ description: "Local-only dry-run or applied batch result from mapping tester issue reports to tasks.",
4926
+ surfaces: ["cli", "sdk"],
4927
+ stability: "stable",
4928
+ required: {
4929
+ schema_version: field("string", "Batch result schema version."),
4930
+ local_only: field("boolean", "Always true; issue reports are applied to local todos state."),
4931
+ dry_run: field("boolean", "True when no tasks were created or updated."),
4932
+ processed_at: isoDateField,
4933
+ results: field("array", "Single report result entries."),
4934
+ summary: field("object", "Counts grouped by action plus total.")
4935
+ },
4936
+ optional: {}
4937
+ }),
4877
4938
  contract({
4878
4939
  id: "verification_provider",
4879
4940
  name: "Verification Provider",
@@ -5914,7 +5975,7 @@ var TODOS_JSON_CONTRACTS_MANIFEST = createJsonContractsManifest({
5914
5975
  });
5915
5976
  // src/lib/onboarding-fixtures.ts
5916
5977
  import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3 } from "fs";
5917
- import { join as join6 } from "path";
5978
+ import { join as join7 } from "path";
5918
5979
 
5919
5980
  // src/lib/local-bridge.ts
5920
5981
  init_database();
@@ -6574,6 +6635,7 @@ function explainRunnerSandbox(input = {}) {
6574
6635
  // src/lib/event-hooks.ts
6575
6636
  init_config();
6576
6637
  var LOCAL_EVENT_TYPES = [
6638
+ "task.created",
6577
6639
  "task.assigned",
6578
6640
  "task.blocked",
6579
6641
  "task.started",
@@ -6816,6 +6878,568 @@ async function testLocalEventHook(name, input) {
6816
6878
  return emitLocalEventHooks({ ...input, hooks: [hook] });
6817
6879
  }
6818
6880
 
6881
+ // node_modules/.bun/@hasna+events@0.1.7/node_modules/@hasna/events/dist/index.js
6882
+ import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
6883
+ import { existsSync as existsSync6 } from "fs";
6884
+ import { homedir } from "os";
6885
+ import { join as join5 } from "path";
6886
+ import { createHmac, timingSafeEqual } from "crypto";
6887
+ import { randomUUID as randomUUID2 } from "crypto";
6888
+ import { spawn } from "child_process";
6889
+ import { randomUUID as randomUUID22 } from "crypto";
6890
+ function getPathValue(input, path) {
6891
+ return path.split(".").reduce((value, part) => {
6892
+ if (value && typeof value === "object" && part in value) {
6893
+ return value[part];
6894
+ }
6895
+ return;
6896
+ }, input);
6897
+ }
6898
+ function wildcardToRegExp(pattern) {
6899
+ const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
6900
+ return new RegExp(`^${escaped}$`);
6901
+ }
6902
+ function matchString(value, matcher) {
6903
+ if (matcher === undefined)
6904
+ return true;
6905
+ if (value === undefined)
6906
+ return false;
6907
+ const matchers = Array.isArray(matcher) ? matcher : [matcher];
6908
+ return matchers.some((item) => wildcardToRegExp(item).test(value));
6909
+ }
6910
+ function matchRecord(input, matcher) {
6911
+ if (!matcher)
6912
+ return true;
6913
+ return Object.entries(matcher).every(([path, expected]) => {
6914
+ const actual = getPathValue(input, path);
6915
+ if (typeof expected === "string" || Array.isArray(expected)) {
6916
+ return matchString(actual === undefined ? undefined : String(actual), expected);
6917
+ }
6918
+ return actual === expected;
6919
+ });
6920
+ }
6921
+ function eventMatchesFilter(event, filter) {
6922
+ 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);
6923
+ }
6924
+ function channelMatchesEvent(channel, event) {
6925
+ if (!channel.enabled)
6926
+ return false;
6927
+ if (!channel.filters || channel.filters.length === 0)
6928
+ return true;
6929
+ return channel.filters.some((filter) => eventMatchesFilter(event, filter));
6930
+ }
6931
+ var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
6932
+ var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
6933
+ function getEventsDataDir(override) {
6934
+ return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join5(homedir(), ".hasna", "events");
6935
+ }
6936
+
6937
+ class JsonEventsStore {
6938
+ dataDir;
6939
+ channelsPath;
6940
+ eventsPath;
6941
+ deliveriesPath;
6942
+ constructor(dataDir = getEventsDataDir()) {
6943
+ this.dataDir = dataDir;
6944
+ this.channelsPath = join5(dataDir, "channels.json");
6945
+ this.eventsPath = join5(dataDir, "events.json");
6946
+ this.deliveriesPath = join5(dataDir, "deliveries.json");
6947
+ }
6948
+ async init() {
6949
+ await mkdir(this.dataDir, { recursive: true, mode: 448 });
6950
+ await chmod(this.dataDir, 448).catch(() => {
6951
+ return;
6952
+ });
6953
+ await this.ensureArrayFile(this.channelsPath);
6954
+ await this.ensureArrayFile(this.eventsPath);
6955
+ await this.ensureArrayFile(this.deliveriesPath);
6956
+ }
6957
+ async addChannel(channel) {
6958
+ await this.init();
6959
+ const channels = await this.readJson(this.channelsPath, []);
6960
+ const index = channels.findIndex((item) => item.id === channel.id);
6961
+ if (index >= 0) {
6962
+ channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
6963
+ } else {
6964
+ channels.push(channel);
6965
+ }
6966
+ await this.writeJson(this.channelsPath, channels);
6967
+ return index >= 0 ? channels[index] : channel;
6968
+ }
6969
+ async listChannels() {
6970
+ await this.init();
6971
+ return this.readJson(this.channelsPath, []);
6972
+ }
6973
+ async getChannel(id) {
6974
+ const channels = await this.listChannels();
6975
+ return channels.find((channel) => channel.id === id);
6976
+ }
6977
+ async removeChannel(id) {
6978
+ await this.init();
6979
+ const channels = await this.readJson(this.channelsPath, []);
6980
+ const next = channels.filter((channel) => channel.id !== id);
6981
+ await this.writeJson(this.channelsPath, next);
6982
+ return next.length !== channels.length;
6983
+ }
6984
+ async appendEvent(event) {
6985
+ await this.init();
6986
+ const events = await this.readJson(this.eventsPath, []);
6987
+ events.push(event);
6988
+ await this.writeJson(this.eventsPath, events);
6989
+ return event;
6990
+ }
6991
+ async listEvents() {
6992
+ await this.init();
6993
+ return this.readJson(this.eventsPath, []);
6994
+ }
6995
+ async findEventByIdentity(identity) {
6996
+ const events = await this.listEvents();
6997
+ return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
6998
+ }
6999
+ async appendDelivery(result) {
7000
+ await this.init();
7001
+ const deliveries = await this.readJson(this.deliveriesPath, []);
7002
+ deliveries.push(result);
7003
+ await this.writeJson(this.deliveriesPath, deliveries);
7004
+ return result;
7005
+ }
7006
+ async listDeliveries() {
7007
+ await this.init();
7008
+ return this.readJson(this.deliveriesPath, []);
7009
+ }
7010
+ async exportData() {
7011
+ return {
7012
+ channels: await this.listChannels(),
7013
+ events: await this.listEvents(),
7014
+ deliveries: await this.listDeliveries()
7015
+ };
7016
+ }
7017
+ async ensureArrayFile(path) {
7018
+ if (!existsSync6(path)) {
7019
+ await writeFile(path, `[]
7020
+ `, { encoding: "utf-8", mode: 384 });
7021
+ }
7022
+ await chmod(path, 384).catch(() => {
7023
+ return;
7024
+ });
7025
+ }
7026
+ async readJson(path, fallback) {
7027
+ try {
7028
+ const raw = await readFile(path, "utf-8");
7029
+ if (!raw.trim())
7030
+ return fallback;
7031
+ return JSON.parse(raw);
7032
+ } catch (error) {
7033
+ if (error.code === "ENOENT")
7034
+ return fallback;
7035
+ throw error;
7036
+ }
7037
+ }
7038
+ async writeJson(path, value) {
7039
+ const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
7040
+ await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
7041
+ `, { encoding: "utf-8", mode: 384 });
7042
+ await rename(tempPath, path);
7043
+ await chmod(path, 384).catch(() => {
7044
+ return;
7045
+ });
7046
+ }
7047
+ }
7048
+ var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
7049
+ function buildSignatureBase(timestamp, body) {
7050
+ return `${timestamp}.${body}`;
7051
+ }
7052
+ function signPayload(secret, timestamp, body) {
7053
+ const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
7054
+ return `sha256=${digest}`;
7055
+ }
7056
+ function now2() {
7057
+ return new Date().toISOString();
7058
+ }
7059
+ function truncate(value, max = 4096) {
7060
+ return value.length > max ? `${value.slice(0, max)}...` : value;
7061
+ }
7062
+ function buildWebhookRequest(event, channel) {
7063
+ if (!channel.webhook)
7064
+ throw new Error(`Channel ${channel.id} has no webhook config`);
7065
+ const body = JSON.stringify(event);
7066
+ const timestamp = event.time;
7067
+ const headers = {
7068
+ "Content-Type": "application/json",
7069
+ "User-Agent": "@hasna/events",
7070
+ "X-Hasna-Event-Id": event.id,
7071
+ "X-Hasna-Event-Type": event.type,
7072
+ "X-Hasna-Timestamp": timestamp,
7073
+ ...channel.webhook.headers
7074
+ };
7075
+ if (channel.webhook.secret) {
7076
+ headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
7077
+ }
7078
+ return { body, headers };
7079
+ }
7080
+ async function dispatchWebhook(event, channel, options = {}) {
7081
+ if (!channel.webhook)
7082
+ throw new Error(`Channel ${channel.id} has no webhook config`);
7083
+ const startedAt = now2();
7084
+ const { body, headers } = buildWebhookRequest(event, channel);
7085
+ const controller = new AbortController;
7086
+ const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
7087
+ try {
7088
+ const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
7089
+ method: "POST",
7090
+ headers,
7091
+ body,
7092
+ signal: controller.signal
7093
+ });
7094
+ const responseBody = truncate(await response.text());
7095
+ return {
7096
+ attempt: 1,
7097
+ status: response.ok ? "success" : "failed",
7098
+ startedAt,
7099
+ completedAt: now2(),
7100
+ responseStatus: response.status,
7101
+ responseBody,
7102
+ error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
7103
+ };
7104
+ } catch (error) {
7105
+ return {
7106
+ attempt: 1,
7107
+ status: "failed",
7108
+ startedAt,
7109
+ completedAt: now2(),
7110
+ error: error instanceof Error ? error.message : String(error)
7111
+ };
7112
+ } finally {
7113
+ clearTimeout(timeout);
7114
+ }
7115
+ }
7116
+ async function dispatchCommand(event, channel) {
7117
+ if (!channel.command)
7118
+ throw new Error(`Channel ${channel.id} has no command config`);
7119
+ const startedAt = now2();
7120
+ const eventJson = JSON.stringify(event);
7121
+ const env = {
7122
+ ...process.env,
7123
+ ...channel.command.env,
7124
+ HASNA_CHANNEL_ID: channel.id,
7125
+ HASNA_EVENT_ID: event.id,
7126
+ HASNA_EVENT_TYPE: event.type,
7127
+ HASNA_EVENT_SOURCE: event.source,
7128
+ HASNA_EVENT_SUBJECT: event.subject ?? "",
7129
+ HASNA_EVENT_SEVERITY: event.severity,
7130
+ HASNA_EVENT_TIME: event.time,
7131
+ HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
7132
+ HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
7133
+ HASNA_EVENT_JSON: eventJson
7134
+ };
7135
+ return new Promise((resolve6) => {
7136
+ const child = spawn(channel.command.command, channel.command.args ?? [], {
7137
+ cwd: channel.command.cwd,
7138
+ env,
7139
+ stdio: ["pipe", "pipe", "pipe"]
7140
+ });
7141
+ let stdout = "";
7142
+ let stderr = "";
7143
+ const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
7144
+ child.stdin.end(eventJson);
7145
+ child.stdout.on("data", (chunk) => {
7146
+ stdout += chunk.toString();
7147
+ });
7148
+ child.stderr.on("data", (chunk) => {
7149
+ stderr += chunk.toString();
7150
+ });
7151
+ child.on("error", (error) => {
7152
+ clearTimeout(timeout);
7153
+ resolve6({
7154
+ attempt: 1,
7155
+ status: "failed",
7156
+ startedAt,
7157
+ completedAt: now2(),
7158
+ stdout: truncate(stdout),
7159
+ stderr: truncate(stderr),
7160
+ error: error.message
7161
+ });
7162
+ });
7163
+ child.on("close", (code, signal) => {
7164
+ clearTimeout(timeout);
7165
+ const success = code === 0;
7166
+ resolve6({
7167
+ attempt: 1,
7168
+ status: success ? "success" : "failed",
7169
+ startedAt,
7170
+ completedAt: now2(),
7171
+ stdout: truncate(stdout),
7172
+ stderr: truncate(stderr),
7173
+ error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
7174
+ });
7175
+ });
7176
+ });
7177
+ }
7178
+ async function dispatchChannel(event, channel, options = {}) {
7179
+ if (channel.transport === "webhook")
7180
+ return dispatchWebhook(event, channel, options);
7181
+ if (channel.transport === "command")
7182
+ return dispatchCommand(event, channel);
7183
+ return {
7184
+ attempt: 1,
7185
+ status: "skipped",
7186
+ startedAt: now2(),
7187
+ completedAt: now2(),
7188
+ error: `Unsupported transport: ${channel.transport}`
7189
+ };
7190
+ }
7191
+ function createDeliveryResult(event, channel, attempts) {
7192
+ const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
7193
+ return {
7194
+ id: randomUUID2(),
7195
+ eventId: event.id,
7196
+ channelId: channel.id,
7197
+ transport: channel.transport,
7198
+ status,
7199
+ attempts,
7200
+ createdAt: attempts[0]?.startedAt ?? now2(),
7201
+ completedAt: attempts.at(-1)?.completedAt ?? now2()
7202
+ };
7203
+ }
7204
+ function createEvent(input) {
7205
+ return {
7206
+ id: input.id ?? randomUUID22(),
7207
+ source: input.source,
7208
+ type: input.type,
7209
+ time: normalizeTime(input.time),
7210
+ subject: input.subject,
7211
+ severity: input.severity ?? "info",
7212
+ data: input.data ?? {},
7213
+ message: input.message,
7214
+ dedupeKey: input.dedupeKey,
7215
+ schemaVersion: input.schemaVersion ?? "1.0",
7216
+ metadata: input.metadata ?? {}
7217
+ };
7218
+ }
7219
+
7220
+ class EventsClient {
7221
+ store;
7222
+ redactors;
7223
+ transportOptions;
7224
+ constructor(options = {}) {
7225
+ this.store = options.store ?? new JsonEventsStore(options.dataDir);
7226
+ this.redactors = options.redactors ?? [];
7227
+ this.transportOptions = { fetchImpl: options.fetchImpl };
7228
+ }
7229
+ async addChannel(input) {
7230
+ const timestamp = new Date().toISOString();
7231
+ return this.store.addChannel({
7232
+ ...input,
7233
+ createdAt: input.createdAt ?? timestamp,
7234
+ updatedAt: input.updatedAt ?? timestamp
7235
+ });
7236
+ }
7237
+ async listChannels() {
7238
+ return this.store.listChannels();
7239
+ }
7240
+ async removeChannel(id) {
7241
+ return this.store.removeChannel(id);
7242
+ }
7243
+ async emit(input, options = {}) {
7244
+ const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
7245
+ if (options.dedupe !== false) {
7246
+ const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
7247
+ if (existing) {
7248
+ return { event: existing, deliveries: [], deduped: true };
7249
+ }
7250
+ }
7251
+ await this.store.appendEvent(event);
7252
+ const deliveries = options.deliver === false ? [] : await this.deliver(event);
7253
+ return { event, deliveries, deduped: false };
7254
+ }
7255
+ async listEvents() {
7256
+ return this.store.listEvents();
7257
+ }
7258
+ async listDeliveries() {
7259
+ return this.store.listDeliveries();
7260
+ }
7261
+ async deliver(event) {
7262
+ const channels = await this.store.listChannels();
7263
+ const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
7264
+ const deliveries = [];
7265
+ for (const channel of selected) {
7266
+ const eventForChannel = await this.applyRedaction(event, channel);
7267
+ const result = await this.deliverWithRetry(eventForChannel, channel);
7268
+ await this.store.appendDelivery(result);
7269
+ deliveries.push(result);
7270
+ }
7271
+ return deliveries;
7272
+ }
7273
+ async testChannel(id, input = {}) {
7274
+ const channel = await this.store.getChannel(id);
7275
+ if (!channel)
7276
+ throw new Error(`Channel not found: ${id}`);
7277
+ const event = createEvent({
7278
+ source: input.source ?? "hasna.events",
7279
+ type: input.type ?? "events.test",
7280
+ subject: input.subject ?? id,
7281
+ severity: input.severity ?? "info",
7282
+ data: input.data ?? { test: true },
7283
+ message: input.message ?? "Hasna events test delivery",
7284
+ dedupeKey: input.dedupeKey,
7285
+ schemaVersion: input.schemaVersion,
7286
+ metadata: input.metadata,
7287
+ time: input.time,
7288
+ id: input.id
7289
+ });
7290
+ const eventForChannel = await this.applyRedaction(event, channel);
7291
+ const result = await this.deliverWithRetry(eventForChannel, channel);
7292
+ await this.store.appendDelivery(result);
7293
+ return result;
7294
+ }
7295
+ async replay(options = {}) {
7296
+ const events = (await this.store.listEvents()).filter((event) => {
7297
+ if (options.eventId && event.id !== options.eventId)
7298
+ return false;
7299
+ if (options.source && event.source !== options.source)
7300
+ return false;
7301
+ if (options.type && event.type !== options.type)
7302
+ return false;
7303
+ return true;
7304
+ });
7305
+ if (options.dryRun)
7306
+ return { events, deliveries: [] };
7307
+ const deliveries = [];
7308
+ for (const event of events) {
7309
+ deliveries.push(...await this.deliver(event));
7310
+ }
7311
+ return { events, deliveries };
7312
+ }
7313
+ async applyRedaction(event, channel) {
7314
+ let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
7315
+ for (const redactor of this.redactors) {
7316
+ next = await redactor(next, channel);
7317
+ }
7318
+ return next;
7319
+ }
7320
+ async deliverWithRetry(event, channel) {
7321
+ const policy = normalizeRetryPolicy(channel.retry);
7322
+ const attempts = [];
7323
+ for (let index = 0;index < policy.maxAttempts; index += 1) {
7324
+ const attempt = await dispatchChannel(event, channel, this.transportOptions);
7325
+ attempt.attempt = index + 1;
7326
+ if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
7327
+ attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
7328
+ }
7329
+ attempts.push(attempt);
7330
+ if (attempt.status !== "failed")
7331
+ break;
7332
+ if (attempt.nextBackoffMs)
7333
+ await Bun.sleep(attempt.nextBackoffMs);
7334
+ }
7335
+ return createDeliveryResult(event, channel, attempts);
7336
+ }
7337
+ }
7338
+ function redactPaths(event, paths, replacement = "[REDACTED]") {
7339
+ if (paths.length === 0)
7340
+ return event;
7341
+ const copy = structuredClone(event);
7342
+ for (const path of paths) {
7343
+ setPath(copy, path, replacement);
7344
+ }
7345
+ return copy;
7346
+ }
7347
+ function redactSensitiveKeys(event, replacement = "[REDACTED]") {
7348
+ return redactValue2(event, replacement);
7349
+ }
7350
+ function shouldRedactKey(key) {
7351
+ return /secret|token|password|api[_-]?key|authorization/i.test(key);
7352
+ }
7353
+ function redactValue2(value, replacement) {
7354
+ if (Array.isArray(value))
7355
+ return value.map((item) => redactValue2(item, replacement));
7356
+ if (!value || typeof value !== "object")
7357
+ return value;
7358
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
7359
+ key,
7360
+ shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
7361
+ ]));
7362
+ }
7363
+ function setPath(input, path, replacement) {
7364
+ const parts = path.split(".");
7365
+ let cursor = input;
7366
+ for (const part of parts.slice(0, -1)) {
7367
+ const next = cursor[part];
7368
+ if (!next || typeof next !== "object")
7369
+ return;
7370
+ cursor = next;
7371
+ }
7372
+ const last = parts.at(-1);
7373
+ if (last && last in cursor)
7374
+ cursor[last] = replacement;
7375
+ }
7376
+ function normalizeTime(value) {
7377
+ if (!value)
7378
+ return new Date().toISOString();
7379
+ return value instanceof Date ? value.toISOString() : value;
7380
+ }
7381
+ function normalizeRetryPolicy(policy) {
7382
+ return {
7383
+ maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
7384
+ backoffMs: Math.max(0, policy?.backoffMs ?? 250),
7385
+ multiplier: Math.max(1, policy?.multiplier ?? 2)
7386
+ };
7387
+ }
7388
+
7389
+ // src/lib/shared-events.ts
7390
+ var SOURCE = "todos";
7391
+ function taskEventData(task, extra = {}) {
7392
+ return {
7393
+ id: task.id,
7394
+ task_id: task.id,
7395
+ short_id: task.short_id,
7396
+ title: task.title,
7397
+ description: task.description,
7398
+ status: task.status,
7399
+ priority: task.priority,
7400
+ project_id: task.project_id,
7401
+ parent_id: task.parent_id,
7402
+ plan_id: task.plan_id,
7403
+ task_list_id: task.task_list_id,
7404
+ agent_id: task.agent_id,
7405
+ assigned_to: task.assigned_to,
7406
+ session_id: task.session_id,
7407
+ working_dir: task.working_dir,
7408
+ tags: task.tags,
7409
+ metadata: task.metadata,
7410
+ version: task.version,
7411
+ created_at: task.created_at,
7412
+ updated_at: task.updated_at,
7413
+ started_at: task.started_at,
7414
+ completed_at: task.completed_at,
7415
+ due_at: task.due_at,
7416
+ ...extra
7417
+ };
7418
+ }
7419
+ async function emitSharedTaskEvent(input) {
7420
+ const data = taskEventData(input.task, input.data);
7421
+ await new EventsClient().emit({
7422
+ source: SOURCE,
7423
+ type: input.type,
7424
+ subject: input.task.id,
7425
+ severity: input.severity ?? "info",
7426
+ message: input.message ?? `${input.type}: ${input.task.title}`,
7427
+ data,
7428
+ dedupeKey: input.dedupeKey ?? `${input.type}:${input.task.id}:${input.task.version}`,
7429
+ metadata: {
7430
+ package: "@hasna/todos",
7431
+ task_id: input.task.id,
7432
+ project_id: input.task.project_id,
7433
+ task_list_id: input.task.task_list_id
7434
+ }
7435
+ }, { deliver: true, dedupe: true });
7436
+ }
7437
+ function emitSharedTaskEventQuiet(input) {
7438
+ emitSharedTaskEvent(input).catch(() => {
7439
+ return;
7440
+ });
7441
+ }
7442
+
6819
7443
  // src/db/audit.ts
6820
7444
  init_database();
6821
7445
  function logTaskChange(taskId, action, field2, oldValue, newValue, agentId, db) {
@@ -7078,7 +7702,7 @@ async function deliverWebhook(wh, event, body, attempt, db) {
7078
7702
  activeDeliveries--;
7079
7703
  }
7080
7704
  }
7081
- async function dispatchWebhook(event, payload, db) {
7705
+ async function dispatchWebhook2(event, payload, db) {
7082
7706
  const d = db || getDatabase();
7083
7707
  const webhooks = listWebhooks(d).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
7084
7708
  const payloadObj = typeof payload === "object" && payload !== null ? payload : {};
@@ -7232,7 +7856,10 @@ function createTask(input, db) {
7232
7856
  insertTaskTags(id, tags, d);
7233
7857
  }
7234
7858
  const task = getTask(id, d);
7235
- 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(() => {});
7859
+ const payload = taskEventData(task);
7860
+ dispatchWebhook2("task.created", payload, d).catch(() => {});
7861
+ emitLocalEventHooksQuiet({ type: "task.created", payload });
7862
+ emitSharedTaskEventQuiet({ type: "task.created", task });
7236
7863
  return task;
7237
7864
  }
7238
7865
  function getTask(id, db) {
@@ -7576,18 +8203,7 @@ function updateTask(id, input, db) {
7576
8203
  logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
7577
8204
  if (input.approved_by !== undefined)
7578
8205
  logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
7579
- if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
7580
- dispatchWebhook("task.assigned", { id, assigned_to: input.assigned_to, title: task.title }, d).catch(() => {});
7581
- emitLocalEventHooksQuiet({ type: "task.assigned", payload: { id, assigned_to: input.assigned_to, title: task.title } });
7582
- }
7583
- if (input.status !== undefined && input.status !== task.status) {
7584
- dispatchWebhook("task.status_changed", { id, old_status: task.status, new_status: input.status, title: task.title }, d).catch(() => {});
7585
- emitLocalEventHooksQuiet({ type: "task.status_changed", payload: { id, old_status: task.status, new_status: input.status, title: task.title } });
7586
- }
7587
- if (input.approved_by !== undefined) {
7588
- emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
7589
- }
7590
- return {
8206
+ const updatedTask = {
7591
8207
  ...task,
7592
8208
  ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
7593
8209
  tags: input.tags ?? task.tags,
@@ -7605,6 +8221,22 @@ function updateTask(id, input, db) {
7605
8221
  approved_by: input.approved_by ?? task.approved_by,
7606
8222
  approved_at: input.approved_by ? timestamp : task.approved_at
7607
8223
  };
8224
+ if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
8225
+ const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
8226
+ dispatchWebhook2("task.assigned", payload, d).catch(() => {});
8227
+ emitLocalEventHooksQuiet({ type: "task.assigned", payload });
8228
+ emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to } });
8229
+ }
8230
+ if (input.status !== undefined && input.status !== task.status) {
8231
+ const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
8232
+ dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
8233
+ emitLocalEventHooksQuiet({ type: "task.status_changed", payload });
8234
+ emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status } });
8235
+ }
8236
+ if (input.approved_by !== undefined) {
8237
+ emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
8238
+ }
8239
+ return updatedTask;
7608
8240
  }
7609
8241
  function deleteTask(id, db) {
7610
8242
  const d = db || getDatabase();
@@ -8337,9 +8969,12 @@ function startTask(id, agentId, db) {
8337
8969
  throw new Error(`Task ${id} could not be started because it changed during claim`);
8338
8970
  }
8339
8971
  logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
8340
- dispatchWebhook("task.started", { id, agent_id: agentId, title: task.title }, d).catch(() => {});
8341
- emitLocalEventHooksQuiet({ type: "task.started", payload: { id, agent_id: agentId, title: task.title } });
8342
- 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 };
8972
+ 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 };
8973
+ const payload = taskEventData(startedTask, { agent_id: agentId });
8974
+ dispatchWebhook2("task.started", payload, d).catch(() => {});
8975
+ emitLocalEventHooksQuiet({ type: "task.started", payload });
8976
+ emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId } });
8977
+ return startedTask;
8343
8978
  }
8344
8979
  function completeTask(id, agentId, db, options) {
8345
8980
  const d = db || getDatabase();
@@ -8375,8 +9010,21 @@ function completeTask(id, agentId, db, options) {
8375
9010
  });
8376
9011
  tx();
8377
9012
  logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
8378
- dispatchWebhook("task.completed", { id, agent_id: agentId, title: task.title, completed_at: timestamp }, d).catch(() => {});
8379
- emitLocalEventHooksQuiet({ type: "task.completed", payload: { id, agent_id: agentId, title: task.title, completed_at: timestamp } });
9013
+ const completedTaskForEvent = {
9014
+ ...task,
9015
+ status: "completed",
9016
+ locked_by: null,
9017
+ locked_at: null,
9018
+ completed_at: timestamp,
9019
+ confidence,
9020
+ version: task.version + 1,
9021
+ updated_at: timestamp,
9022
+ metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
9023
+ };
9024
+ const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
9025
+ dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
9026
+ emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload });
9027
+ emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp } });
8380
9028
  let spawnedTask = null;
8381
9029
  if (task.recurrence_rule && !options?.skip_recurrence) {
8382
9030
  spawnedTask = spawnNextRecurrence(task, d, timestamp);
@@ -8417,8 +9065,12 @@ function completeTask(id, agentId, db, options) {
8417
9065
  if (unblockedDeps.length > 0) {
8418
9066
  meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
8419
9067
  for (const dep of unblockedDeps) {
8420
- dispatchWebhook("task.unblocked", { id: dep.id, unblocked_by: id, title: dep.title }, d).catch(() => {});
8421
- emitLocalEventHooksQuiet({ type: "task.unblocked", payload: { id: dep.id, unblocked_by: id, title: dep.title } });
9068
+ const depTask = getTask(dep.id, d);
9069
+ const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
9070
+ dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
9071
+ emitLocalEventHooksQuiet({ type: "task.unblocked", payload });
9072
+ if (depTask)
9073
+ emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id } });
8422
9074
  }
8423
9075
  }
8424
9076
  return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
@@ -8604,9 +9256,6 @@ function failTask(id, agentId, reason, options, db) {
8604
9256
  const timestamp = now();
8605
9257
  d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
8606
9258
  WHERE id = ?`, [JSON.stringify(meta), timestamp, id]);
8607
- logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
8608
- dispatchWebhook("task.failed", { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title }, d).catch(() => {});
8609
- emitLocalEventHooksQuiet({ type: "task.failed", payload: { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title } });
8610
9259
  const failedTask = {
8611
9260
  ...task,
8612
9261
  status: "failed",
@@ -8616,6 +9265,11 @@ function failTask(id, agentId, reason, options, db) {
8616
9265
  version: task.version + 1,
8617
9266
  updated_at: timestamp
8618
9267
  };
9268
+ logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
9269
+ const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
9270
+ dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
9271
+ emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload });
9272
+ emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning" });
8619
9273
  let retryTask;
8620
9274
  if (options?.retry) {
8621
9275
  const retryCount = (task.retry_count || 0) + 1;
@@ -8690,9 +9344,12 @@ function stealTask(agentId, opts, db) {
8690
9344
  return null;
8691
9345
  logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
8692
9346
  logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
8693
- dispatchWebhook("task.assigned", { id: target.id, agent_id: agentId, title: target.title, stolen_from: target.assigned_to }, d).catch(() => {});
8694
- emitLocalEventHooksQuiet({ type: "task.assigned", payload: { id: target.id, agent_id: agentId, title: target.title, stolen_from: target.assigned_to } });
8695
- return { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
9347
+ const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
9348
+ const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
9349
+ dispatchWebhook2("task.assigned", payload, d).catch(() => {});
9350
+ emitLocalEventHooksQuiet({ type: "task.assigned", payload });
9351
+ emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to } });
9352
+ return stolenTask;
8696
9353
  }
8697
9354
  function claimOrSteal(agentId, filters, db) {
8698
9355
  const d = db || getDatabase();
@@ -9751,8 +10408,8 @@ init_database();
9751
10408
  init_database();
9752
10409
  init_redaction();
9753
10410
  import { createHash as createHash2 } from "crypto";
9754
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
9755
- import { basename, dirname as dirname5, join as join5, resolve as resolve6 } from "path";
10411
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
10412
+ import { basename, dirname as dirname5, join as join6, resolve as resolve6 } from "path";
9756
10413
  import { tmpdir } from "os";
9757
10414
  function isInMemoryDb2(path) {
9758
10415
  return path === ":memory:" || path.startsWith("file::memory:");
@@ -9764,15 +10421,15 @@ function artifactStoreRoot() {
9764
10421
  return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
9765
10422
  const dbPath = getDatabasePath();
9766
10423
  if (isInMemoryDb2(dbPath))
9767
- return join5(tmpdir(), "hasna-todos-artifacts");
9768
- return join5(dirname5(resolve6(dbPath)), "artifacts");
10424
+ return join6(tmpdir(), "hasna-todos-artifacts");
10425
+ return join6(dirname5(resolve6(dbPath)), "artifacts");
9769
10426
  }
9770
10427
  function artifactStorePath(relativePath) {
9771
10428
  const normalized = relativePath.replace(/\\/g, "/");
9772
10429
  if (normalized.includes("..") || normalized.startsWith("/") || normalized.length === 0) {
9773
10430
  throw new Error("Invalid artifact store path");
9774
10431
  }
9775
- return join5(artifactStoreRoot(), normalized);
10432
+ return join6(artifactStoreRoot(), normalized);
9776
10433
  }
9777
10434
  function sha256(buffer) {
9778
10435
  return createHash2("sha256").update(buffer).digest("hex");
@@ -9813,7 +10470,7 @@ function mediaTypeFor(path, textLike) {
9813
10470
  }
9814
10471
  function storeArtifactContent(input) {
9815
10472
  const sourcePath = resolve6(input.path);
9816
- if (!existsSync7(sourcePath))
10473
+ if (!existsSync8(sourcePath))
9817
10474
  return null;
9818
10475
  const sourceStat = statSync2(sourcePath);
9819
10476
  if (!sourceStat.isFile())
@@ -9830,9 +10487,9 @@ function storeArtifactContent(input) {
9830
10487
  redactionStatus = "redacted";
9831
10488
  }
9832
10489
  const storedSha = sha256(storedBuffer);
9833
- const relativePath = join5("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
10490
+ const relativePath = join6("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
9834
10491
  const destination = artifactStorePath(relativePath);
9835
- if (!existsSync7(destination)) {
10492
+ if (!existsSync8(destination)) {
9836
10493
  mkdirSync4(dirname5(destination), { recursive: true });
9837
10494
  writeFileSync2(destination, storedBuffer);
9838
10495
  }
@@ -9892,7 +10549,7 @@ function verifyStoredArtifact(input) {
9892
10549
  };
9893
10550
  }
9894
10551
  const storedPath = artifactStorePath(store.relative_path);
9895
- if (!existsSync7(storedPath)) {
10552
+ if (!existsSync8(storedPath)) {
9896
10553
  return {
9897
10554
  id: input.id,
9898
10555
  path: input.path,
@@ -9973,15 +10630,15 @@ function getArtifactStoreRoot(dbPath) {
9973
10630
  return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
9974
10631
  const path = dbPath ?? getDatabasePath();
9975
10632
  if (isInMemoryDb2(path))
9976
- return join5(tmpdir(), "hasna-todos-artifacts");
9977
- return join5(dirname5(resolve6(path)), "artifacts");
10633
+ return join6(tmpdir(), "hasna-todos-artifacts");
10634
+ return join6(dirname5(resolve6(path)), "artifacts");
9978
10635
  }
9979
10636
  function computeContentHash(path) {
9980
10637
  return sha256(readFileSync4(resolve6(path)));
9981
10638
  }
9982
10639
  function storeArtifactFile(input) {
9983
10640
  const sourcePath = resolve6(input.sourcePath);
9984
- if (!existsSync7(sourcePath)) {
10641
+ if (!existsSync8(sourcePath)) {
9985
10642
  throw new Error(`Source file not found: ${input.sourcePath}`);
9986
10643
  }
9987
10644
  if (!statSync2(sourcePath).isFile()) {
@@ -9994,7 +10651,7 @@ function storeArtifactFile(input) {
9994
10651
  let localPath = sourcePath;
9995
10652
  if (storageMode === "copy") {
9996
10653
  const fileName = input.name && input.name.trim().length > 0 ? basename(input.name) : basename(sourcePath);
9997
- const destination = join5(getArtifactStoreRoot(input.dbPath), input.artifactId, fileName);
10654
+ const destination = join6(getArtifactStoreRoot(input.dbPath), input.artifactId, fileName);
9998
10655
  mkdirSync4(dirname5(destination), { recursive: true });
9999
10656
  writeFileSync2(destination, buffer);
10000
10657
  localPath = destination;
@@ -10004,7 +10661,7 @@ function storeArtifactFile(input) {
10004
10661
  function deleteStoredArtifactFile(localPath, storageMode, _dbPath2) {
10005
10662
  if (storageMode === "reference")
10006
10663
  return false;
10007
- if (!localPath || !existsSync7(localPath))
10664
+ if (!localPath || !existsSync8(localPath))
10008
10665
  return false;
10009
10666
  rmSync(localPath, { force: true });
10010
10667
  try {
@@ -10016,8 +10673,8 @@ function isArtifactExpired(deletedAt, policy = {}) {
10016
10673
  if (!deletedAt)
10017
10674
  return false;
10018
10675
  const retentionDays = policy.deleted_retention_days ?? DEFAULT_DELETED_RETENTION_DAYS;
10019
- const now2 = policy.now ?? new Date;
10020
- const ageMs = now2.getTime() - new Date(deletedAt).getTime();
10676
+ const now3 = policy.now ?? new Date;
10677
+ const ageMs = now3.getTime() - new Date(deletedAt).getTime();
10021
10678
  return ageMs > retentionDays * 24 * 60 * 60 * 1000;
10022
10679
  }
10023
10680
  function buildArtifactExportManifest(artifacts, dbPath) {
@@ -11783,7 +12440,7 @@ function writeOnboardingFixtureFiles(directory) {
11783
12440
  mkdirSync5(directory, { recursive: true });
11784
12441
  const files = [];
11785
12442
  for (const fixture of allFixtures()) {
11786
- const path = join6(directory, `${fixture.summary.name}.bridge.json`);
12443
+ const path = join7(directory, `${fixture.summary.name}.bridge.json`);
11787
12444
  writeFileSync3(path, `${JSON.stringify(fixture.bundle, null, 2)}
11788
12445
  `, "utf-8");
11789
12446
  files.push(path);
@@ -12609,7 +13266,7 @@ function renderLocalSnapshotMarkdown(snapshot) {
12609
13266
  }
12610
13267
  // src/lib/sdk-integration-fixtures.ts
12611
13268
  import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync5 } from "fs";
12612
- import { join as join7 } from "path";
13269
+ import { join as join8 } from "path";
12613
13270
 
12614
13271
  // src/cli-mcp-parity.ts
12615
13272
  function source4(version) {
@@ -13919,7 +14576,7 @@ function limits(input) {
13919
14576
  stale_after_hours: clamp(input.stale_after_hours, DEFAULT_LIMITS.stale_after_hours, 24 * 365)
13920
14577
  };
13921
14578
  }
13922
- function truncate(value, max) {
14579
+ function truncate2(value, max) {
13923
14580
  if (!value)
13924
14581
  return value ?? null;
13925
14582
  const redacted = redactEvidenceText(value);
@@ -13940,9 +14597,9 @@ function acceptanceCriteria(task2, maxText) {
13940
14597
  const metadata = task2.metadata || {};
13941
14598
  const raw = metadata["acceptance_criteria"] ?? metadata["acceptanceCriteria"] ?? metadata["criteria"];
13942
14599
  if (Array.isArray(raw))
13943
- return raw.map((item) => truncate(String(item), maxText)).filter((item) => Boolean(item));
14600
+ return raw.map((item) => truncate2(String(item), maxText)).filter((item) => Boolean(item));
13944
14601
  if (typeof raw === "string") {
13945
- return raw.split(/\r?\n/).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).map((line) => truncate(line, maxText)).filter((item) => Boolean(item));
14602
+ return raw.split(/\r?\n/).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).map((line) => truncate2(line, maxText)).filter((item) => Boolean(item));
13946
14603
  }
13947
14604
  return [];
13948
14605
  }
@@ -13965,7 +14622,7 @@ function addFile(files, path, source5, base) {
13965
14622
  path,
13966
14623
  status: base?.status || "active",
13967
14624
  agent_id: base?.agent_id ?? null,
13968
- note: truncate(base?.note, 240),
14625
+ note: truncate2(base?.note, 240),
13969
14626
  updated_at: base?.updated_at || "",
13970
14627
  sources: [source5]
13971
14628
  });
@@ -14023,7 +14680,7 @@ function estimateTokens(value) {
14023
14680
  return Math.max(1, Math.ceil((text || "").length / 4));
14024
14681
  }
14025
14682
  function summarizeStrings(values, maxChars) {
14026
- return truncate(values.filter(Boolean).join("; "), maxChars) || "No local details were available before this section was omitted.";
14683
+ return truncate2(values.filter(Boolean).join("; "), maxChars) || "No local details were available before this section was omitted.";
14027
14684
  }
14028
14685
  function summarizeSection(pack, section, maxChars) {
14029
14686
  if (section === "project")
@@ -14214,8 +14871,8 @@ function createAgentContextPack(input, db) {
14214
14871
  ...taskFiles.map((file) => file.updated_at)
14215
14872
  ], task2.updated_at);
14216
14873
  const warnings = [];
14217
- const now2 = input.now ? new Date(input.now) : new Date;
14218
- if (Date.parse(task2.updated_at) < now2.getTime() - limit.stale_after_hours * 60 * 60 * 1000) {
14874
+ const now3 = input.now ? new Date(input.now) : new Date;
14875
+ if (Date.parse(task2.updated_at) < now3.getTime() - limit.stale_after_hours * 60 * 60 * 1000) {
14219
14876
  warnings.push(`task state is older than ${limit.stale_after_hours} hours`);
14220
14877
  }
14221
14878
  if (comments.length > recentComments.length)
@@ -14230,7 +14887,7 @@ function createAgentContextPack(input, db) {
14230
14887
  id: task2.id,
14231
14888
  short_id: task2.short_id,
14232
14889
  title: redactEvidenceText(task2.title),
14233
- description: truncate(task2.description, limit.max_text_chars),
14890
+ description: truncate2(task2.description, limit.max_text_chars),
14234
14891
  status: task2.status,
14235
14892
  priority: task2.priority,
14236
14893
  assigned_to: task2.assigned_to,
@@ -14254,7 +14911,7 @@ function createAgentContextPack(input, db) {
14254
14911
  plan: plan ? {
14255
14912
  id: plan.id,
14256
14913
  name: plan.name,
14257
- description: truncate(plan.description, limit.max_text_chars),
14914
+ description: truncate2(plan.description, limit.max_text_chars),
14258
14915
  status: plan.status,
14259
14916
  agent_id: plan.agent_id,
14260
14917
  tasks: planTasks.slice(0, limit.plan_task_limit).map(taskSummary).filter((item) => Boolean(item)),
@@ -14273,7 +14930,7 @@ function createAgentContextPack(input, db) {
14273
14930
  type: comment.type,
14274
14931
  progress_pct: comment.progress_pct,
14275
14932
  created_at: comment.created_at,
14276
- content: truncate(comment.content, limit.max_text_chars) || ""
14933
+ content: truncate2(comment.content, limit.max_text_chars) || ""
14277
14934
  })),
14278
14935
  omitted: Math.max(0, comments.length - recentComments.length)
14279
14936
  },
@@ -14281,7 +14938,7 @@ function createAgentContextPack(input, db) {
14281
14938
  traceability: {
14282
14939
  commits: traceability.commits.map((commit) => ({
14283
14940
  sha: commit.sha,
14284
- message: truncate(commit.message, 240),
14941
+ message: truncate2(commit.message, 240),
14285
14942
  files_changed: commit.files_changed,
14286
14943
  committed_at: commit.committed_at
14287
14944
  })),
@@ -14289,7 +14946,7 @@ function createAgentContextPack(input, db) {
14289
14946
  verifications: verifications.map((verification) => ({
14290
14947
  command: verification.command,
14291
14948
  status: verification.status,
14292
- output_summary: truncate(verification.output_summary, limit.max_text_chars),
14949
+ output_summary: truncate2(verification.output_summary, limit.max_text_chars),
14293
14950
  artifact_path: verification.artifact_path,
14294
14951
  run_at: verification.run_at
14295
14952
  })),
@@ -14300,14 +14957,14 @@ function createAgentContextPack(input, db) {
14300
14957
  id: ledger.run.id,
14301
14958
  title: ledger.run.title,
14302
14959
  status: ledger.run.status,
14303
- summary: truncate(ledger.run.summary, limit.max_text_chars),
14960
+ summary: truncate2(ledger.run.summary, limit.max_text_chars),
14304
14961
  agent_id: ledger.run.agent_id,
14305
14962
  started_at: ledger.run.started_at,
14306
14963
  completed_at: ledger.run.completed_at,
14307
- events: ledger.events.map((event) => ({ event_type: event.event_type, message: truncate(event.message, 500), created_at: event.created_at })),
14308
- 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 })),
14309
- files: ledger.files.map((file) => ({ path: file.path, status: file.status, note: truncate(file.note, 240) })),
14310
- artifacts: ledger.artifacts.map((artifact) => ({ path: artifact.path, artifact_type: artifact.artifact_type, description: truncate(artifact.description, 240), sha256: artifact.sha256 }))
14964
+ events: ledger.events.map((event) => ({ event_type: event.event_type, message: truncate2(event.message, 500), created_at: event.created_at })),
14965
+ 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 })),
14966
+ files: ledger.files.map((file) => ({ path: file.path, status: file.status, note: truncate2(file.note, 240) })),
14967
+ artifacts: ledger.artifacts.map((artifact) => ({ path: artifact.path, artifact_type: artifact.artifact_type, description: truncate2(artifact.description, 240), sha256: artifact.sha256 }))
14311
14968
  })),
14312
14969
  omitted: Math.max(0, runs.length - selectedRuns.length)
14313
14970
  },
@@ -14412,7 +15069,7 @@ function renderAgentContextPackCompactMarkdown(pack) {
14412
15069
  const lines = [
14413
15070
  `# Context: ${pack.task.title}`,
14414
15071
  `${pack.task.status} | ${pack.task.priority} | ${pack.task.short_id || pack.task.id.slice(0, 8)}`,
14415
- pack.task.description ? truncate(pack.task.description, Math.min(pack.limits.summary_char_limit, 700)) : null,
15072
+ pack.task.description ? truncate2(pack.task.description, Math.min(pack.limits.summary_char_limit, 700)) : null,
14416
15073
  "",
14417
15074
  "## Must Know",
14418
15075
  bullet([
@@ -14558,7 +15215,7 @@ function writeSdkIntegrationFixtures(directory, options = {}) {
14558
15215
  ];
14559
15216
  const written = [];
14560
15217
  for (const [name, payload] of files) {
14561
- const file = join7(directory, name);
15218
+ const file = join8(directory, name);
14562
15219
  writeFileSync5(file, `${JSON.stringify(payload, null, 2)}
14563
15220
  `, "utf-8");
14564
15221
  written.push(file);
@@ -15275,7 +15932,7 @@ function createRoadmap(input) {
15275
15932
  if (!name)
15276
15933
  throw new Error("Roadmap name is required");
15277
15934
  const store = readStore();
15278
- const now2 = timestamp();
15935
+ const now3 = timestamp();
15279
15936
  const roadmap = {
15280
15937
  id: newId("roadmap"),
15281
15938
  name,
@@ -15286,8 +15943,8 @@ function createRoadmap(input) {
15286
15943
  agent_id: cleanString(input.agent_id),
15287
15944
  release: cleanString(input.release),
15288
15945
  milestone_ids: [],
15289
- created_at: now2,
15290
- updated_at: now2
15946
+ created_at: now3,
15947
+ updated_at: now3
15291
15948
  };
15292
15949
  store.roadmaps[roadmap.id] = roadmap;
15293
15950
  writeStore(store);
@@ -15347,7 +16004,7 @@ function createMilestone(input) {
15347
16004
  const title = input.title.trim();
15348
16005
  if (!title)
15349
16006
  throw new Error("Milestone title is required");
15350
- const now2 = timestamp();
16007
+ const now3 = timestamp();
15351
16008
  const milestone = {
15352
16009
  id: newId("milestone"),
15353
16010
  roadmap_id: roadmapId,
@@ -15362,11 +16019,11 @@ function createMilestone(input) {
15362
16019
  run_ids: cleanList3(input.run_ids),
15363
16020
  release: cleanString(input.release ?? roadmap.release ?? undefined),
15364
16021
  tags: cleanList3(input.tags),
15365
- created_at: now2,
15366
- updated_at: now2
16022
+ created_at: now3,
16023
+ updated_at: now3
15367
16024
  };
15368
16025
  store.milestones[milestone.id] = milestone;
15369
- store.roadmaps[roadmapId] = { ...roadmap, milestone_ids: cleanList3([...roadmap.milestone_ids, milestone.id]), updated_at: now2 };
16026
+ store.roadmaps[roadmapId] = { ...roadmap, milestone_ids: cleanList3([...roadmap.milestone_ids, milestone.id]), updated_at: now3 };
15370
16027
  writeStore(store);
15371
16028
  return milestone;
15372
16029
  }
@@ -15426,7 +16083,7 @@ function upsertReleaseGroup(input) {
15426
16083
  throw new Error("Release group name is required");
15427
16084
  const key = releaseKey(roadmapId, name);
15428
16085
  const existing = store.releases[key];
15429
- const now2 = timestamp();
16086
+ const now3 = timestamp();
15430
16087
  const release = {
15431
16088
  name,
15432
16089
  version: input.version === undefined ? existing?.version ?? null : cleanString(input.version),
@@ -15437,8 +16094,8 @@ function upsertReleaseGroup(input) {
15437
16094
  plan_ids: input.plan_ids === undefined ? existing?.plan_ids ?? [] : cleanList3(input.plan_ids),
15438
16095
  run_ids: input.run_ids === undefined ? existing?.run_ids ?? [] : cleanList3(input.run_ids),
15439
16096
  notes: input.notes === undefined ? existing?.notes ?? null : cleanString(input.notes),
15440
- created_at: existing?.created_at ?? now2,
15441
- updated_at: now2
16097
+ created_at: existing?.created_at ?? now3,
16098
+ updated_at: now3
15442
16099
  };
15443
16100
  store.releases[key] = release;
15444
16101
  writeStore(store);
@@ -15829,7 +16486,7 @@ function renderLocalAuditLedgerMarkdown(ledger) {
15829
16486
  init_migrations();
15830
16487
  init_schema();
15831
16488
  import { readFileSync as readFileSync6 } from "fs";
15832
- import { join as join8, resolve as resolve8 } from "path";
16489
+ import { join as join9, resolve as resolve8 } from "path";
15833
16490
  import { Database as Database2 } from "bun:sqlite";
15834
16491
  var LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION = 1;
15835
16492
  var EXPECTED_PACKAGE_NAME = "@hasna/todos";
@@ -15875,7 +16532,7 @@ function warn(id, message, details) {
15875
16532
  return { id, status: "warning", message, details };
15876
16533
  }
15877
16534
  function readPackageJson(root) {
15878
- return JSON.parse(readFileSync6(join8(root, "package.json"), "utf8"));
16535
+ return JSON.parse(readFileSync6(join9(root, "package.json"), "utf8"));
15879
16536
  }
15880
16537
  function sortedKeys(value) {
15881
16538
  return Object.keys(value ?? {}).sort((left, right) => left.localeCompare(right));
@@ -17287,6 +17944,470 @@ function importExternalIssues(input, db) {
17287
17944
  ]
17288
17945
  };
17289
17946
  }
17947
+ // src/lib/tester-issue-reports.ts
17948
+ init_database();
17949
+ import { createHash as createHash7 } from "crypto";
17950
+ init_redaction();
17951
+ var TESTERS_ISSUE_REPORT_SCHEMA_VERSION = "testers.issue_report.v1";
17952
+ var TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION = "todos.tester_issue_report_result.v1";
17953
+ var TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION = "todos.tester_issue_report_batch_result.v1";
17954
+ var PRIORITIES3 = ["low", "medium", "high", "critical"];
17955
+ var SEVERITIES = new Set(PRIORITIES3);
17956
+ var KINDS = new Set([
17957
+ "assertion_failure",
17958
+ "runtime_error",
17959
+ "console_error",
17960
+ "network_error",
17961
+ "visual_regression",
17962
+ "accessibility",
17963
+ "performance",
17964
+ "broken_link",
17965
+ "security",
17966
+ "unknown"
17967
+ ]);
17968
+ function asObject3(value) {
17969
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
17970
+ }
17971
+ function asString3(value) {
17972
+ if (typeof value === "string" && value.trim())
17973
+ return value.trim();
17974
+ if (typeof value === "number" && Number.isFinite(value))
17975
+ return String(value);
17976
+ return null;
17977
+ }
17978
+ function stringArray(value, limit = 20) {
17979
+ if (!Array.isArray(value))
17980
+ return [];
17981
+ return [...new Set(value.map((item) => asString3(item)).filter((item) => Boolean(item)))].slice(0, limit);
17982
+ }
17983
+ function objectArray(value, limit = 20) {
17984
+ if (!Array.isArray(value))
17985
+ return [];
17986
+ return value.map(asObject3).filter((item) => Object.keys(item).length > 0).slice(0, limit);
17987
+ }
17988
+ function cleanKey(value) {
17989
+ return value.toLowerCase().trim().replace(/[^a-z0-9._:-]+/g, "-").replace(/^-+|-+$/g, "");
17990
+ }
17991
+ function truncate3(value, max) {
17992
+ if (!value)
17993
+ return;
17994
+ const redacted = redactEvidenceText(value).trim();
17995
+ if (!redacted)
17996
+ return;
17997
+ return redacted.length > max ? `${redacted.slice(0, max - 3)}...` : redacted;
17998
+ }
17999
+ function normalizeText2(value) {
18000
+ return (value || "").toLowerCase().replace(/https?:\/\/\S+/g, " ").replace(/[`"'()[\]{}.,:;!?/#\\_-]+/g, " ").replace(/\s+/g, " ").trim();
18001
+ }
18002
+ function normalizeUrlPattern(value) {
18003
+ if (!value)
18004
+ return "";
18005
+ try {
18006
+ const url = new URL(value);
18007
+ return `${url.origin.toLowerCase()}${url.pathname.replace(/\/+$/, "") || "/"}`;
18008
+ } catch {
18009
+ return value.split(/[?#]/, 1)[0].replace(/\/+$/, "").toLowerCase();
18010
+ }
18011
+ }
18012
+ function normalizeKind(value) {
18013
+ const raw = cleanKey(asString3(value) || "unknown").replace(/-/g, "_");
18014
+ return KINDS.has(raw) ? raw : raw || "unknown";
18015
+ }
18016
+ function normalizeSeverity(value, fallback) {
18017
+ const raw = cleanKey(asString3(value) || fallback);
18018
+ if (SEVERITIES.has(raw))
18019
+ return raw;
18020
+ if (/^(p0|blocker|urgent|highest)$/.test(raw))
18021
+ return "critical";
18022
+ if (/^(p1|major)$/.test(raw))
18023
+ return "high";
18024
+ if (/^(p3|minor|info)$/.test(raw))
18025
+ return "low";
18026
+ return fallback;
18027
+ }
18028
+ function reportSource(input) {
18029
+ const source6 = asObject3(input["source"]);
18030
+ const normalized = {
18031
+ tool: asString3(source6["tool"]) || asString3(input["tool"]) || "testers",
18032
+ run_id: asString3(source6["run_id"]) || asString3(input["run_id"]) || undefined,
18033
+ result_id: asString3(source6["result_id"]) || asString3(input["result_id"]) || undefined,
18034
+ scenario_id: asString3(source6["scenario_id"]) || asString3(input["scenario_id"]) || undefined,
18035
+ scenario_name: asString3(source6["scenario_name"]) || asString3(input["scenario_name"]) || undefined,
18036
+ project_id: asString3(source6["project_id"]) || asString3(input["project_id"]) || undefined,
18037
+ url: asString3(source6["url"]) || asString3(input["url"]) || undefined,
18038
+ page_url: asString3(source6["page_url"]) || asString3(input["page_url"]) || undefined,
18039
+ artifact_url: asString3(source6["artifact_url"]) || undefined,
18040
+ screenshot_url: asString3(source6["screenshot_url"]) || undefined,
18041
+ commit: asString3(source6["commit"]) || undefined,
18042
+ branch: asString3(source6["branch"]) || undefined
18043
+ };
18044
+ return Object.values(normalized).some(Boolean) ? normalized : undefined;
18045
+ }
18046
+ function reportTarget(input) {
18047
+ const target = asObject3(input["target"]);
18048
+ const normalized = {
18049
+ url: asString3(target["url"]) || asString3(input["target_url"]) || undefined,
18050
+ route: asString3(target["route"]) || undefined,
18051
+ selector: asString3(target["selector"]) || undefined,
18052
+ component: asString3(target["component"]) || undefined,
18053
+ browser: asString3(target["browser"]) || undefined,
18054
+ viewport: asString3(target["viewport"]) || undefined
18055
+ };
18056
+ return Object.values(normalized).some(Boolean) ? normalized : undefined;
18057
+ }
18058
+ function reportFailure(input) {
18059
+ const failure = asObject3(input["failure"]);
18060
+ const steps = stringArray(failure["steps"] ?? input["steps"], 50);
18061
+ const normalized = {
18062
+ message: truncate3(asString3(failure["message"]) || asString3(input["error"]) || asString3(input["message"]), 1000),
18063
+ expected: truncate3(asString3(failure["expected"]), 1000),
18064
+ actual: truncate3(asString3(failure["actual"]), 1000),
18065
+ stack: truncate3(asString3(failure["stack"]) || asString3(input["stack"]), 3000),
18066
+ reasoning: truncate3(asString3(failure["reasoning"]) || asString3(input["reasoning"]), 1500),
18067
+ steps: steps.length > 0 ? steps.map((step) => truncate3(step, 400)).filter(Boolean) : undefined
18068
+ };
18069
+ return Object.values(normalized).some(Boolean) ? normalized : undefined;
18070
+ }
18071
+ function artifactArray(value) {
18072
+ return objectArray(value, 12).map((item) => ({
18073
+ kind: asString3(item["kind"]) || undefined,
18074
+ label: asString3(item["label"]) || asString3(item["name"]) || undefined,
18075
+ path: asString3(item["path"]) || asString3(item["file_path"]) || undefined,
18076
+ url: asString3(item["url"]) || undefined
18077
+ })).filter((item) => item.kind || item.label || item.path || item.url);
18078
+ }
18079
+ function reportEvidence(input) {
18080
+ const evidence = asObject3(input["evidence"]);
18081
+ const logs = stringArray(evidence["logs"], 8).map((log) => truncate3(log, 1000)).filter(Boolean);
18082
+ const screenshots = artifactArray(evidence["screenshots"] ?? input["screenshots"]);
18083
+ const artifacts = artifactArray(evidence["artifacts"] ?? input["artifacts"]);
18084
+ const normalized = {
18085
+ logs: logs.length > 0 ? logs : undefined,
18086
+ screenshots: screenshots.length > 0 ? screenshots : undefined,
18087
+ artifacts: artifacts.length > 0 ? artifacts : undefined
18088
+ };
18089
+ return Object.values(normalized).some(Boolean) ? normalized : undefined;
18090
+ }
18091
+ function normalizeTesterIssueReport(value, fallbackPriority = "medium") {
18092
+ const input = asObject3(value);
18093
+ if (input["schema_version"] !== TESTERS_ISSUE_REPORT_SCHEMA_VERSION) {
18094
+ throw new Error(`Expected schema_version ${TESTERS_ISSUE_REPORT_SCHEMA_VERSION}`);
18095
+ }
18096
+ const failure = reportFailure(input);
18097
+ const source6 = reportSource(input);
18098
+ const target = reportTarget(input);
18099
+ const title = truncate3(asString3(input["title"]) || asString3(input["summary"]) || failure?.message || source6?.scenario_name || "Tester issue report", 220);
18100
+ if (!title)
18101
+ throw new Error("Tester issue report requires a title");
18102
+ const labels = [
18103
+ ...stringArray(input["labels"], 20),
18104
+ ...stringArray(input["tags"], 20)
18105
+ ].map(cleanKey).filter(Boolean);
18106
+ const report = {
18107
+ schema_version: TESTERS_ISSUE_REPORT_SCHEMA_VERSION,
18108
+ id: asString3(input["id"]) || undefined,
18109
+ fingerprint: asString3(input["fingerprint"]) || undefined,
18110
+ title,
18111
+ summary: truncate3(asString3(input["summary"]), 1000) ?? null,
18112
+ kind: normalizeKind(input["kind"] ?? input["type"]),
18113
+ severity: normalizeSeverity(input["severity"] ?? input["priority"], fallbackPriority),
18114
+ source: source6,
18115
+ target,
18116
+ failure,
18117
+ evidence: reportEvidence(input),
18118
+ labels: labels.length > 0 ? [...new Set(labels)].slice(0, 20) : undefined,
18119
+ metadata: redactValue(asObject3(input["metadata"])),
18120
+ occurred_at: asString3(input["occurred_at"]) || asString3(input["timestamp"]) || undefined
18121
+ };
18122
+ return redactValue(report);
18123
+ }
18124
+ function fingerprintTesterIssueReport(report) {
18125
+ if (report.fingerprint)
18126
+ return `testers:${cleanKey(report.fingerprint)}`;
18127
+ const stackTop = report.failure?.stack?.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || "";
18128
+ const url = normalizeUrlPattern(report.target?.url || report.source?.page_url || report.source?.url || "");
18129
+ const raw = [
18130
+ report.kind || "unknown",
18131
+ report.source?.project_id || "",
18132
+ report.source?.scenario_id || report.source?.scenario_name || "",
18133
+ url,
18134
+ report.target?.route || "",
18135
+ report.target?.selector || report.target?.component || "",
18136
+ normalizeText2(report.failure?.message || report.summary || report.title).slice(0, 240),
18137
+ normalizeText2(stackTop).slice(0, 160)
18138
+ ].join("::");
18139
+ return `testers:${createHash7("sha256").update(raw).digest("hex").slice(0, 16)}`;
18140
+ }
18141
+ function priorityForSeverity(severity, fallback) {
18142
+ return PRIORITIES3.includes(severity) ? severity : fallback;
18143
+ }
18144
+ function maxPriority(left, right) {
18145
+ return PRIORITIES3.indexOf(right) > PRIORITIES3.indexOf(left) ? right : left;
18146
+ }
18147
+ function taskTitle2(report) {
18148
+ const title = report.title.replace(/^BUG:\s*/i, "").replace(/^\[testers\]\s*/i, "");
18149
+ return `BUG: [testers] ${title}`.slice(0, 240);
18150
+ }
18151
+ function evidenceLines(report) {
18152
+ const lines = [];
18153
+ for (const item of report.evidence?.screenshots || []) {
18154
+ lines.push(`Screenshot: ${item.label || item.kind || item.path || item.url}${item.path ? ` (${item.path})` : item.url ? ` (${item.url})` : ""}`);
18155
+ }
18156
+ for (const item of report.evidence?.artifacts || []) {
18157
+ lines.push(`Artifact: ${item.label || item.kind || item.path || item.url}${item.path ? ` (${item.path})` : item.url ? ` (${item.url})` : ""}`);
18158
+ }
18159
+ for (const log of report.evidence?.logs || [])
18160
+ lines.push(`Log: ${log}`);
18161
+ return lines.slice(0, 12);
18162
+ }
18163
+ function taskDescription2(report, fingerprint2) {
18164
+ const failure = report.failure;
18165
+ const lines = [
18166
+ "Tester issue report.",
18167
+ "",
18168
+ `Schema: ${report.schema_version}`,
18169
+ `Fingerprint: ${fingerprint2}`,
18170
+ `Kind: ${report.kind || "unknown"}`,
18171
+ `Severity: ${report.severity || "medium"}`,
18172
+ report.source?.run_id ? `Run: ${report.source.run_id}` : null,
18173
+ report.source?.result_id ? `Result: ${report.source.result_id}` : null,
18174
+ report.source?.scenario_name || report.source?.scenario_id ? `Scenario: ${report.source.scenario_name || report.source.scenario_id}` : null,
18175
+ report.target?.url || report.source?.page_url || report.source?.url ? `URL: ${report.target?.url || report.source?.page_url || report.source?.url}` : null,
18176
+ report.target?.route ? `Route: ${report.target.route}` : null,
18177
+ report.target?.selector ? `Selector: ${report.target.selector}` : null,
18178
+ report.occurred_at ? `Occurred at: ${report.occurred_at}` : null,
18179
+ "",
18180
+ report.summary ? `Summary:
18181
+ ${report.summary}` : null,
18182
+ failure?.message ? `Failure:
18183
+ ${failure.message}` : null,
18184
+ failure?.expected ? `Expected:
18185
+ ${failure.expected}` : null,
18186
+ failure?.actual ? `Actual:
18187
+ ${failure.actual}` : null,
18188
+ failure?.reasoning ? `Reasoning:
18189
+ ${failure.reasoning}` : null,
18190
+ failure?.steps?.length ? `Steps:
18191
+ ${failure.steps.map((step, index) => `${index + 1}. ${step}`).join(`
18192
+ `)}` : null,
18193
+ evidenceLines(report).length ? `Evidence:
18194
+ ${evidenceLines(report).map((line) => `- ${line}`).join(`
18195
+ `)}` : null,
18196
+ failure?.stack ? `Stack:
18197
+ ${failure.stack}` : null
18198
+ ].filter((line) => line !== null);
18199
+ return lines.join(`
18200
+ `).replace(/\n{3,}/g, `
18201
+
18202
+ `).slice(0, 6000);
18203
+ }
18204
+ function taskTags(report) {
18205
+ return [...new Set([
18206
+ "bug",
18207
+ "testers",
18208
+ "tester-report",
18209
+ report.kind ? cleanKey(String(report.kind)).replace(/_/g, "-") : "unknown",
18210
+ ...report.labels || []
18211
+ ].filter(Boolean))].slice(0, 16);
18212
+ }
18213
+ function storedReportSummary(report) {
18214
+ return {
18215
+ id: report.id ?? null,
18216
+ title: report.title,
18217
+ kind: report.kind ?? "unknown",
18218
+ severity: report.severity ?? "medium",
18219
+ run_id: report.source?.run_id ?? null,
18220
+ result_id: report.source?.result_id ?? null,
18221
+ scenario_id: report.source?.scenario_id ?? null,
18222
+ scenario_name: report.source?.scenario_name ?? null,
18223
+ url: report.target?.url ?? report.source?.page_url ?? report.source?.url ?? null,
18224
+ occurred_at: report.occurred_at ?? null
18225
+ };
18226
+ }
18227
+ function testerMetadata(report, fingerprint2, previous, timestamp2) {
18228
+ const occurrenceCount = typeof previous?.["occurrence_count"] === "number" ? previous["occurrence_count"] + 1 : 1;
18229
+ const previousRecent = previous?.["recent_reports"];
18230
+ const recent = Array.isArray(previousRecent) ? previousRecent : [];
18231
+ return {
18232
+ schema_version: TESTERS_ISSUE_REPORT_SCHEMA_VERSION,
18233
+ fingerprint: fingerprint2,
18234
+ first_seen_at: asString3(previous?.["first_seen_at"]) || timestamp2,
18235
+ last_seen_at: timestamp2,
18236
+ occurrence_count: occurrenceCount,
18237
+ latest_report: storedReportSummary(report),
18238
+ recent_reports: [...recent.slice(-4), storedReportSummary(report)]
18239
+ };
18240
+ }
18241
+ function sourceMetadata(report) {
18242
+ const url = report.target?.url || report.source?.page_url || report.source?.url || null;
18243
+ return {
18244
+ ...url ? { source_url: url, external_url: url, issue_url: url } : {},
18245
+ ...report.source?.run_id ? { tester_run_id: report.source.run_id } : {},
18246
+ ...report.source?.result_id ? { tester_result_id: report.source.result_id } : {},
18247
+ ...report.source?.scenario_id ? { tester_scenario_id: report.source.scenario_id } : {},
18248
+ ...report.source?.project_id ? { tester_project_id: report.source.project_id } : {}
18249
+ };
18250
+ }
18251
+ function findExistingTask2(fingerprint2, input, db) {
18252
+ for (const task2 of listTasks({
18253
+ include_archived: true,
18254
+ project_id: input.project_id,
18255
+ task_list_id: input.task_list_id
18256
+ }, db)) {
18257
+ const metadata = task2.metadata || {};
18258
+ const tester = asObject3(metadata["tester_issue_report"]);
18259
+ if (tester["fingerprint"] === fingerprint2)
18260
+ return task2;
18261
+ if (metadata["tester_issue_fingerprint"] === fingerprint2)
18262
+ return task2;
18263
+ if (metadata["external_ref"] === fingerprint2)
18264
+ return task2;
18265
+ }
18266
+ return null;
18267
+ }
18268
+ function commandsFor(task2) {
18269
+ return [
18270
+ "todos issues report --file tester-report.json --apply --json",
18271
+ task2 ? `todos show ${task2.id.slice(0, 8)}` : `todos list --tags tester-report --json`,
18272
+ `todos dedupe scan --threshold 0.8 --json`
18273
+ ];
18274
+ }
18275
+ function updateExistingTask(task2, report, fingerprint2, input, timestamp2, db) {
18276
+ if (input.update_existing === false)
18277
+ return { action: "matched", task: task2 };
18278
+ const previous = asObject3(task2.metadata["tester_issue_report"]);
18279
+ const severityPriority = priorityForSeverity(report.severity, input.default_priority || "medium");
18280
+ const nextStatus = task2.status === "completed" || task2.status === "cancelled" ? "pending" : task2.status;
18281
+ const action = nextStatus !== task2.status ? "regressed" : "updated";
18282
+ const updated = updateTask(task2.id, {
18283
+ version: task2.version,
18284
+ title: taskTitle2(report),
18285
+ description: taskDescription2(report, fingerprint2),
18286
+ priority: maxPriority(task2.priority, severityPriority),
18287
+ status: nextStatus,
18288
+ completed_at: nextStatus !== task2.status ? null : undefined,
18289
+ tags: [...new Set([...task2.tags, ...taskTags(report)])],
18290
+ metadata: {
18291
+ ...task2.metadata,
18292
+ ...sourceMetadata(report),
18293
+ external_ref: fingerprint2,
18294
+ tester_issue_fingerprint: fingerprint2,
18295
+ tester_issue_report: testerMetadata(report, fingerprint2, previous, timestamp2)
18296
+ },
18297
+ ...input.assigned_to !== undefined ? { assigned_to: input.assigned_to } : {},
18298
+ task_type: task2.task_type || "bug"
18299
+ }, db);
18300
+ return { action, task: updated };
18301
+ }
18302
+ function upsertTesterIssueReport(input, db) {
18303
+ const d = db || getDatabase();
18304
+ const timestamp2 = now();
18305
+ const warnings = [];
18306
+ const report = normalizeTesterIssueReport(input.report, input.default_priority || "medium");
18307
+ const fingerprint2 = fingerprintTesterIssueReport(report);
18308
+ const existing = findExistingTask2(fingerprint2, input, d);
18309
+ if (!input.apply) {
18310
+ const action = existing ? "matched" : "preview";
18311
+ return {
18312
+ schema_version: TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
18313
+ local_only: true,
18314
+ dry_run: true,
18315
+ processed_at: timestamp2,
18316
+ action,
18317
+ fingerprint: fingerprint2,
18318
+ report,
18319
+ task: existing,
18320
+ warnings,
18321
+ commands: commandsFor(existing)
18322
+ };
18323
+ }
18324
+ if (existing) {
18325
+ const updated = updateExistingTask(existing, report, fingerprint2, input, timestamp2, d);
18326
+ return {
18327
+ schema_version: TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
18328
+ local_only: true,
18329
+ dry_run: false,
18330
+ processed_at: timestamp2,
18331
+ action: updated.action,
18332
+ fingerprint: fingerprint2,
18333
+ report,
18334
+ task: updated.task,
18335
+ warnings,
18336
+ commands: commandsFor(updated.task)
18337
+ };
18338
+ }
18339
+ const priority = priorityForSeverity(report.severity, input.default_priority || "medium");
18340
+ const task2 = createTask({
18341
+ title: taskTitle2(report),
18342
+ description: taskDescription2(report, fingerprint2),
18343
+ priority,
18344
+ status: "pending",
18345
+ tags: taskTags(report),
18346
+ metadata: {
18347
+ ...sourceMetadata(report),
18348
+ external_ref: fingerprint2,
18349
+ tester_issue_fingerprint: fingerprint2,
18350
+ tester_issue_report: testerMetadata(report, fingerprint2, null, timestamp2),
18351
+ tester_issue_report_raw: redactValue({
18352
+ ...report,
18353
+ evidence: report.evidence ? {
18354
+ screenshots: report.evidence.screenshots,
18355
+ artifacts: report.evidence.artifacts
18356
+ } : undefined
18357
+ })
18358
+ },
18359
+ project_id: input.project_id,
18360
+ task_list_id: input.task_list_id,
18361
+ agent_id: input.agent_id,
18362
+ assigned_to: input.assigned_to,
18363
+ task_type: "bug"
18364
+ }, d);
18365
+ return {
18366
+ schema_version: TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
18367
+ local_only: true,
18368
+ dry_run: false,
18369
+ processed_at: timestamp2,
18370
+ action: "created",
18371
+ fingerprint: fingerprint2,
18372
+ report,
18373
+ task: task2,
18374
+ warnings,
18375
+ commands: commandsFor(task2)
18376
+ };
18377
+ }
18378
+ function upsertTesterIssueReports(input, db) {
18379
+ const d = db || getDatabase();
18380
+ const run = () => input.reports.map((report) => upsertTesterIssueReport({ ...input, report }, d));
18381
+ const results = input.apply ? d.transaction(run)() : run();
18382
+ const summary = {
18383
+ total: results.length,
18384
+ preview: 0,
18385
+ matched: 0,
18386
+ created: 0,
18387
+ updated: 0,
18388
+ regressed: 0
18389
+ };
18390
+ for (const result of results)
18391
+ summary[result.action]++;
18392
+ return {
18393
+ schema_version: TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION,
18394
+ local_only: true,
18395
+ dry_run: !input.apply,
18396
+ processed_at: now(),
18397
+ results,
18398
+ summary
18399
+ };
18400
+ }
18401
+ function readTesterIssueReportsPayload(value) {
18402
+ if (Array.isArray(value))
18403
+ return value;
18404
+ const record = asObject3(value);
18405
+ if (Array.isArray(record["reports"]))
18406
+ return record["reports"];
18407
+ if (Array.isArray(record["issues"]))
18408
+ return record["issues"];
18409
+ return [value];
18410
+ }
17290
18411
  // src/lib/local-notifications.ts
17291
18412
  init_database();
17292
18413
 
@@ -17389,13 +18510,13 @@ function eventSeverity(eventType) {
17389
18510
  function payloadText(payload) {
17390
18511
  return JSON.stringify(payload).toLowerCase();
17391
18512
  }
17392
- function asString3(value) {
18513
+ function asString4(value) {
17393
18514
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
17394
18515
  }
17395
18516
  function fieldMatches(allowed, value) {
17396
18517
  if (!allowed || allowed.length === 0)
17397
18518
  return true;
17398
- const stringValue = asString3(value);
18519
+ const stringValue = asString4(value);
17399
18520
  return Boolean(stringValue && allowed.includes(stringValue));
17400
18521
  }
17401
18522
  function containsMatches(needles, payload) {
@@ -17461,8 +18582,8 @@ function evaluateTerminalWatchRules(input, rules = listTerminalNotificationRules
17461
18582
  if (isQuietTime(rule.quiet_hours, timestamp2))
17462
18583
  skipped.push("quiet hours active");
17463
18584
  const matched = skipped.length === 0;
17464
- const title = asString3(payload["title"]) || asString3(payload["name"]) || input.type;
17465
- const taskId = asString3(payload["id"]) || asString3(payload["task_id"]);
18585
+ const title = asString4(payload["title"]) || asString4(payload["name"]) || input.type;
18586
+ const taskId = asString4(payload["id"]) || asString4(payload["task_id"]);
17466
18587
  const notification = {
17467
18588
  rule: rule.name,
17468
18589
  event_type: input.type,
@@ -17471,8 +18592,8 @@ function evaluateTerminalWatchRules(input, rules = listTerminalNotificationRules
17471
18592
  message: `${input.type}: ${title}`,
17472
18593
  timestamp: timestamp2,
17473
18594
  task_id: taskId,
17474
- project_id: asString3(payload["project_id"]),
17475
- agent_id: asString3(payload["agent_id"]) || asString3(payload["assigned_to"]),
18595
+ project_id: asString4(payload["project_id"]),
18596
+ agent_id: asString4(payload["agent_id"]) || asString4(payload["assigned_to"]),
17476
18597
  bell: rule.bell && severity === "critical",
17477
18598
  payload
17478
18599
  };
@@ -18122,12 +19243,12 @@ function summarizeTask(task2) {
18122
19243
  };
18123
19244
  }
18124
19245
  function overdueTasks(tasks, nowIso) {
18125
- const now2 = Date.parse(nowIso);
19246
+ const now3 = Date.parse(nowIso);
18126
19247
  return tasks.filter((task2) => {
18127
19248
  if (isTerminal(task2) || !task2.due_at)
18128
19249
  return false;
18129
19250
  const due = Date.parse(task2.due_at);
18130
- return Number.isFinite(due) && due < now2;
19251
+ return Number.isFinite(due) && due < now3;
18131
19252
  });
18132
19253
  }
18133
19254
  function isReady(task2, db) {
@@ -18460,7 +19581,7 @@ function renderLocalReportMarkdown(report) {
18460
19581
  // src/lib/local-encryption.ts
18461
19582
  init_config();
18462
19583
  init_redaction();
18463
- import { createCipheriv, createDecipheriv, createHash as createHash7, randomBytes, scryptSync, timingSafeEqual } from "crypto";
19584
+ import { createCipheriv, createDecipheriv, createHash as createHash8, randomBytes, scryptSync, timingSafeEqual as timingSafeEqual2 } from "crypto";
18464
19585
  var TODOS_ENCRYPTED_VALUE_KIND = "hasna.todos.encrypted-value";
18465
19586
  var TODOS_ENCRYPTED_BRIDGE_KIND = "hasna.todos.encrypted-bridge";
18466
19587
  var TODOS_ENCRYPTION_SCHEMA_VERSION = 1;
@@ -18482,11 +19603,11 @@ class EncryptedPayloadError extends Error {
18482
19603
  super(message);
18483
19604
  }
18484
19605
  }
18485
- function now2() {
19606
+ function now3() {
18486
19607
  return new Date().toISOString();
18487
19608
  }
18488
19609
  function sha2564(value) {
18489
- return createHash7("sha256").update(value).digest("hex");
19610
+ return createHash8("sha256").update(value).digest("hex");
18490
19611
  }
18491
19612
  function normalizeProfileName(value) {
18492
19613
  const name = (value || DEFAULT_ENCRYPTION_PROFILE).trim();
@@ -18519,7 +19640,7 @@ function upsertEncryptionProfile(input) {
18519
19640
  const name = normalizeProfileName(input.name);
18520
19641
  const config = loadConfig();
18521
19642
  const existing = config.encryption_profiles?.[name];
18522
- const timestamp2 = now2();
19643
+ const timestamp2 = now3();
18523
19644
  const profile = {
18524
19645
  name,
18525
19646
  algorithm: "aes-256-gcm",
@@ -18576,7 +19697,7 @@ function encryptString(plaintext, options = {}) {
18576
19697
  return {
18577
19698
  schemaVersion: TODOS_ENCRYPTION_SCHEMA_VERSION,
18578
19699
  kind: TODOS_ENCRYPTED_VALUE_KIND,
18579
- encryptedAt: options.encryptedAt ?? now2(),
19700
+ encryptedAt: options.encryptedAt ?? now3(),
18580
19701
  profile: profile.name,
18581
19702
  key_env: profile.key_env,
18582
19703
  algorithm: "aes-256-gcm",
@@ -18611,7 +19732,7 @@ function decryptString(envelope, env = process.env) {
18611
19732
  ]).toString("utf8");
18612
19733
  const expected = Buffer.from(envelope.plaintext_sha256, "hex");
18613
19734
  const actual = Buffer.from(sha2564(plaintext), "hex");
18614
- if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
19735
+ if (expected.length !== actual.length || !timingSafeEqual2(expected, actual)) {
18615
19736
  throw new EncryptedPayloadError("decrypted payload checksum mismatch");
18616
19737
  }
18617
19738
  return plaintext;
@@ -20702,7 +21823,7 @@ function createHybridTodosStorageAdapter(options) {
20702
21823
  }
20703
21824
 
20704
21825
  // src/storage/postgres-adapter.ts
20705
- import { randomUUID as randomUUID2 } from "crypto";
21826
+ import { randomUUID as randomUUID3 } from "crypto";
20706
21827
  function createPostgresTodosStorageAdapter(options) {
20707
21828
  const store = new PostgresJsonRecordStore(options);
20708
21829
  const adapter = {
@@ -20885,7 +22006,7 @@ async function createTask2(input, store, context) {
20885
22006
  const timestamp2 = new Date().toISOString();
20886
22007
  const shortId = input.project_id ? await nextTaskShortId2(input.project_id, store, context) : null;
20887
22008
  const task2 = {
20888
- id: randomUUID2(),
22009
+ id: randomUUID3(),
20889
22010
  short_id: shortId,
20890
22011
  project_id: input.project_id ?? context?.projectId ?? null,
20891
22012
  parent_id: input.parent_id ?? null,
@@ -21052,7 +22173,7 @@ async function getChangedSince(since, filters, store) {
21052
22173
  async function createProject2(input, store, context) {
21053
22174
  const timestamp2 = new Date().toISOString();
21054
22175
  const project = {
21055
- id: randomUUID2(),
22176
+ id: randomUUID3(),
21056
22177
  name: input.name,
21057
22178
  path: input.path,
21058
22179
  description: input.description ?? null,
@@ -21072,7 +22193,7 @@ async function updateProject2(id, input, store) {
21072
22193
  async function createPlan2(input, store, context) {
21073
22194
  const timestamp2 = new Date().toISOString();
21074
22195
  return store.upsert("plans", {
21075
- id: randomUUID2(),
22196
+ id: randomUUID3(),
21076
22197
  project_id: input.project_id ?? context?.projectId ?? null,
21077
22198
  task_list_id: input.task_list_id ?? context?.taskListId ?? null,
21078
22199
  agent_id: input.agent_id ?? context?.agentId ?? null,
@@ -21094,7 +22215,7 @@ async function registerAgent2(input, store, context) {
21094
22215
  }
21095
22216
  const timestamp2 = new Date().toISOString();
21096
22217
  const agent = {
21097
- id: existing?.id ?? randomUUID2().slice(0, 8),
22218
+ id: existing?.id ?? randomUUID3().slice(0, 8),
21098
22219
  name: input.name,
21099
22220
  description: input.description ?? existing?.description ?? null,
21100
22221
  role: input.role ?? existing?.role ?? null,
@@ -21130,7 +22251,7 @@ async function updateAgent2(id, input, store) {
21130
22251
  async function createTaskList2(input, store, context) {
21131
22252
  const timestamp2 = new Date().toISOString();
21132
22253
  return store.upsert("task_lists", {
21133
- id: randomUUID2(),
22254
+ id: randomUUID3(),
21134
22255
  project_id: input.project_id ?? context?.projectId ?? null,
21135
22256
  slug: input.slug ?? slugify2(input.name),
21136
22257
  name: input.name,
@@ -21152,7 +22273,7 @@ async function updateTaskList2(id, input, store) {
21152
22273
  async function createTemplate2(input, store, context) {
21153
22274
  const timestamp2 = new Date().toISOString();
21154
22275
  return store.upsert("templates", {
21155
- id: randomUUID2(),
22276
+ id: randomUUID3(),
21156
22277
  name: input.name,
21157
22278
  title_pattern: input.title_pattern,
21158
22279
  description: input.description ?? null,
@@ -21181,7 +22302,7 @@ async function updateTemplate2(id, input, store) {
21181
22302
  }
21182
22303
  async function logTaskChange2(taskId, action, field2, oldValue, newValue, agentId, store, context) {
21183
22304
  const entry2 = {
21184
- id: randomUUID2(),
22305
+ id: randomUUID3(),
21185
22306
  task_id: taskId,
21186
22307
  action,
21187
22308
  field: field2 ?? null,
@@ -21194,7 +22315,7 @@ async function logTaskChange2(taskId, action, field2, oldValue, newValue, agentI
21194
22315
  }
21195
22316
  async function addComment2(input, store, context) {
21196
22317
  const comment = {
21197
- id: randomUUID2(),
22318
+ id: randomUUID3(),
21198
22319
  task_id: input.task_id,
21199
22320
  agent_id: input.agent_id ?? context?.agentId ?? null,
21200
22321
  session_id: input.session_id ?? context?.sessionId ?? null,
@@ -21369,10 +22490,10 @@ function assertRemoteAdapterCapabilities(adapter, mode) {
21369
22490
  }
21370
22491
  }
21371
22492
  // src/storage/s3-artifacts.ts
21372
- import { createHash as createHash8, createHmac } from "crypto";
22493
+ import { createHash as createHash9, createHmac as createHmac2 } from "crypto";
21373
22494
  function createTodosS3ArtifactStore(options) {
21374
22495
  const requestFetch = options.fetch ?? fetch;
21375
- const now3 = options.now ?? (() => new Date);
22496
+ const now4 = options.now ?? (() => new Date);
21376
22497
  return {
21377
22498
  objectKey: (relativePath) => buildS3ObjectKey(options.config, relativePath),
21378
22499
  objectUrl: (relativePath) => buildS3ObjectUrl(options.config, buildS3ObjectKey(options.config, relativePath)),
@@ -21394,7 +22515,7 @@ function createTodosS3ArtifactStore(options) {
21394
22515
  headers,
21395
22516
  body,
21396
22517
  credentials: options.credentials,
21397
- now: now3()
22518
+ now: now4()
21398
22519
  });
21399
22520
  const response = await requestFetch(url, { method: "PUT", headers: signed.headers, body });
21400
22521
  if (!response.ok)
@@ -21415,7 +22536,7 @@ function createTodosS3ArtifactStore(options) {
21415
22536
  service: "s3",
21416
22537
  headers: {},
21417
22538
  credentials: options.credentials,
21418
- now: now3()
22539
+ now: now4()
21419
22540
  });
21420
22541
  const response = await requestFetch(url, { method: "GET", headers: signed.headers });
21421
22542
  if (!response.ok)
@@ -21431,7 +22552,7 @@ function createTodosS3ArtifactStore(options) {
21431
22552
  service: "s3",
21432
22553
  headers: {},
21433
22554
  credentials: options.credentials,
21434
- now: now3()
22555
+ now: now4()
21435
22556
  });
21436
22557
  const response = await requestFetch(url, { method: "DELETE", headers: signed.headers });
21437
22558
  if (!response.ok && response.status !== 404)
@@ -21541,13 +22662,13 @@ function toAmzDate(date) {
21541
22662
  return date.toISOString().replace(/[:-]|\.\d{3}/g, "");
21542
22663
  }
21543
22664
  function sha256Hex(value) {
21544
- return createHash8("sha256").update(value).digest("hex");
22665
+ return createHash9("sha256").update(value).digest("hex");
21545
22666
  }
21546
22667
  function hmac(key, value) {
21547
- return createHmac("sha256", key).update(value).digest();
22668
+ return createHmac2("sha256", key).update(value).digest();
21548
22669
  }
21549
22670
  function hmacHex(key, value) {
21550
- return createHmac("sha256", key).update(value).digest("hex");
22671
+ return createHmac2("sha256", key).update(value).digest("hex");
21551
22672
  }
21552
22673
  function getSigningKey(secretAccessKey, dateStamp, region, service) {
21553
22674
  const dateKey = hmac(`AWS4${secretAccessKey}`, dateStamp);
@@ -21559,7 +22680,7 @@ function getSigningKey(secretAccessKey, dateStamp, region, service) {
21559
22680
  init_database();
21560
22681
  async function uploadRunArtifactsToS3(options) {
21561
22682
  const db = options.db ?? getDatabase();
21562
- const now3 = options.now ?? (() => new Date);
22683
+ const now4 = options.now ?? (() => new Date);
21563
22684
  const result = emptyResult();
21564
22685
  for (const artifact of listRunArtifacts(db, options.filter)) {
21565
22686
  try {
@@ -21598,7 +22719,7 @@ async function uploadRunArtifactsToS3(options) {
21598
22719
  url: ref.url,
21599
22720
  sha256: content.sha256,
21600
22721
  size_bytes: content.size_bytes,
21601
- uploaded_at: now3().toISOString()
22722
+ uploaded_at: now4().toISOString()
21602
22723
  };
21603
22724
  updateArtifactMetadata(db, artifact.id, {
21604
22725
  ...metadata,
@@ -21673,7 +22794,7 @@ function planRunArtifactsS3Sync(options) {
21673
22794
  }
21674
22795
  async function downloadRunArtifactsFromS3(options) {
21675
22796
  const db = options.db ?? getDatabase();
21676
- const now3 = options.now ?? (() => new Date);
22797
+ const now4 = options.now ?? (() => new Date);
21677
22798
  const result = emptyResult();
21678
22799
  for (const artifact of listRunArtifacts(db, options.filter)) {
21679
22800
  try {
@@ -21709,7 +22830,7 @@ async function downloadRunArtifactsFromS3(options) {
21709
22830
  ...metadata,
21710
22831
  remote_artifact_store: {
21711
22832
  ...remote,
21712
- downloaded_at: now3().toISOString()
22833
+ downloaded_at: now4().toISOString()
21713
22834
  }
21714
22835
  });
21715
22836
  result.downloaded += 1;
@@ -22728,7 +23849,7 @@ function parseObject2(value) {
22728
23849
  function normalizeTags(tags) {
22729
23850
  return [...new Set((tags || []).map((tag) => tag.trim()).filter(Boolean))].sort((left, right) => left.localeCompare(right));
22730
23851
  }
22731
- function normalizeText2(value) {
23852
+ function normalizeText3(value) {
22732
23853
  if (value === undefined)
22733
23854
  return null;
22734
23855
  const trimmed = value.trim();
@@ -22783,9 +23904,9 @@ function createKnowledgeRecord(input, db) {
22783
23904
  id,
22784
23905
  input.record_type,
22785
23906
  title,
22786
- normalizeText2(input.content),
22787
- normalizeText2(input.decision),
22788
- normalizeText2(input.rationale),
23907
+ normalizeText3(input.content),
23908
+ normalizeText3(input.decision),
23909
+ normalizeText3(input.rationale),
22789
23910
  JSON.stringify(input.alternatives || []),
22790
23911
  resolveKnownId("tasks", input.task_id, d),
22791
23912
  resolveKnownId("projects", input.project_id, d),
@@ -22980,7 +24101,7 @@ function parseObject3(value) {
22980
24101
  function normalizeTags2(tags) {
22981
24102
  return [...new Set((tags || []).flatMap((tag) => tag.split(",")).map((tag) => tag.trim()).filter(Boolean))].sort((left, right) => left.localeCompare(right));
22982
24103
  }
22983
- function normalizeText3(value) {
24104
+ function normalizeText4(value) {
22984
24105
  if (value === null || value === undefined)
22985
24106
  return null;
22986
24107
  const trimmed = value.trim();
@@ -23049,13 +24170,13 @@ function createRisk(input, db) {
23049
24170
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
23050
24171
  id,
23051
24172
  title,
23052
- normalizeText3(input.description),
24173
+ normalizeText4(input.description),
23053
24174
  status,
23054
24175
  severity,
23055
24176
  probability,
23056
- normalizeText3(input.owner),
23057
- normalizeText3(input.mitigation),
23058
- normalizeText3(input.due_at),
24177
+ normalizeText4(input.owner),
24178
+ normalizeText4(input.mitigation),
24179
+ normalizeText4(input.due_at),
23059
24180
  resolveKnownId2("projects", input.project_id, d),
23060
24181
  resolveKnownId2("plans", input.plan_id, d),
23061
24182
  resolveKnownId2("tasks", input.task_id, d),
@@ -23090,7 +24211,7 @@ function updateRisk(id, input, db) {
23090
24211
  }
23091
24212
  if (input.description !== undefined) {
23092
24213
  sets.push("description = ?");
23093
- params.push(normalizeText3(input.description));
24214
+ params.push(normalizeText4(input.description));
23094
24215
  }
23095
24216
  if (input.status !== undefined) {
23096
24217
  assertStatus(input.status);
@@ -23111,15 +24232,15 @@ function updateRisk(id, input, db) {
23111
24232
  }
23112
24233
  if (input.owner !== undefined) {
23113
24234
  sets.push("owner = ?");
23114
- params.push(normalizeText3(input.owner));
24235
+ params.push(normalizeText4(input.owner));
23115
24236
  }
23116
24237
  if (input.mitigation !== undefined) {
23117
24238
  sets.push("mitigation = ?");
23118
- params.push(normalizeText3(input.mitigation));
24239
+ params.push(normalizeText4(input.mitigation));
23119
24240
  }
23120
24241
  if (input.due_at !== undefined) {
23121
24242
  sets.push("due_at = ?");
23122
- params.push(normalizeText3(input.due_at));
24243
+ params.push(normalizeText4(input.due_at));
23123
24244
  }
23124
24245
  if (input.project_id !== undefined) {
23125
24246
  sets.push("project_id = ?");
@@ -23640,7 +24761,7 @@ function renderRetrospectiveMarkdown(record) {
23640
24761
  }
23641
24762
  // src/lib/project-bootstrap.ts
23642
24763
  init_database();
23643
- import { existsSync as existsSync8, readFileSync as readFileSync7, statSync as statSync3 } from "fs";
24764
+ import { existsSync as existsSync9, readFileSync as readFileSync7, statSync as statSync3 } from "fs";
23644
24765
  import { basename as basename2, dirname as dirname7, resolve as resolve9 } from "path";
23645
24766
  function safeStat(path) {
23646
24767
  try {
@@ -23659,7 +24780,7 @@ function canonicalPath(input) {
23659
24780
  function findUp(start, marker) {
23660
24781
  let current = canonicalPath(start);
23661
24782
  while (true) {
23662
- if (existsSync8(resolve9(current, marker)))
24783
+ if (existsSync9(resolve9(current, marker)))
23663
24784
  return current;
23664
24785
  const parent = dirname7(current);
23665
24786
  if (parent === current)
@@ -23671,7 +24792,7 @@ function readPackageJson2(path) {
23671
24792
  if (!path)
23672
24793
  return null;
23673
24794
  const file = resolve9(path, "package.json");
23674
- if (!existsSync8(file))
24795
+ if (!existsSync9(file))
23675
24796
  return null;
23676
24797
  try {
23677
24798
  const parsed = JSON.parse(readFileSync7(file, "utf-8"));
@@ -23693,7 +24814,7 @@ function workspaceMarker(root, rootPackage) {
23693
24814
  if (rootPackage?.workspaces)
23694
24815
  markers.push("package.json#workspaces");
23695
24816
  for (const marker of ["pnpm-workspace.yaml", "turbo.json", "nx.json", "lerna.json", "rush.json", "bun.lock", "bun.lockb"]) {
23696
- if (existsSync8(resolve9(root, marker)))
24817
+ if (existsSync9(resolve9(root, marker)))
23697
24818
  markers.push(marker);
23698
24819
  }
23699
24820
  const kind = markers.find((marker) => marker !== "bun.lock" && marker !== "bun.lockb") ?? null;
@@ -23801,7 +24922,7 @@ function getProjectByPathForBootstrap(path, db) {
23801
24922
  }
23802
24923
  // src/db/api-keys.ts
23803
24924
  init_database();
23804
- import { createHash as createHash9, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "crypto";
24925
+ import { createHash as createHash10, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual3 } from "crypto";
23805
24926
  function rowToRecord(row) {
23806
24927
  return {
23807
24928
  id: row.id,
@@ -23815,12 +24936,12 @@ function rowToRecord(row) {
23815
24936
  };
23816
24937
  }
23817
24938
  function hashApiKey(key) {
23818
- return createHash9("sha256").update(key).digest("hex");
24939
+ return createHash10("sha256").update(key).digest("hex");
23819
24940
  }
23820
24941
  function safeEqualHex(a, b) {
23821
24942
  if (a.length !== b.length)
23822
24943
  return false;
23823
- return timingSafeEqual2(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
24944
+ return timingSafeEqual3(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
23824
24945
  }
23825
24946
  function generatePlaintextKey() {
23826
24947
  return `tdos_${randomBytes2(32).toString("base64url")}`;
@@ -24006,18 +25127,18 @@ var gatherTrainingData = async (options = {}) => {
24006
25127
  };
24007
25128
  // src/lib/model-config.ts
24008
25129
  init_sync_utils();
24009
- import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
24010
- import { join as join9 } from "path";
25130
+ import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
25131
+ import { join as join10 } from "path";
24011
25132
  var DEFAULT_MODEL = "gpt-4o-mini";
24012
25133
  function getConfigDir() {
24013
25134
  return getTodosGlobalDir();
24014
25135
  }
24015
25136
  function getConfigPath2() {
24016
- return join9(getConfigDir(), "config.json");
25137
+ return join10(getConfigDir(), "config.json");
24017
25138
  }
24018
25139
  function readConfig() {
24019
25140
  const configPath = getConfigPath2();
24020
- if (!existsSync9(configPath))
25141
+ if (!existsSync10(configPath))
24021
25142
  return {};
24022
25143
  try {
24023
25144
  const raw = readFileSync8(configPath, "utf-8");
@@ -24028,7 +25149,7 @@ function readConfig() {
24028
25149
  }
24029
25150
  function writeConfig(config) {
24030
25151
  const configDir = getConfigDir();
24031
- if (!existsSync9(configDir)) {
25152
+ if (!existsSync10(configDir)) {
24032
25153
  mkdirSync8(configDir, { recursive: true });
24033
25154
  }
24034
25155
  writeFileSync6(getConfigPath2(), JSON.stringify(config, null, 2) + `
@@ -24753,7 +25874,7 @@ CLI equivalent: \`${r.equivalent_cli}\`
24753
25874
  `);
24754
25875
  }
24755
25876
  // src/lib/verification-providers.ts
24756
- import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
25877
+ import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
24757
25878
  init_database();
24758
25879
  init_config();
24759
25880
  init_redaction();
@@ -24919,7 +26040,7 @@ Timed out after ${provider.timeout_ms}ms`);
24919
26040
  };
24920
26041
  }
24921
26042
  function runCiLogProvider(input) {
24922
- const text = input.log_text ?? (input.log_path && existsSync10(input.log_path) ? readFileSync9(input.log_path, "utf-8") : "");
26043
+ const text = input.log_text ?? (input.log_path && existsSync11(input.log_path) ? readFileSync9(input.log_path, "utf-8") : "");
24923
26044
  return {
24924
26045
  status: classifyLog(text),
24925
26046
  attempts: 1,
@@ -24931,7 +26052,7 @@ function runBrowserProvider(input) {
24931
26052
  if (!input.artifact_path) {
24932
26053
  return { status: "unknown", attempts: 1, exit_code: null, output_summary: "browser provider needs a screenshot or artifact path" };
24933
26054
  }
24934
- if (!existsSync10(input.artifact_path)) {
26055
+ if (!existsSync11(input.artifact_path)) {
24935
26056
  return { status: "failed", attempts: 1, exit_code: null, output_summary: `artifact not found: ${input.artifact_path}` };
24936
26057
  }
24937
26058
  return {
@@ -25529,11 +26650,11 @@ function buildResourceSnapshot(uri, staleMs = DEFAULT_STALE_MS) {
25529
26650
  content_hash: hashContent(content)
25530
26651
  };
25531
26652
  }
25532
- function isSnapshotStale(snapshot, now3 = new Date) {
26653
+ function isSnapshotStale(snapshot, now4 = new Date) {
25533
26654
  const staleAt = Date.parse(snapshot.stale_after);
25534
26655
  if (!Number.isFinite(staleAt))
25535
26656
  return true;
25536
- return now3.getTime() >= staleAt;
26657
+ return now4.getTime() >= staleAt;
25537
26658
  }
25538
26659
  function subscribeResource(uri, agentId) {
25539
26660
  const key = `${uri}:${agentId || "*"}`;
@@ -25570,21 +26691,21 @@ function resourceDiagnostics() {
25570
26691
  };
25571
26692
  }
25572
26693
  // src/lib/sandbox-profiles.ts
25573
- import { existsSync as existsSync11, readFileSync as readFileSync10, writeFileSync as writeFileSync8, mkdirSync as mkdirSync10 } from "fs";
25574
- import { join as join10, dirname as dirname9 } from "path";
26694
+ import { existsSync as existsSync12, readFileSync as readFileSync10, writeFileSync as writeFileSync8, mkdirSync as mkdirSync10 } from "fs";
26695
+ import { join as join11, dirname as dirname9 } from "path";
25575
26696
  var SANDBOX_PROFILE_VERSION = "todos.sandbox-profile.v1";
25576
26697
  function getProfilesPath() {
25577
26698
  if (process.env["TODOS_SANDBOX_PROFILES_PATH"]) {
25578
26699
  return process.env["TODOS_SANDBOX_PROFILES_PATH"];
25579
26700
  }
25580
- const localDir = join10(process.cwd(), ".todos");
25581
- const local = join10(localDir, "sandbox-profiles.json");
25582
- if (existsSync11(localDir))
26701
+ const localDir = join11(process.cwd(), ".todos");
26702
+ const local = join11(localDir, "sandbox-profiles.json");
26703
+ if (existsSync12(localDir))
25583
26704
  return local;
25584
- if (existsSync11(local))
26705
+ if (existsSync12(local))
25585
26706
  return local;
25586
26707
  const home = process.env["HOME"] || "~";
25587
- return join10(home, ".hasna", "todos", "sandbox-profiles.json");
26708
+ return join11(home, ".hasna", "todos", "sandbox-profiles.json");
25588
26709
  }
25589
26710
  var cached2 = null;
25590
26711
  function resetSandboxProfileCache() {
@@ -25616,7 +26737,7 @@ function loadSandboxProfiles() {
25616
26737
  if (cached2)
25617
26738
  return cached2;
25618
26739
  const path = getProfilesPath();
25619
- if (!existsSync11(path)) {
26740
+ if (!existsSync12(path)) {
25620
26741
  cached2 = getDefaultSandboxProfiles();
25621
26742
  return cached2;
25622
26743
  }
@@ -25989,7 +27110,7 @@ function getDefaultAgentAdapters() {
25989
27110
  }
25990
27111
  function resetAgentAdapterCache() {}
25991
27112
  // src/lib/git-traceability.ts
25992
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
27113
+ import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
25993
27114
  import { spawnSync as spawnSync2 } from "child_process";
25994
27115
  import { resolve as resolve11 } from "path";
25995
27116
  var GIT_TRACEABILITY_SCHEMA_VERSION = "todos.git_traceability.v1";
@@ -26035,7 +27156,7 @@ function inspectGitCommit(sha, cwd) {
26035
27156
  }
26036
27157
  function loadCiSnapshot(path) {
26037
27158
  const target = path ? resolve11(path) : resolve11(process.cwd(), ".todos", "ci-snapshot.json");
26038
- if (!existsSync12(target))
27159
+ if (!existsSync13(target))
26039
27160
  return null;
26040
27161
  try {
26041
27162
  const parsed = JSON.parse(readFileSync11(target, "utf8"));
@@ -26131,8 +27252,8 @@ function formatTraceabilityReport(report) {
26131
27252
  `);
26132
27253
  }
26133
27254
  // src/lib/mention-resolver.ts
26134
- import { existsSync as existsSync13, readdirSync as readdirSync2, readFileSync as readFileSync12, statSync as statSync4 } from "fs";
26135
- import { basename as basename3, isAbsolute, join as join11, relative as relative4, resolve as resolve12, sep } from "path";
27255
+ import { existsSync as existsSync14, readdirSync as readdirSync2, readFileSync as readFileSync12, statSync as statSync4 } from "fs";
27256
+ import { basename as basename3, isAbsolute, join as join12, relative as relative4, resolve as resolve12, sep } from "path";
26136
27257
  init_database();
26137
27258
  var PREFIXES = {
26138
27259
  file: "file",
@@ -26283,7 +27404,7 @@ function resolveFile(parsed, workspace) {
26283
27404
  return resolution;
26284
27405
  }
26285
27406
  resolution.path = relPath;
26286
- if (!existsSync13(absolutePath)) {
27407
+ if (!existsSync14(absolutePath)) {
26287
27408
  resolution.warnings.push("file does not exist in the local workspace");
26288
27409
  return resolution;
26289
27410
  }
@@ -26316,7 +27437,7 @@ function walkSourceFiles(root, current = root, files = []) {
26316
27437
  if (SKIP_DIRS.has(entry2.name))
26317
27438
  continue;
26318
27439
  }
26319
- const absolutePath = join11(current, entry2.name);
27440
+ const absolutePath = join12(current, entry2.name);
26320
27441
  if (entry2.isDirectory()) {
26321
27442
  if (!SKIP_DIRS.has(entry2.name))
26322
27443
  walkSourceFiles(root, absolutePath, files);
@@ -28189,9 +29310,9 @@ function getAdapterDocsFingerprint() {
28189
29310
  }
28190
29311
  // src/lib/inbox-intake.ts
28191
29312
  init_database();
28192
- import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
29313
+ import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
28193
29314
  import { basename as basename4 } from "path";
28194
- import { createHash as createHash10 } from "crypto";
29315
+ import { createHash as createHash11 } from "crypto";
28195
29316
  init_secret_redaction();
28196
29317
  var INBOX_INTAKE_SCHEMA = "todos.inbox_intake.v1";
28197
29318
  var INTAKE_SOURCE_TYPES = [
@@ -28204,7 +29325,7 @@ var INTAKE_SOURCE_TYPES = [
28204
29325
  ];
28205
29326
  var INTAKE_TRIAGE_STATUSES = ["preview", "triaged", "duplicate", "created"];
28206
29327
  function fingerprint2(text) {
28207
- return createHash10("sha256").update(text).digest("hex").slice(0, 16);
29328
+ return createHash11("sha256").update(text).digest("hex").slice(0, 16);
28208
29329
  }
28209
29330
  function loadRawContent(input) {
28210
29331
  if (input.github_url) {
@@ -28235,7 +29356,7 @@ function loadRawContent(input) {
28235
29356
  }
28236
29357
  }
28237
29358
  if (input.file_path) {
28238
- if (!existsSync14(input.file_path))
29359
+ if (!existsSync15(input.file_path))
28239
29360
  throw new Error(`File not found: ${input.file_path}`);
28240
29361
  const raw = readFileSync13(input.file_path, "utf8");
28241
29362
  const name = basename4(input.file_path).toLowerCase();
@@ -28853,7 +29974,7 @@ function formatNlIntakePreviewText(preview) {
28853
29974
  }
28854
29975
  // src/lib/issue-importers.ts
28855
29976
  init_database();
28856
- import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
29977
+ import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
28857
29978
  var ISSUE_IMPORT_SCHEMA = "todos.issue_import.v1";
28858
29979
  var ISSUE_SOURCES = ["github", "linear", "jira", "auto"];
28859
29980
  var GITHUB_LABEL_PRIORITY = {
@@ -29072,7 +30193,7 @@ function parseIssueExport(data, source9 = "auto") {
29072
30193
  return normalized;
29073
30194
  }
29074
30195
  function loadIssueExportFromFile(path) {
29075
- if (!existsSync15(path))
30196
+ if (!existsSync16(path))
29076
30197
  throw new Error(`File not found: ${path}`);
29077
30198
  return JSON.parse(readFileSync14(path, "utf8"));
29078
30199
  }
@@ -29232,8 +30353,8 @@ todos import issues ./linear.json --source linear --dry-run
29232
30353
  // src/lib/run-records.ts
29233
30354
  init_database();
29234
30355
  init_secret_redaction();
29235
- import { existsSync as existsSync16, mkdirSync as mkdirSync11, writeFileSync as writeFileSync9 } from "fs";
29236
- import { join as join12, dirname as dirname10 } from "path";
30356
+ import { existsSync as existsSync17, mkdirSync as mkdirSync11, writeFileSync as writeFileSync9 } from "fs";
30357
+ import { join as join13, dirname as dirname10 } from "path";
29237
30358
  var RUN_RECORD_SCHEMA = "todos.run_record.v1";
29238
30359
  var RUN_RECORD_STATUSES = ["active", "completed", "failed", "archived"];
29239
30360
  function parseJsonArray3(raw, fallback = []) {
@@ -29426,7 +30547,7 @@ function buildRunReplayBundle(id, db) {
29426
30547
  }
29427
30548
  function exportRunReplay(id, outputPath, db) {
29428
30549
  const bundle = buildRunReplayBundle(id, db);
29429
- const path = outputPath ?? join12(process.cwd(), ".todos", "replays", `${id.slice(0, 8)}.json`);
30550
+ const path = outputPath ?? join13(process.cwd(), ".todos", "replays", `${id.slice(0, 8)}.json`);
29430
30551
  mkdirSync11(dirname10(path), { recursive: true });
29431
30552
  writeFileSync9(path, JSON.stringify(bundle, null, 2));
29432
30553
  const d = db || getDatabase();
@@ -29466,16 +30587,16 @@ function formatRunRecordMarkdown(record) {
29466
30587
  `;
29467
30588
  }
29468
30589
  function getDefaultReplayDir() {
29469
- const local = join12(process.cwd(), ".todos", "replays");
29470
- if (existsSync16(join12(process.cwd(), ".todos")))
30590
+ const local = join13(process.cwd(), ".todos", "replays");
30591
+ if (existsSync17(join13(process.cwd(), ".todos")))
29471
30592
  return local;
29472
30593
  const home = process.env["HOME"] || "~";
29473
- return join12(home, ".hasna", "todos", "replays");
30594
+ return join13(home, ".hasna", "todos", "replays");
29474
30595
  }
29475
30596
  // src/lib/release-checks.ts
29476
30597
  init_secret_redaction();
29477
- import { existsSync as existsSync17, readFileSync as readFileSync15, readdirSync as readdirSync3, statSync as statSync5 } from "fs";
29478
- import { join as join13, relative as relative5 } from "path";
30598
+ import { existsSync as existsSync18, readFileSync as readFileSync15, readdirSync as readdirSync3, statSync as statSync5 } from "fs";
30599
+ import { join as join14, relative as relative5 } from "path";
29479
30600
  var RELEASE_CHECK_SCHEMA = "todos.release_check.v1";
29480
30601
  var FORBIDDEN_DIST_PATTERNS = [
29481
30602
  {
@@ -29488,16 +30609,16 @@ var FORBIDDEN_DIST_PATTERNS = [
29488
30609
  ];
29489
30610
  var REQUIRED_BINS = ["todos", "todos-mcp", "todos-serve"];
29490
30611
  function readPackageJson3(root) {
29491
- const path = join13(root, "package.json");
29492
- if (!existsSync17(path))
30612
+ const path = join14(root, "package.json");
30613
+ if (!existsSync18(path))
29493
30614
  throw new Error(`package.json not found in ${root}`);
29494
30615
  return JSON.parse(readFileSync15(path, "utf8"));
29495
30616
  }
29496
30617
  function walkFiles(dir, acc = []) {
29497
- if (!existsSync17(dir))
30618
+ if (!existsSync18(dir))
29498
30619
  return acc;
29499
30620
  for (const entry2 of readdirSync3(dir)) {
29500
- const full = join13(dir, entry2);
30621
+ const full = join14(dir, entry2);
29501
30622
  const st = statSync5(full);
29502
30623
  if (st.isDirectory())
29503
30624
  walkFiles(full, acc);
@@ -29515,8 +30636,8 @@ function auditPackageContents(root) {
29515
30636
  checks.push({ id: "files_dist", severity: "error", message: "package.json files must include dist" });
29516
30637
  }
29517
30638
  for (const pattern of files) {
29518
- const target = join13(root, pattern);
29519
- if (!existsSync17(target)) {
30639
+ const target = join14(root, pattern);
30640
+ if (!existsSync18(target)) {
29520
30641
  checks.push({ id: `files_missing_${pattern}`, severity: "error", message: `Published file path missing: ${pattern}` });
29521
30642
  }
29522
30643
  }
@@ -29530,8 +30651,8 @@ function auditPackageContents(root) {
29530
30651
  checks.push({ id: `bin_${name}`, severity: "error", message: `Missing bin entry: ${name}` });
29531
30652
  continue;
29532
30653
  }
29533
- const binPath = join13(root, rel);
29534
- if (!existsSync17(binPath)) {
30654
+ const binPath = join14(root, rel);
30655
+ if (!existsSync18(binPath)) {
29535
30656
  checks.push({ id: `bin_path_${name}`, severity: "error", message: `Bin file missing: ${rel}` });
29536
30657
  } else {
29537
30658
  checks.push({ id: `bin_ok_${name}`, severity: "info", message: `Bin present: ${name} \u2192 ${rel}` });
@@ -29548,8 +30669,8 @@ function auditPackageContents(root) {
29548
30669
  }
29549
30670
  function scanDistArtifacts(root) {
29550
30671
  const checks = [];
29551
- const distDir = join13(root, "dist");
29552
- if (!existsSync17(distDir)) {
30672
+ const distDir = join14(root, "dist");
30673
+ if (!existsSync18(distDir)) {
29553
30674
  checks.push({ id: "dist_missing", severity: "error", message: "dist/ directory not found \u2014 run bun run build" });
29554
30675
  return checks;
29555
30676
  }
@@ -29855,8 +30976,8 @@ function renderReleaseNotesMarkdown(document) {
29855
30976
  // src/lib/db-backup.ts
29856
30977
  init_database();
29857
30978
  init_migrations();
29858
- import { existsSync as existsSync18, copyFileSync, mkdirSync as mkdirSync12, readFileSync as readFileSync16, statSync as statSync6, writeFileSync as writeFileSync10, unlinkSync } from "fs";
29859
- import { dirname as dirname11, join as join14, resolve as resolve13 } from "path";
30979
+ import { existsSync as existsSync19, copyFileSync, mkdirSync as mkdirSync12, readFileSync as readFileSync16, statSync as statSync6, writeFileSync as writeFileSync10, unlinkSync } from "fs";
30980
+ import { dirname as dirname11, join as join15, resolve as resolve13 } from "path";
29860
30981
  import { Database as Database3 } from "bun:sqlite";
29861
30982
  var DB_BACKUP_SCHEMA = "todos.db_backup.v1";
29862
30983
  function resolveDbPath(dbPath) {
@@ -29873,7 +30994,7 @@ function resolveDbPath(dbPath) {
29873
30994
  }
29874
30995
  function backupDatabase(outputPath, sourcePath) {
29875
30996
  const source9 = resolveDbPath(sourcePath);
29876
- if (!existsSync18(source9))
30997
+ if (!existsSync19(source9))
29877
30998
  throw new Error(`Database not found: ${source9}`);
29878
30999
  mkdirSync12(dirname11(outputPath), { recursive: true });
29879
31000
  closeDatabase();
@@ -29896,7 +31017,7 @@ function backupDatabase(outputPath, sourcePath) {
29896
31017
  };
29897
31018
  }
29898
31019
  function restoreDatabase(backupPath, targetPath) {
29899
- if (!existsSync18(backupPath))
31020
+ if (!existsSync19(backupPath))
29900
31021
  throw new Error(`Backup not found: ${backupPath}`);
29901
31022
  const integrity = checkDatabaseIntegrity(backupPath);
29902
31023
  if (!integrity.ok) {
@@ -29923,7 +31044,7 @@ function restoreDatabase(backupPath, targetPath) {
29923
31044
  function checkDatabaseIntegrity(dbPath) {
29924
31045
  const path = dbPath ? resolve13(dbPath) : resolveDbPath();
29925
31046
  const errors = [];
29926
- if (!existsSync18(path)) {
31047
+ if (!existsSync19(path)) {
29927
31048
  return {
29928
31049
  schema_version: DB_BACKUP_SCHEMA,
29929
31050
  path,
@@ -30022,11 +31143,11 @@ function migrationDryRun(dbPath) {
30022
31143
  function defaultBackupPath(dbPath) {
30023
31144
  const base = dbPath ? dirname11(resolve13(dbPath)) : dirname11(resolveDbPath());
30024
31145
  const stamp = new Date().toISOString().replace(/[:.]/g, "-");
30025
- return join14(base, "backups", `todos-${stamp}.db`);
31146
+ return join15(base, "backups", `todos-${stamp}.db`);
30026
31147
  }
30027
31148
  function readBackupManifest(backupPath) {
30028
31149
  const manifestPath = `${backupPath}.json`;
30029
- if (!existsSync18(manifestPath))
31150
+ if (!existsSync19(manifestPath))
30030
31151
  return null;
30031
31152
  try {
30032
31153
  return JSON.parse(readFileSync16(manifestPath, "utf8"));
@@ -30054,7 +31175,10 @@ var SCHEMA_ENTITIES = [
30054
31175
  "verification_evidence",
30055
31176
  "handoff",
30056
31177
  "import_export_bundle",
30057
- "mcp_response"
31178
+ "mcp_response",
31179
+ "tester_issue_report",
31180
+ "tester_issue_report_result",
31181
+ "tester_issue_report_batch_result"
30058
31182
  ];
30059
31183
  var ISO_DATE = { type: "string", description: "ISO-8601 timestamp" };
30060
31184
  function def(entity, schemaVersion, title, required, properties) {
@@ -30184,6 +31308,81 @@ var JSON_SCHEMAS = {
30184
31308
  }
30185
31309
  },
30186
31310
  is_error: { type: "boolean" }
31311
+ }),
31312
+ tester_issue_report: def("tester_issue_report", "testers.issue_report.v1", "TesterIssueReport", ["schema_version", "title"], {
31313
+ schema_version: { type: "string", enum: ["testers.issue_report.v1"] },
31314
+ id: { type: "string" },
31315
+ fingerprint: { type: "string" },
31316
+ title: { type: "string" },
31317
+ summary: { type: ["string", "null"] },
31318
+ kind: { type: "string" },
31319
+ severity: { type: "string", enum: ["low", "medium", "high", "critical"] },
31320
+ source: {
31321
+ type: "object",
31322
+ additionalProperties: true,
31323
+ properties: {
31324
+ tool: { type: "string" },
31325
+ run_id: { type: "string" },
31326
+ result_id: { type: "string" },
31327
+ scenario_id: { type: "string" },
31328
+ scenario_name: { type: "string" },
31329
+ project_id: { type: "string" },
31330
+ url: { type: "string" },
31331
+ page_url: { type: "string" },
31332
+ artifact_url: { type: "string" },
31333
+ screenshot_url: { type: "string" },
31334
+ commit: { type: "string" },
31335
+ branch: { type: "string" }
31336
+ }
31337
+ },
31338
+ target: {
31339
+ type: "object",
31340
+ additionalProperties: true,
31341
+ properties: {
31342
+ url: { type: "string" },
31343
+ route: { type: "string" },
31344
+ selector: { type: "string" },
31345
+ component: { type: "string" },
31346
+ browser: { type: "string" },
31347
+ viewport: { type: "string" }
31348
+ }
31349
+ },
31350
+ failure: {
31351
+ type: "object",
31352
+ additionalProperties: true,
31353
+ properties: {
31354
+ message: { type: "string" },
31355
+ expected: { type: "string" },
31356
+ actual: { type: "string" },
31357
+ stack: { type: "string" },
31358
+ reasoning: { type: "string" },
31359
+ steps: { type: "array", items: { type: "string" } }
31360
+ }
31361
+ },
31362
+ evidence: { type: "object", additionalProperties: true },
31363
+ labels: { type: "array", items: { type: "string" } },
31364
+ metadata: { type: "object", additionalProperties: true },
31365
+ occurred_at: ISO_DATE
31366
+ }),
31367
+ tester_issue_report_result: def("tester_issue_report_result", "todos.tester_issue_report_result.v1", "TesterIssueReportResult", ["schema_version", "local_only", "dry_run", "processed_at", "action", "fingerprint", "report", "task", "warnings", "commands"], {
31368
+ schema_version: { type: "string", enum: ["todos.tester_issue_report_result.v1"] },
31369
+ local_only: { type: "boolean" },
31370
+ dry_run: { type: "boolean" },
31371
+ processed_at: ISO_DATE,
31372
+ action: { type: "string", enum: ["preview", "matched", "created", "updated", "regressed"] },
31373
+ fingerprint: { type: "string" },
31374
+ report: { type: "object", additionalProperties: true },
31375
+ task: { type: ["object", "null"], additionalProperties: true },
31376
+ warnings: { type: "array", items: { type: "string" } },
31377
+ commands: { type: "array", items: { type: "string" } }
31378
+ }),
31379
+ tester_issue_report_batch_result: def("tester_issue_report_batch_result", "todos.tester_issue_report_batch_result.v1", "TesterIssueReportBatchResult", ["schema_version", "local_only", "dry_run", "processed_at", "results", "summary"], {
31380
+ schema_version: { type: "string", enum: ["todos.tester_issue_report_batch_result.v1"] },
31381
+ local_only: { type: "boolean" },
31382
+ dry_run: { type: "boolean" },
31383
+ processed_at: ISO_DATE,
31384
+ results: { type: "array", items: { type: "object", additionalProperties: true } },
31385
+ summary: { type: "object", additionalProperties: true }
30187
31386
  })
30188
31387
  };
30189
31388
  var SCHEMA_CONTRACT_FIXTURES = {
@@ -30264,6 +31463,47 @@ var SCHEMA_CONTRACT_FIXTURES = {
30264
31463
  mcp_response: {
30265
31464
  schema_version: "todos.mcp_response.v1",
30266
31465
  content: [{ type: "text", text: "ok" }]
31466
+ },
31467
+ tester_issue_report: {
31468
+ schema_version: "testers.issue_report.v1",
31469
+ title: "Checkout button fails",
31470
+ kind: "assertion_failure",
31471
+ severity: "high",
31472
+ fingerprint: "checkout-button-1234",
31473
+ source: {
31474
+ tool: "testers",
31475
+ run_id: "run-001",
31476
+ scenario_id: "scenario-001",
31477
+ url: "https://preview.example.com/checkout"
31478
+ },
31479
+ failure: {
31480
+ message: "Expected checkout button to become enabled",
31481
+ steps: ["Open checkout", "Fill required fields"]
31482
+ },
31483
+ labels: ["checkout", "regression"]
31484
+ },
31485
+ tester_issue_report_result: {
31486
+ schema_version: "todos.tester_issue_report_result.v1",
31487
+ local_only: true,
31488
+ dry_run: false,
31489
+ processed_at: "2026-01-01T00:00:00.000Z",
31490
+ action: "created",
31491
+ fingerprint: "testers:checkout-button-1234",
31492
+ report: {
31493
+ schema_version: "testers.issue_report.v1",
31494
+ title: "Checkout button fails"
31495
+ },
31496
+ task: null,
31497
+ warnings: [],
31498
+ commands: []
31499
+ },
31500
+ tester_issue_report_batch_result: {
31501
+ schema_version: "todos.tester_issue_report_batch_result.v1",
31502
+ local_only: true,
31503
+ dry_run: false,
31504
+ processed_at: "2026-01-01T00:00:00.000Z",
31505
+ results: [],
31506
+ summary: { total: 0, preview: 0, matched: 0, created: 0, updated: 0, regressed: 0 }
30267
31507
  }
30268
31508
  };
30269
31509
  function typeMatches(value, expected) {
@@ -30427,15 +31667,15 @@ ${SCHEMA_ENTITIES.map((e) => `- **${e}**: \`${JSON_SCHEMAS[e].schema_version}\``
30427
31667
  }
30428
31668
  function exportSchemasToDirectory(dir) {
30429
31669
  const { mkdirSync: mkdirSync13, writeFileSync: writeFileSync11 } = __require("fs");
30430
- const { join: join15 } = __require("path");
31670
+ const { join: join16 } = __require("path");
30431
31671
  mkdirSync13(dir, { recursive: true });
30432
31672
  const written = [];
30433
31673
  for (const entity of SCHEMA_ENTITIES) {
30434
- const path = join15(dir, `${entity}.${JSON_SCHEMAS[entity].schema_version.replace(/\./g, "-")}.json`);
31674
+ const path = join16(dir, `${entity}.${JSON_SCHEMAS[entity].schema_version.replace(/\./g, "-")}.json`);
30435
31675
  writeFileSync11(path, JSON.stringify(JSON_SCHEMAS[entity], null, 2));
30436
31676
  written.push(path);
30437
31677
  }
30438
- const catalogPath = join15(dir, "catalog.json");
31678
+ const catalogPath = join16(dir, "catalog.json");
30439
31679
  writeFileSync11(catalogPath, JSON.stringify({
30440
31680
  catalog_version: JSON_SCHEMA_CATALOG_VERSION,
30441
31681
  semver: SCHEMA_SEMVER,
@@ -30641,6 +31881,9 @@ function hasFts(db) {
30641
31881
  function escapeFtsQuery(q) {
30642
31882
  return q.replace(/["*^()]/g, " ").trim().split(/\s+/).filter(Boolean).map((token) => `"${token}"*`).join(" ");
30643
31883
  }
31884
+ function shouldUseFts(q) {
31885
+ return /^[\p{L}\p{N}_\-\s]+$/u.test(q);
31886
+ }
30644
31887
  function searchTasks(options, projectId, taskListId, db) {
30645
31888
  const opts = typeof options === "string" ? { query: options || undefined, project_id: projectId, task_list_id: taskListId } : options;
30646
31889
  const d = db || getDatabase();
@@ -30649,7 +31892,8 @@ function searchTasks(options, projectId, taskListId, db) {
30649
31892
  let sql;
30650
31893
  const raw = opts.query?.trim() ?? "";
30651
31894
  const q = raw === "*" ? "" : raw;
30652
- if (hasFts(d) && q) {
31895
+ const useFts = hasFts(d) && q && shouldUseFts(q);
31896
+ if (useFts) {
30653
31897
  const ftsQuery = escapeFtsQuery(q);
30654
31898
  sql = `SELECT t.* FROM tasks t
30655
31899
  INNER JOIN tasks_fts fts ON fts.rowid = t.rowid
@@ -30657,8 +31901,16 @@ function searchTasks(options, projectId, taskListId, db) {
30657
31901
  params.push(ftsQuery);
30658
31902
  } else if (q) {
30659
31903
  const pattern = `%${q}%`;
30660
- 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 ?))`;
30661
- params.push(pattern, pattern, pattern);
31904
+ sql = `SELECT * FROM tasks t WHERE (
31905
+ t.id LIKE ?
31906
+ OR t.short_id LIKE ?
31907
+ OR t.title LIKE ?
31908
+ OR t.description LIKE ?
31909
+ OR t.working_dir LIKE ?
31910
+ OR t.metadata LIKE ?
31911
+ OR EXISTS (SELECT 1 FROM task_tags WHERE task_tags.task_id = t.id AND tag LIKE ?)
31912
+ )`;
31913
+ params.push(pattern, pattern, pattern, pattern, pattern, pattern, pattern);
30662
31914
  } else {
30663
31915
  sql = `SELECT * FROM tasks t WHERE 1=1`;
30664
31916
  }
@@ -30714,7 +31966,7 @@ function searchTasks(options, projectId, taskListId, db) {
30714
31966
  } else if (opts.is_blocked === false) {
30715
31967
  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')";
30716
31968
  }
30717
- if (hasFts(d) && q) {
31969
+ if (useFts) {
30718
31970
  sql += ` ORDER BY bm25(tasks_fts),
30719
31971
  CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
30720
31972
  t.created_at DESC`;
@@ -32992,7 +34244,7 @@ function generateCliReferenceMarkdown() {
32992
34244
  // src/db/builtin-templates.ts
32993
34245
  init_database();
32994
34246
  import { mkdirSync as mkdirSync15, writeFileSync as writeFileSync13 } from "fs";
32995
- import { join as join15 } from "path";
34247
+ import { join as join16 } from "path";
32996
34248
  var BUILTIN_TEMPLATE_LIBRARY_VERSION = "2026-05-21";
32997
34249
  var BUILTIN_TEMPLATE_LIBRARY_SOURCE = "bundled-local-template-library";
32998
34250
  var TEMPLATE_LIBRARY_SCHEMA = "todos.template_library.v1";
@@ -33268,7 +34520,7 @@ function writeBuiltinTemplateFiles(directory) {
33268
34520
  mkdirSync15(directory, { recursive: true });
33269
34521
  const files = [];
33270
34522
  for (const entry2 of exportBuiltinTemplateFiles()) {
33271
- const path = join15(directory, entry2.filename);
34523
+ const path = join16(directory, entry2.filename);
33272
34524
  writeFileSync13(path, `${JSON.stringify(entry2.template, null, 2)}
33273
34525
  `, "utf-8");
33274
34526
  files.push(path);
@@ -33550,10 +34802,10 @@ todos machines topology # full diagnostic report
33550
34802
  `;
33551
34803
  }
33552
34804
  // src/lib/environment-snapshots.ts
33553
- import { createHash as createHash11 } from "crypto";
33554
- import { existsSync as existsSync19, readFileSync as readFileSync19, statSync as statSync7 } from "fs";
34805
+ import { createHash as createHash12 } from "crypto";
34806
+ import { existsSync as existsSync20, readFileSync as readFileSync19, statSync as statSync7 } from "fs";
33555
34807
  import { hostname as hostname3, platform, arch } from "os";
33556
- import { dirname as dirname15, join as join16, resolve as resolve14 } from "path";
34808
+ import { dirname as dirname15, join as join17, resolve as resolve14 } from "path";
33557
34809
  import { tmpdir as tmpdir2 } from "os";
33558
34810
  init_database();
33559
34811
  init_redaction();
@@ -33575,11 +34827,11 @@ var CONFIG_FILES = [
33575
34827
  "dashboard/vite.config.ts"
33576
34828
  ];
33577
34829
  function sha2565(value) {
33578
- return createHash11("sha256").update(value).digest("hex");
34830
+ return createHash12("sha256").update(value).digest("hex");
33579
34831
  }
33580
34832
  function fileRecord(root, relativePath) {
33581
- const path = join16(root, relativePath);
33582
- if (!existsSync19(path))
34833
+ const path = join17(root, relativePath);
34834
+ if (!existsSync20(path))
33583
34835
  return null;
33584
34836
  const stat = statSync7(path);
33585
34837
  if (!stat.isFile())
@@ -33591,7 +34843,7 @@ function manifestRecord(root, relativePath) {
33591
34843
  const base = fileRecord(root, relativePath);
33592
34844
  if (!base)
33593
34845
  return null;
33594
- const parsed = readJsonFile(join16(root, relativePath));
34846
+ const parsed = readJsonFile(join17(root, relativePath));
33595
34847
  if (!parsed)
33596
34848
  return { ...base, redacted: {} };
33597
34849
  const redacted = redactValue({
@@ -33686,8 +34938,8 @@ function commandEnv(env, includeValues) {
33686
34938
  function defaultSnapshotDir() {
33687
34939
  const dbPath = getDatabasePath();
33688
34940
  if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
33689
- return join16(tmpdir2(), "hasna-todos", "environment-snapshots");
33690
- return join16(dirname15(resolve14(dbPath)), "environment-snapshots");
34941
+ return join17(tmpdir2(), "hasna-todos", "environment-snapshots");
34942
+ return join17(dirname15(resolve14(dbPath)), "environment-snapshots");
33691
34943
  }
33692
34944
  function snapshotWithId(snapshot) {
33693
34945
  const digest = sha2565(JSON.stringify(snapshot)).slice(0, 24);
@@ -33734,7 +34986,7 @@ function captureEnvironmentSnapshot(input = {}) {
33734
34986
  });
33735
34987
  }
33736
34988
  function writeEnvironmentSnapshot(snapshot, outputPath) {
33737
- const path = outputPath ? resolve14(outputPath) : join16(defaultSnapshotDir(), `${snapshot.id}.json`);
34989
+ const path = outputPath ? resolve14(outputPath) : join17(defaultSnapshotDir(), `${snapshot.id}.json`);
33738
34990
  ensureDir2(dirname15(path));
33739
34991
  writeJsonFile(path, snapshot);
33740
34992
  return path;
@@ -33825,9 +35077,9 @@ function compareEnvironmentSnapshotFiles(leftPath, rightPath) {
33825
35077
  }
33826
35078
  // src/lib/decision-records.ts
33827
35079
  init_database();
33828
- import { createHash as createHash12 } from "crypto";
35080
+ import { createHash as createHash13 } from "crypto";
33829
35081
  import { mkdirSync as mkdirSync17, writeFileSync as writeFileSync15 } from "fs";
33830
- import { dirname as dirname16, join as join17 } from "path";
35082
+ import { dirname as dirname16, join as join18 } from "path";
33831
35083
  var DECISION_RECORD_SCHEMA = "todos.decision_record.v1";
33832
35084
  var KNOWLEDGE_SNAPSHOT_SCHEMA = "todos.knowledge_snapshot.v1";
33833
35085
  var DECISION_STATUSES = ["proposed", "accepted", "deprecated", "superseded", "rejected"];
@@ -33881,7 +35133,7 @@ function rowToDecisionRecord(row) {
33881
35133
  }
33882
35134
  function stableSnapshotHash(payload) {
33883
35135
  const { captured_at: _capturedAt, ...rest } = payload;
33884
- return createHash12("sha256").update(JSON.stringify(rest)).digest("hex");
35136
+ return createHash13("sha256").update(JSON.stringify(rest)).digest("hex");
33885
35137
  }
33886
35138
  function createDecisionRecord(input, db) {
33887
35139
  const d = db || getDatabase();
@@ -34057,7 +35309,7 @@ function exportDecisionRecord(id, outputPath, format = "markdown", db) {
34057
35309
  if (!record)
34058
35310
  throw new Error(`Decision record not found: ${id}`);
34059
35311
  const content = format === "markdown" ? formatDecisionRecordMarkdown(record) : JSON.stringify(record, null, 2);
34060
- const path = outputPath ?? join17(process.cwd(), ".todos", "decisions", `${record.short_ref}.${format === "markdown" ? "md" : "json"}`);
35312
+ const path = outputPath ?? join18(process.cwd(), ".todos", "decisions", `${record.short_ref}.${format === "markdown" ? "md" : "json"}`);
34061
35313
  mkdirSync17(dirname16(path), { recursive: true });
34062
35314
  writeFileSync15(path, content, "utf8");
34063
35315
  return { path, content };
@@ -34205,7 +35457,7 @@ function exportKnowledgeSnapshot(id, outputPath, format = "markdown", db) {
34205
35457
  throw new Error(`Knowledge snapshot not found: ${id}`);
34206
35458
  const content = format === "markdown" ? formatKnowledgeSnapshotMarkdown(record) : JSON.stringify(record, null, 2);
34207
35459
  const slug = record.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
34208
- const path = outputPath ?? join17(process.cwd(), ".todos", "knowledge", `${slug || record.id.slice(0, 8)}.${format === "markdown" ? "md" : "json"}`);
35460
+ const path = outputPath ?? join18(process.cwd(), ".todos", "knowledge", `${slug || record.id.slice(0, 8)}.${format === "markdown" ? "md" : "json"}`);
34209
35461
  mkdirSync17(dirname16(path), { recursive: true });
34210
35462
  writeFileSync15(path, content, "utf8");
34211
35463
  return { path, content };
@@ -34500,8 +35752,8 @@ todos report export --kind retrospective --days 14 --format markdown --out retro
34500
35752
  `;
34501
35753
  }
34502
35754
  // src/lib/command-aliases.ts
34503
- import { existsSync as existsSync20, readFileSync as readFileSync20, writeFileSync as writeFileSync17, mkdirSync as mkdirSync19 } from "fs";
34504
- import { join as join18 } from "path";
35755
+ import { existsSync as existsSync21, readFileSync as readFileSync20, writeFileSync as writeFileSync17, mkdirSync as mkdirSync19 } from "fs";
35756
+ import { join as join19 } from "path";
34505
35757
  var COMMAND_ALIASES_SCHEMA = "todos.command_aliases.v1";
34506
35758
  var RESERVED = new Set([...listTopLevelCommands(), "help", "version", "alias", "shortcuts"]);
34507
35759
  var BUILTIN_SHORTCUTS = [
@@ -34520,7 +35772,7 @@ var BUILTIN_SHORTCUTS = [
34520
35772
  { pattern: /^reports?$/, argv: ["report", "docs"], explain: "Report export documentation" }
34521
35773
  ];
34522
35774
  function aliasesPath(cwd = process.cwd()) {
34523
- return join18(cwd, ".todos", "aliases.json");
35775
+ return join19(cwd, ".todos", "aliases.json");
34524
35776
  }
34525
35777
  function emptyStore() {
34526
35778
  return { schema_version: COMMAND_ALIASES_SCHEMA, aliases: {}, updated_at: new Date(0).toISOString() };
@@ -34537,7 +35789,7 @@ function validateAliasName(name) {
34537
35789
  }
34538
35790
  function loadAliasStore(cwd) {
34539
35791
  const path = aliasesPath(cwd);
34540
- if (!existsSync20(path))
35792
+ if (!existsSync21(path))
34541
35793
  return emptyStore();
34542
35794
  const parsed = JSON.parse(readFileSync20(path, "utf8"));
34543
35795
  if (parsed.schema_version !== COMMAND_ALIASES_SCHEMA) {
@@ -34547,7 +35799,7 @@ function loadAliasStore(cwd) {
34547
35799
  }
34548
35800
  function saveAliasStore(store, cwd) {
34549
35801
  const path = aliasesPath(cwd);
34550
- mkdirSync19(join18(path, ".."), { recursive: true });
35802
+ mkdirSync19(join19(path, ".."), { recursive: true });
34551
35803
  store.updated_at = new Date().toISOString();
34552
35804
  writeFileSync17(path, JSON.stringify(store, null, 2), "utf8");
34553
35805
  }
@@ -35195,18 +36447,18 @@ function createBranchWorkPlan(input, db) {
35195
36447
  }
35196
36448
  // src/lib/user-scaffolds.ts
35197
36449
  init_database();
35198
- import { existsSync as existsSync21, readFileSync as readFileSync21, writeFileSync as writeFileSync18, mkdirSync as mkdirSync20 } from "fs";
35199
- import { join as join19 } from "path";
36450
+ import { existsSync as existsSync22, readFileSync as readFileSync21, writeFileSync as writeFileSync18, mkdirSync as mkdirSync20 } from "fs";
36451
+ import { join as join20 } from "path";
35200
36452
  var USER_SCAFFOLD_SCHEMA = "todos.user_scaffold.v1";
35201
36453
  var SCAFFOLD_KINDS = ["task", "project", "plan", "checklist", "contract", "verification_policy"];
35202
36454
  function storeDir(cwd = process.cwd()) {
35203
- return join19(cwd, ".todos", "scaffolds");
36455
+ return join20(cwd, ".todos", "scaffolds");
35204
36456
  }
35205
36457
  function storePath(cwd) {
35206
- return join19(storeDir(cwd), "store.json");
36458
+ return join20(storeDir(cwd), "store.json");
35207
36459
  }
35208
36460
  function versionsDir(cwd) {
35209
- return join19(storeDir(cwd), "versions");
36461
+ return join20(storeDir(cwd), "versions");
35210
36462
  }
35211
36463
  function slugify5(name) {
35212
36464
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
@@ -35216,7 +36468,7 @@ function emptyStore2() {
35216
36468
  }
35217
36469
  function loadUserScaffoldStore(cwd) {
35218
36470
  const path = storePath(cwd);
35219
- if (!existsSync21(path))
36471
+ if (!existsSync22(path))
35220
36472
  return emptyStore2();
35221
36473
  const parsed = JSON.parse(readFileSync21(path, "utf8"));
35222
36474
  if (parsed.schema_version !== USER_SCAFFOLD_SCHEMA) {
@@ -35231,7 +36483,7 @@ function saveUserScaffoldStore(store, cwd) {
35231
36483
  }
35232
36484
  function snapshotVersion(scaffold, cwd) {
35233
36485
  mkdirSync20(versionsDir(cwd), { recursive: true });
35234
- const path = join19(versionsDir(cwd), `${scaffold.id}-v${scaffold.version}.json`);
36486
+ const path = join20(versionsDir(cwd), `${scaffold.id}-v${scaffold.version}.json`);
35235
36487
  writeFileSync18(path, JSON.stringify(scaffold, null, 2), "utf8");
35236
36488
  }
35237
36489
  function listUserScaffolds(kind, cwd) {
@@ -35478,7 +36730,7 @@ function listLinkedTemplates(db, cwd) {
35478
36730
  // src/lib/agent-workflow-demo.ts
35479
36731
  init_database();
35480
36732
  import { mkdtempSync } from "fs";
35481
- import { join as join20 } from "path";
36733
+ import { join as join21 } from "path";
35482
36734
  import { tmpdir as tmpdir3 } from "os";
35483
36735
  var AGENT_WORKFLOW_DEMO_SCHEMA = "todos.agent_workflow_demo.v1";
35484
36736
  var DEMO_DEFAULT_AGENT = "demoagent";
@@ -35494,7 +36746,7 @@ function setupEphemeralDemoDb(options = {}) {
35494
36746
  if (options.db_path) {
35495
36747
  db_path = options.db_path;
35496
36748
  } else if (options.persist) {
35497
- db_path = join20(mkdtempSync(join20(tmpdir3(), "todos-demo-")), "todos.db");
36749
+ db_path = join21(mkdtempSync(join21(tmpdir3(), "todos-demo-")), "todos.db");
35498
36750
  } else {
35499
36751
  db_path = ":memory:";
35500
36752
  }
@@ -37830,18 +39082,18 @@ function runSearchView(idOrName, db) {
37830
39082
  return { ...runSavedSearch(view.filters, view.scope, d), view };
37831
39083
  }
37832
39084
  // src/lib/claude-tasks.ts
37833
- import { existsSync as existsSync22, readFileSync as readFileSync22, readdirSync as readdirSync4, writeFileSync as writeFileSync19 } from "fs";
37834
- import { join as join21 } from "path";
39085
+ import { existsSync as existsSync23, readFileSync as readFileSync22, readdirSync as readdirSync4, writeFileSync as writeFileSync19 } from "fs";
39086
+ import { join as join22 } from "path";
37835
39087
  init_config();
37836
39088
  init_sync_utils();
37837
39089
  function getTaskListDir(taskListId) {
37838
- return join21(HOME, ".claude", "tasks", taskListId);
39090
+ return join22(HOME, ".claude", "tasks", taskListId);
37839
39091
  }
37840
39092
  function readClaudeTask(dir, filename) {
37841
- return readJsonFile(join21(dir, filename));
39093
+ return readJsonFile(join22(dir, filename));
37842
39094
  }
37843
39095
  function writeClaudeTask(dir, task2) {
37844
- writeJsonFile(join21(dir, `${task2.id}.json`), task2);
39096
+ writeJsonFile(join22(dir, `${task2.id}.json`), task2);
37845
39097
  }
37846
39098
  function toClaudeStatus(status) {
37847
39099
  if (status === "pending" || status === "in_progress" || status === "completed") {
@@ -37853,14 +39105,14 @@ function toSqliteStatus(status) {
37853
39105
  return status;
37854
39106
  }
37855
39107
  function readPrefixCounter(dir) {
37856
- const path = join21(dir, ".prefix-counter");
37857
- if (!existsSync22(path))
39108
+ const path = join22(dir, ".prefix-counter");
39109
+ if (!existsSync23(path))
37858
39110
  return 0;
37859
39111
  const val = parseInt(readFileSync22(path, "utf-8").trim(), 10);
37860
39112
  return isNaN(val) ? 0 : val;
37861
39113
  }
37862
39114
  function writePrefixCounter(dir, value) {
37863
- writeFileSync19(join21(dir, ".prefix-counter"), String(value));
39115
+ writeFileSync19(join22(dir, ".prefix-counter"), String(value));
37864
39116
  }
37865
39117
  function formatPrefixedSubject(title, prefix, counter) {
37866
39118
  const padded = String(counter).padStart(5, "0");
@@ -37887,7 +39139,7 @@ function taskToClaudeTask(task2, claudeTaskId, existingMeta) {
37887
39139
  }
37888
39140
  function pushToClaudeTaskList(taskListId, projectId, options = {}) {
37889
39141
  const dir = getTaskListDir(taskListId);
37890
- if (!existsSync22(dir))
39142
+ if (!existsSync23(dir))
37891
39143
  ensureDir2(dir);
37892
39144
  const filter = {};
37893
39145
  if (projectId)
@@ -37896,7 +39148,7 @@ function pushToClaudeTaskList(taskListId, projectId, options = {}) {
37896
39148
  const existingByTodosId = new Map;
37897
39149
  const files = listJsonFiles(dir);
37898
39150
  for (const f of files) {
37899
- const path = join21(dir, f);
39151
+ const path = join22(dir, f);
37900
39152
  const ct = readClaudeTask(dir, f);
37901
39153
  if (ct?.metadata?.["todos_id"]) {
37902
39154
  existingByTodosId.set(ct.metadata["todos_id"], { task: ct, mtimeMs: getFileMtimeMs(path) });
@@ -37983,7 +39235,7 @@ function pushToClaudeTaskList(taskListId, projectId, options = {}) {
37983
39235
  }
37984
39236
  function pullFromClaudeTaskList(taskListId, projectId, options = {}) {
37985
39237
  const dir = getTaskListDir(taskListId);
37986
- if (!existsSync22(dir)) {
39238
+ if (!existsSync23(dir)) {
37987
39239
  return { pushed: 0, pulled: 0, errors: [`Task list directory not found: ${dir}`] };
37988
39240
  }
37989
39241
  const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
@@ -38003,7 +39255,7 @@ function pullFromClaudeTaskList(taskListId, projectId, options = {}) {
38003
39255
  }
38004
39256
  for (const f of files) {
38005
39257
  try {
38006
- const filePath = join21(dir, f);
39258
+ const filePath = join22(dir, f);
38007
39259
  const ct = readClaudeTask(dir, f);
38008
39260
  if (!ct)
38009
39261
  continue;
@@ -38071,22 +39323,22 @@ function syncClaudeTaskList(taskListId, projectId, options = {}) {
38071
39323
  }
38072
39324
 
38073
39325
  // src/lib/agent-tasks.ts
38074
- import { existsSync as existsSync23 } from "fs";
38075
- import { join as join22 } from "path";
39326
+ import { existsSync as existsSync24 } from "fs";
39327
+ import { join as join23 } from "path";
38076
39328
  init_sync_utils();
38077
39329
  init_config();
38078
39330
  function agentBaseDir(agent) {
38079
39331
  const key = `TODOS_${agent.toUpperCase()}_TASKS_DIR`;
38080
- return process.env[key] || getAgentTasksDir(agent) || process.env["TODOS_AGENT_TASKS_DIR"] || join22(getTodosGlobalDir(), "agents");
39332
+ return process.env[key] || getAgentTasksDir(agent) || process.env["TODOS_AGENT_TASKS_DIR"] || join23(getTodosGlobalDir(), "agents");
38081
39333
  }
38082
39334
  function getTaskListDir2(agent, taskListId) {
38083
- return join22(agentBaseDir(agent), agent, taskListId);
39335
+ return join23(agentBaseDir(agent), agent, taskListId);
38084
39336
  }
38085
39337
  function readAgentTask(dir, filename) {
38086
- return readJsonFile(join22(dir, filename));
39338
+ return readJsonFile(join23(dir, filename));
38087
39339
  }
38088
39340
  function writeAgentTask(dir, task2) {
38089
- writeJsonFile(join22(dir, `${task2.id}.json`), task2);
39341
+ writeJsonFile(join23(dir, `${task2.id}.json`), task2);
38090
39342
  }
38091
39343
  function taskToAgentTask(task2, externalId, existingMeta) {
38092
39344
  return {
@@ -38111,7 +39363,7 @@ function metadataKey(agent) {
38111
39363
  }
38112
39364
  function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
38113
39365
  const dir = getTaskListDir2(agent, taskListId);
38114
- if (!existsSync23(dir))
39366
+ if (!existsSync24(dir))
38115
39367
  ensureDir2(dir);
38116
39368
  const filter = {};
38117
39369
  if (projectId)
@@ -38120,7 +39372,7 @@ function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
38120
39372
  const existingByTodosId = new Map;
38121
39373
  const files = listJsonFiles(dir);
38122
39374
  for (const f of files) {
38123
- const path = join22(dir, f);
39375
+ const path = join23(dir, f);
38124
39376
  const at = readAgentTask(dir, f);
38125
39377
  if (at?.metadata?.["todos_id"]) {
38126
39378
  existingByTodosId.set(at.metadata["todos_id"], { task: at, mtimeMs: getFileMtimeMs(path) });
@@ -38194,7 +39446,7 @@ function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
38194
39446
  }
38195
39447
  function pullFromAgentTaskList(agent, taskListId, projectId, options = {}) {
38196
39448
  const dir = getTaskListDir2(agent, taskListId);
38197
- if (!existsSync23(dir)) {
39449
+ if (!existsSync24(dir)) {
38198
39450
  return { pushed: 0, pulled: 0, errors: [`Task list directory not found: ${dir}`] };
38199
39451
  }
38200
39452
  const files = listJsonFiles(dir);
@@ -38213,7 +39465,7 @@ function pullFromAgentTaskList(agent, taskListId, projectId, options = {}) {
38213
39465
  }
38214
39466
  for (const f of files) {
38215
39467
  try {
38216
- const filePath = join22(dir, f);
39468
+ const filePath = join23(dir, f);
38217
39469
  const at = readAgentTask(dir, f);
38218
39470
  if (!at)
38219
39471
  continue;
@@ -38351,9 +39603,9 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
38351
39603
  return { pushed, pulled, errors };
38352
39604
  }
38353
39605
  // src/lib/extract.ts
38354
- import { existsSync as existsSync24, readFileSync as readFileSync23, statSync as statSync8 } from "fs";
38355
- import { createHash as createHash13 } from "crypto";
38356
- import { relative as relative6, resolve as resolve15, join as join23 } from "path";
39606
+ import { existsSync as existsSync25, readFileSync as readFileSync23, statSync as statSync8 } from "fs";
39607
+ import { createHash as createHash14 } from "crypto";
39608
+ import { relative as relative6, resolve as resolve15, join as join24 } from "path";
38357
39609
  var EXTRACT_TAGS = ["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"];
38358
39610
  var DEFAULT_EXTENSIONS = new Set([
38359
39611
  ".ts",
@@ -38417,15 +39669,15 @@ var SKIP_DIRS2 = new Set([
38417
39669
  ".parcel-cache"
38418
39670
  ]);
38419
39671
  function stableHash(value) {
38420
- return createHash13("sha256").update(value).digest("hex");
39672
+ return createHash14("sha256").update(value).digest("hex");
38421
39673
  }
38422
39674
  function normalizePathForMatch(value) {
38423
39675
  return value.replace(/\\/g, "/").replace(/^\.\//, "");
38424
39676
  }
38425
39677
  function readGitignorePatterns(basePath) {
38426
39678
  const root = statSync8(basePath).isFile() ? resolve15(basePath, "..") : basePath;
38427
- const gitignorePath = join23(root, ".gitignore");
38428
- if (!existsSync24(gitignorePath))
39679
+ const gitignorePath = join24(root, ".gitignore");
39680
+ if (!existsSync25(gitignorePath))
38429
39681
  return [];
38430
39682
  try {
38431
39683
  return readFileSync23(gitignorePath, "utf-8").split(`
@@ -38567,7 +39819,7 @@ function buildCodebaseIndex(options) {
38567
39819
  const files = collectFiles(basePath, extensions, excludes, respectGitignore);
38568
39820
  const indexed = [];
38569
39821
  for (const file of files) {
38570
- const fullPath = statSync8(basePath).isFile() ? basePath : join23(basePath, file);
39822
+ const fullPath = statSync8(basePath).isFile() ? basePath : join24(basePath, file);
38571
39823
  try {
38572
39824
  const source9 = readFileSync23(fullPath, "utf-8");
38573
39825
  const relPath = statSync8(basePath).isFile() ? relative6(resolve15(basePath, ".."), fullPath) : file;
@@ -38598,7 +39850,7 @@ function extractTodos(options, db) {
38598
39850
  const files = collectFiles(basePath, extensions, excludes, respectGitignore);
38599
39851
  const allComments = [];
38600
39852
  for (const file of files) {
38601
- const fullPath = statSync8(basePath).isFile() ? basePath : join23(basePath, file);
39853
+ const fullPath = statSync8(basePath).isFile() ? basePath : join24(basePath, file);
38602
39854
  try {
38603
39855
  const source9 = readFileSync23(fullPath, "utf-8");
38604
39856
  const relPath = statSync8(basePath).isFile() ? relative6(resolve15(basePath, ".."), fullPath) : file;
@@ -38633,7 +39885,7 @@ function extractTodos(options, db) {
38633
39885
  skipped++;
38634
39886
  continue;
38635
39887
  }
38636
- const taskTags = ["extracted", comment.tag.toLowerCase(), ...options.tags || []];
39888
+ const taskTags2 = ["extracted", comment.tag.toLowerCase(), ...options.tags || []];
38637
39889
  const task2 = createTask({
38638
39890
  title: `[${comment.tag}] ${comment.message}`,
38639
39891
  description: `Extracted from code comment in \`${comment.file}\` at line ${comment.line}:
@@ -38645,7 +39897,7 @@ ${comment.raw.trim()}
38645
39897
  task_list_id: options.task_list_id,
38646
39898
  assigned_to: options.assigned_to,
38647
39899
  agent_id: options.agent_id,
38648
- tags: taskTags,
39900
+ tags: taskTags2,
38649
39901
  metadata: {
38650
39902
  source: "code_comment",
38651
39903
  comment_type: comment.tag,
@@ -38906,7 +40158,7 @@ init_config();
38906
40158
  // src/lib/natural-language-intake.ts
38907
40159
  init_database();
38908
40160
  init_redaction();
38909
- var PRIORITIES3 = new Set(["low", "medium", "high", "critical"]);
40161
+ var PRIORITIES4 = new Set(["low", "medium", "high", "critical"]);
38910
40162
  function compact2(value) {
38911
40163
  return value.replace(/\s+/g, " ").trim();
38912
40164
  }
@@ -38918,7 +40170,7 @@ function splitItems(text) {
38918
40170
  }
38919
40171
  function parsePriority(line, fallback) {
38920
40172
  const explicit = line.match(/\b(?:priority[:= ]+|p:)(critical|high|medium|low)\b/i)?.[1]?.toLowerCase();
38921
- if (explicit && PRIORITIES3.has(explicit))
40173
+ if (explicit && PRIORITIES4.has(explicit))
38922
40174
  return explicit;
38923
40175
  if (/\b(p0|urgent|blocker|critical)\b/i.test(line))
38924
40176
  return "critical";
@@ -39305,7 +40557,7 @@ function renderWorkflowStatesMarkdown(states = listWorkflowStates()) {
39305
40557
  }
39306
40558
  // src/lib/agent-replay-simulator.ts
39307
40559
  init_redaction();
39308
- import { createHash as createHash14 } from "crypto";
40560
+ import { createHash as createHash15 } from "crypto";
39309
40561
  import { readFileSync as readFileSync24 } from "fs";
39310
40562
  function isObject(value) {
39311
40563
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
@@ -39327,7 +40579,7 @@ function stable2(value) {
39327
40579
  return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable2(value[key])]));
39328
40580
  }
39329
40581
  function fingerprint3(value) {
39330
- return createHash14("sha256").update(JSON.stringify(stable2(value))).digest("hex");
40582
+ return createHash15("sha256").update(JSON.stringify(stable2(value))).digest("hex");
39331
40583
  }
39332
40584
  function unpackFixture(input) {
39333
40585
  if (!isObject(input))
@@ -39566,9 +40818,9 @@ function renderAgentReplaySimulationMarkdown(simulation) {
39566
40818
  }
39567
40819
  // src/lib/local-extensions.ts
39568
40820
  init_config();
39569
- import { createHash as createHash15, createVerify } from "crypto";
39570
- import { existsSync as existsSync25, readdirSync as readdirSync5, readFileSync as readFileSync25, statSync as statSync9 } from "fs";
39571
- import { basename as basename5, join as join24, resolve as resolve16 } from "path";
40821
+ import { createHash as createHash16, createVerify } from "crypto";
40822
+ import { existsSync as existsSync26, readdirSync as readdirSync5, readFileSync as readFileSync25, statSync as statSync9 } from "fs";
40823
+ import { basename as basename5, join as join25, resolve as resolve16 } from "path";
39572
40824
  init_redaction();
39573
40825
  function isObject2(value) {
39574
40826
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
@@ -39653,7 +40905,7 @@ function parseJson(path) {
39653
40905
  return JSON.parse(readFileSync25(path, "utf8"));
39654
40906
  }
39655
40907
  function sha2566(bytes) {
39656
- return `sha256:${createHash15("sha256").update(bytes).digest("hex")}`;
40908
+ return `sha256:${createHash16("sha256").update(bytes).digest("hex")}`;
39657
40909
  }
39658
40910
  function compareVersions(a, b) {
39659
40911
  const left = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
@@ -39849,10 +41101,10 @@ function verifyExtensionSignature(input) {
39849
41101
  }
39850
41102
  function inspectExtensionSource(source9) {
39851
41103
  const resolved = resolve16(source9);
39852
- if (!existsSync25(resolved))
41104
+ if (!existsSync26(resolved))
39853
41105
  throw new Error(`extension source not found: ${source9}`);
39854
41106
  const stat = statSync9(resolved);
39855
- const manifestPath = stat.isDirectory() ? [join24(resolved, "todos.extension.json"), join24(resolved, "extension.json")].find(existsSync25) : resolved;
41107
+ const manifestPath = stat.isDirectory() ? [join25(resolved, "todos.extension.json"), join25(resolved, "extension.json")].find(existsSync26) : resolved;
39856
41108
  if (!manifestPath)
39857
41109
  throw new Error(`extension directory ${source9} is missing todos.extension.json`);
39858
41110
  const raw = readFileSync25(manifestPath);
@@ -39948,20 +41200,20 @@ function projectExtensionSources(projectPath) {
39948
41200
  return [];
39949
41201
  const root = resolve16(projectPath);
39950
41202
  const candidates = [
39951
- join24(root, "todos.extension.json"),
39952
- join24(root, ".todos", "todos.extension.json")
41203
+ join25(root, "todos.extension.json"),
41204
+ join25(root, ".todos", "todos.extension.json")
39953
41205
  ];
39954
- const extensionDir = join24(root, ".todos", "extensions");
39955
- if (existsSync25(extensionDir)) {
41206
+ const extensionDir = join25(root, ".todos", "extensions");
41207
+ if (existsSync26(extensionDir)) {
39956
41208
  for (const entry2 of readdirSync5(extensionDir)) {
39957
41209
  if (entry2.startsWith("."))
39958
41210
  continue;
39959
- const full = join24(extensionDir, entry2);
41211
+ const full = join25(extensionDir, entry2);
39960
41212
  if (statSync9(full).isDirectory() || entry2.endsWith(".json"))
39961
41213
  candidates.push(full);
39962
41214
  }
39963
41215
  }
39964
- return candidates.filter(existsSync25);
41216
+ return candidates.filter(existsSync26);
39965
41217
  }
39966
41218
  function discoverLocalExtensions(options = {}) {
39967
41219
  const config = loadConfig();
@@ -40248,6 +41500,18 @@ function formatDispatchMessage(tasks, opts = {}) {
40248
41500
  // src/lib/tmux.ts
40249
41501
  var DELAY_MIN = 3000;
40250
41502
  var DELAY_MAX = 5000;
41503
+ var IDLE_TMUX_COMMANDS = new Set([
41504
+ "bash",
41505
+ "dash",
41506
+ "elvish",
41507
+ "fish",
41508
+ "ksh",
41509
+ "nu",
41510
+ "pwsh",
41511
+ "sh",
41512
+ "tmux",
41513
+ "zsh"
41514
+ ]);
40251
41515
  function parseTmuxTarget(spec) {
40252
41516
  if (!spec || spec.trim() === "") {
40253
41517
  throw new Error("tmux target spec cannot be empty");
@@ -40304,17 +41568,73 @@ async function validateTmuxTarget(spec) {
40304
41568
  throw new Error(`tmux target "${targetStr}" not found: ${stderr.trim() || "unknown error"}`);
40305
41569
  }
40306
41570
  }
41571
+ async function inspectTmuxPane(target) {
41572
+ const proc = Bun.spawn([
41573
+ "tmux",
41574
+ "display-message",
41575
+ "-p",
41576
+ "-t",
41577
+ target,
41578
+ "#{pane_id}\t#{pane_current_command}\t#{pane_dead}\t#{pane_input_off}\t#{pane_in_mode}"
41579
+ ], {
41580
+ stdout: "pipe",
41581
+ stderr: "pipe"
41582
+ });
41583
+ const exitCode = await proc.exited;
41584
+ if (exitCode !== 0) {
41585
+ const stderr = await new Response(proc.stderr).text();
41586
+ throw new Error(`tmux target "${target}" not found: ${stderr.trim() || "unknown error"}`);
41587
+ }
41588
+ const stdout = (await new Response(proc.stdout).text()).trim();
41589
+ const [paneId, currentCommand = "", paneDead = "0", inputOff = "0", inMode = "0"] = stdout.split("\t");
41590
+ if (!paneId) {
41591
+ throw new Error(`tmux target "${target}" did not resolve to a pane`);
41592
+ }
41593
+ return {
41594
+ target,
41595
+ paneId,
41596
+ currentCommand,
41597
+ paneDead: paneDead === "1",
41598
+ inputOff: inputOff === "1",
41599
+ inMode: inMode === "1"
41600
+ };
41601
+ }
41602
+ function tmuxPaneBusyStatus(pane) {
41603
+ if (pane.paneDead) {
41604
+ return { busy: true, reason: "pane is dead" };
41605
+ }
41606
+ if (pane.inputOff) {
41607
+ return { busy: true, reason: "pane input is disabled" };
41608
+ }
41609
+ if (pane.inMode) {
41610
+ return { busy: true, reason: "pane is in copy or alternate mode" };
41611
+ }
41612
+ const currentCommand = pane.currentCommand.trim();
41613
+ if (currentCommand && !IDLE_TMUX_COMMANDS.has(currentCommand)) {
41614
+ return { busy: true, reason: `pane is running ${currentCommand}` };
41615
+ }
41616
+ return { busy: false, reason: null };
41617
+ }
40307
41618
  function calculateDelay(message) {
40308
41619
  const len = message.length;
40309
41620
  const extra = Math.floor(len / 100 * 40);
40310
41621
  return Math.min(DELAY_MIN + extra, DELAY_MAX);
40311
41622
  }
40312
- async function sendToTmux(target, message, delayMs, dryRun = false) {
41623
+ async function sendToTmux(target, message, delayMs, options = false) {
41624
+ const opts = typeof options === "boolean" ? { dryRun: options } : options;
41625
+ const dryRun = opts.dryRun ?? false;
40313
41626
  if (dryRun) {
40314
41627
  console.log(`[dry-run] sendToTmux target=${target} delay=${delayMs}ms`);
40315
41628
  console.log(`[dry-run] message: ${message.slice(0, 200)}`);
40316
41629
  return;
40317
41630
  }
41631
+ if (!opts.confirmBusy) {
41632
+ const pane = await inspectTmuxPane(target);
41633
+ const status = tmuxPaneBusyStatus(pane);
41634
+ if (status.busy) {
41635
+ throw new Error(`tmux target "${target}" appears busy (${status.reason}). Re-run with --confirm-busy to send anyway.`);
41636
+ }
41637
+ }
40318
41638
  const sendProc = Bun.spawn(["tmux", "send-keys", "-t", target, message, ""], {
40319
41639
  stdout: "pipe",
40320
41640
  stderr: "pipe"
@@ -40346,7 +41666,10 @@ async function executeDispatch(dispatch, opts = {}, db) {
40346
41666
  }
40347
41667
  const delayMs = dispatch.delay_ms ?? calculateDelay(message);
40348
41668
  try {
40349
- await sendToTmux(dispatch.target_window, message, delayMs, opts.dryRun ?? false);
41669
+ await sendToTmux(dispatch.target_window, message, delayMs, {
41670
+ dryRun: opts.dryRun ?? false,
41671
+ confirmBusy: opts.confirmBusy ?? false
41672
+ });
40350
41673
  createDispatchLog({
40351
41674
  dispatch_id: dispatch.id,
40352
41675
  target_window: dispatch.target_window,
@@ -40404,7 +41727,7 @@ async function dispatchToMultiple(input, opts = {}, db) {
40404
41727
  init_redaction();
40405
41728
 
40406
41729
  // src/lib/retention-cleanup.ts
40407
- import { existsSync as existsSync26, unlinkSync as unlinkSync2 } from "fs";
41730
+ import { existsSync as existsSync27, unlinkSync as unlinkSync2 } from "fs";
40408
41731
  init_database();
40409
41732
  var RETENTION_CLEANUP_CONFIRMATION = "delete-local-retention-data";
40410
41733
  var ALL_SCOPES = ["comments", "runs", "verifications", "expired_artifacts"];
@@ -40616,7 +41939,7 @@ function applyRetentionCleanup(input, db) {
40616
41939
  for (const artifact of report.candidates.artifact_files) {
40617
41940
  try {
40618
41941
  const path = artifactStorePath(artifact.relative_path);
40619
- if (!existsSync26(path)) {
41942
+ if (!existsSync27(path)) {
40620
41943
  report.warnings.push(`stored artifact already missing: ${artifact.relative_path}`);
40621
41944
  continue;
40622
41945
  }
@@ -40854,8 +42177,8 @@ function renderScalePerformanceReportMarkdown(report) {
40854
42177
  init_database();
40855
42178
  init_migrations();
40856
42179
  init_schema();
40857
- import { chmodSync, copyFileSync as copyFileSync2, existsSync as existsSync27, mkdirSync as mkdirSync21, statSync as statSync10 } from "fs";
40858
- import { basename as basename6, dirname as dirname18, join as join25 } from "path";
42180
+ import { chmodSync, copyFileSync as copyFileSync2, existsSync as existsSync28, mkdirSync as mkdirSync21, statSync as statSync10 } from "fs";
42181
+ import { basename as basename6, dirname as dirname18, join as join26 } from "path";
40859
42182
  var REQUIRED_TABLES2 = [
40860
42183
  "_migrations",
40861
42184
  "projects",
@@ -40962,7 +42285,7 @@ function findMissingProjectRoots(db) {
40962
42285
  continue;
40963
42286
  if (!row.path.startsWith("/"))
40964
42287
  continue;
40965
- if (!existsSync27(row.path))
42288
+ if (!existsSync28(row.path))
40966
42289
  missing++;
40967
42290
  }
40968
42291
  return missing;
@@ -41022,16 +42345,16 @@ function databasePermissionsAreUnsafe(dbPath) {
41022
42345
  function createBackup(dbPath) {
41023
42346
  if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
41024
42347
  return;
41025
- if (!existsSync27(dbPath))
42348
+ if (!existsSync28(dbPath))
41026
42349
  return;
41027
42350
  const stamp = now().replace(/[:.]/g, "-");
41028
- const backupDir = join25(dirname18(dbPath), `${basename6(dbPath)}.backup-${stamp}`);
42351
+ const backupDir = join26(dirname18(dbPath), `${basename6(dbPath)}.backup-${stamp}`);
41029
42352
  const files = [];
41030
42353
  mkdirSync21(backupDir, { recursive: true });
41031
42354
  for (const source9 of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
41032
- if (!existsSync27(source9))
42355
+ if (!existsSync28(source9))
41033
42356
  continue;
41034
- const target = join25(backupDir, basename6(source9));
42357
+ const target = join26(backupDir, basename6(source9));
41035
42358
  copyFileSync2(source9, target);
41036
42359
  files.push(target);
41037
42360
  }
@@ -41320,7 +42643,7 @@ function upsertCapacityProfile(input) {
41320
42643
  const store = readStore2();
41321
42644
  const key = profileKey(agentId, projectId);
41322
42645
  const existing = store.profiles[key];
41323
- const now3 = timestamp2();
42646
+ const now4 = timestamp2();
41324
42647
  const profile = {
41325
42648
  id: existing?.id ?? key,
41326
42649
  agent_id: agentId,
@@ -41328,8 +42651,8 @@ function upsertCapacityProfile(input) {
41328
42651
  minutes_per_day: assertMinutes(input.minutes_per_day),
41329
42652
  working_days: normalizeWorkingDays(input.working_days),
41330
42653
  effective_from: cleanString2(input.effective_from),
41331
- created_at: existing?.created_at ?? now3,
41332
- updated_at: now3
42654
+ created_at: existing?.created_at ?? now4,
42655
+ updated_at: now4
41333
42656
  };
41334
42657
  store.profiles[key] = profile;
41335
42658
  writeStore2(store);
@@ -41726,6 +43049,8 @@ export {
41726
43049
  uuid,
41727
43050
  upsertWorkspaceTrustProfile,
41728
43051
  upsertVerificationProvider,
43052
+ upsertTesterIssueReports,
43053
+ upsertTesterIssueReport,
41729
43054
  upsertTerminalNotificationRule,
41730
43055
  upsertSecretSafetyConfig,
41731
43056
  upsertRunnerSandboxProfile,
@@ -41937,6 +43262,7 @@ export {
41937
43262
  recordTaskReview,
41938
43263
  recordFilesTouched,
41939
43264
  recordEnvironmentSnapshot,
43265
+ readTesterIssueReportsPayload,
41940
43266
  readLocalBackupFile,
41941
43267
  readEnvironmentSnapshot,
41942
43268
  readBundleFile,
@@ -41975,6 +43301,7 @@ export {
41975
43301
  parseCiLog,
41976
43302
  now,
41977
43303
  notifyUpcomingDeadlines2 as notifyUpcomingDeadlines,
43304
+ normalizeTesterIssueReport,
41978
43305
  normalizeScope,
41979
43306
  normalizeGeneratedAgentNames,
41980
43307
  normalizeFeatureManifest,
@@ -42363,6 +43690,7 @@ export {
42363
43690
  formatAgentWorkflowDemoReport,
42364
43691
  formatActivityRecordText,
42365
43692
  finishTaskRun,
43693
+ fingerprintTesterIssueReport,
42366
43694
  fingerprintInboxInput,
42367
43695
  findTasksByFile,
42368
43696
  findTaskByCommit,
@@ -42422,7 +43750,7 @@ export {
42422
43750
  emitLocalEventHooksQuiet,
42423
43751
  emitLocalEventHooks,
42424
43752
  downloadRunArtifactsFromS3,
42425
- dispatchWebhook,
43753
+ dispatchWebhook2 as dispatchWebhook,
42426
43754
  dispatchToMultiple,
42427
43755
  dismissReminder,
42428
43756
  discoverVerificationProviderCapabilities,
@@ -42665,6 +43993,9 @@ export {
42665
43993
  TODOS_CLI_MCP_PARITY,
42666
43994
  TODOS_CAPABILITIES,
42667
43995
  TODOS_API_ROUTES,
43996
+ TESTERS_ISSUE_REPORT_SCHEMA_VERSION,
43997
+ TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
43998
+ TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION,
42668
43999
  TASK_STATUSES,
42669
44000
  TASK_SCHEDULING_SCHEMA,
42670
44001
  TASK_PRIORITIES,