@hasna/todos 0.11.56 → 0.11.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/contracts.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);
@@ -4769,6 +4769,67 @@ var TODOS_JSON_CONTRACTS = [
4769
4769
  },
4770
4770
  optional: {}
4771
4771
  }),
4772
+ contract({
4773
+ id: "tester_issue_report",
4774
+ name: "Tester Issue Report",
4775
+ description: "Generic issue report payload emitted by @hasna/testers or compatible test runners for conversion into local tasks.",
4776
+ surfaces: ["cli", "sdk"],
4777
+ stability: "stable",
4778
+ required: {
4779
+ schema_version: field("string", "Must be testers.issue_report.v1."),
4780
+ title: field("string", "Short human-readable issue title.")
4781
+ },
4782
+ optional: {
4783
+ kind: field("string", "Issue kind such as assertion_failure, console_error, network_error, accessibility, or unknown."),
4784
+ severity: field("string", "Issue severity mapped to task priority: low, medium, high, or critical."),
4785
+ id: idField,
4786
+ fingerprint: field("string", "Stable tester-side fingerprint. If omitted, @hasna/todos derives one from issue fields."),
4787
+ summary: field(["string", "null"], "Short issue summary.", true),
4788
+ source: field("object", "Run, result, scenario, URL, commit, and branch source metadata."),
4789
+ target: field("object", "URL, route, selector, component, browser, and viewport metadata."),
4790
+ failure: field("object", "Failure message, expected/actual values, stack, reasoning, and reproduction steps."),
4791
+ evidence: field("object", "Logs, screenshots, and artifact references."),
4792
+ labels: tagsField,
4793
+ metadata: metadataField,
4794
+ occurred_at: isoDateField
4795
+ }
4796
+ }),
4797
+ contract({
4798
+ id: "tester_issue_report_result",
4799
+ name: "Tester Issue Report Result",
4800
+ description: "Local-only dry-run or applied result from mapping a tester issue report to a task.",
4801
+ surfaces: ["cli", "sdk"],
4802
+ stability: "stable",
4803
+ required: {
4804
+ schema_version: field("string", "Result schema version."),
4805
+ local_only: field("boolean", "Always true; issue reports are applied to local todos state."),
4806
+ dry_run: field("boolean", "True when no task was created or updated."),
4807
+ processed_at: isoDateField,
4808
+ action: field("string", "preview, matched, created, updated, or regressed."),
4809
+ fingerprint: field("string", "Computed or supplied tester issue fingerprint."),
4810
+ report: field("object", "Normalized tester issue report."),
4811
+ task: field(["object", "null"], "Created, updated, matched, or null task.", true),
4812
+ warnings: field("array", "Non-fatal warnings."),
4813
+ commands: field("array", "Follow-up CLI commands for operators and agents.")
4814
+ },
4815
+ optional: {}
4816
+ }),
4817
+ contract({
4818
+ id: "tester_issue_report_batch_result",
4819
+ name: "Tester Issue Report Batch Result",
4820
+ description: "Local-only dry-run or applied batch result from mapping tester issue reports to tasks.",
4821
+ surfaces: ["cli", "sdk"],
4822
+ stability: "stable",
4823
+ required: {
4824
+ schema_version: field("string", "Batch result schema version."),
4825
+ local_only: field("boolean", "Always true; issue reports are applied to local todos state."),
4826
+ dry_run: field("boolean", "True when no tasks were created or updated."),
4827
+ processed_at: isoDateField,
4828
+ results: field("array", "Single report result entries."),
4829
+ summary: field("object", "Counts grouped by action plus total.")
4830
+ },
4831
+ optional: {}
4832
+ }),
4772
4833
  contract({
4773
4834
  id: "verification_provider",
4774
4835
  name: "Verification Provider",
@@ -5809,7 +5870,7 @@ var TODOS_JSON_CONTRACTS_MANIFEST = createJsonContractsManifest({
5809
5870
  });
5810
5871
  // src/lib/onboarding-fixtures.ts
5811
5872
  import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3 } from "fs";
5812
- import { join as join6 } from "path";
5873
+ import { join as join7 } from "path";
5813
5874
 
5814
5875
  // src/lib/local-bridge.ts
5815
5876
  init_database();
@@ -6469,6 +6530,7 @@ function explainRunnerSandbox(input = {}) {
6469
6530
  // src/lib/event-hooks.ts
6470
6531
  init_config();
6471
6532
  var LOCAL_EVENT_TYPES = [
6533
+ "task.created",
6472
6534
  "task.assigned",
6473
6535
  "task.blocked",
6474
6536
  "task.started",
@@ -6711,6 +6773,752 @@ async function testLocalEventHook(name, input) {
6711
6773
  return emitLocalEventHooks({ ...input, hooks: [hook] });
6712
6774
  }
6713
6775
 
6776
+ // node_modules/.bun/@hasna+events@0.1.9/node_modules/@hasna/events/dist/index.js
6777
+ import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
6778
+ import { existsSync as existsSync6 } from "fs";
6779
+ import { homedir } from "os";
6780
+ import { join as join5 } from "path";
6781
+ import { createHmac, timingSafeEqual } from "crypto";
6782
+ import { randomUUID as randomUUID2 } from "crypto";
6783
+ import { spawn } from "child_process";
6784
+ import { randomUUID as randomUUID22 } from "crypto";
6785
+ function getPathValue(input, path) {
6786
+ return path.split(".").reduce((value, part) => {
6787
+ if (value && typeof value === "object" && part in value) {
6788
+ return value[part];
6789
+ }
6790
+ return;
6791
+ }, input);
6792
+ }
6793
+ function wildcardToRegExp(pattern, options = {}) {
6794
+ let body = "";
6795
+ for (let index = 0;index < pattern.length; index += 1) {
6796
+ const char = pattern[index];
6797
+ if (char === "*") {
6798
+ if (pattern[index + 1] === "*") {
6799
+ body += ".*";
6800
+ index += 1;
6801
+ } else {
6802
+ body += options.segmentSafe ? "[^/]*" : ".*";
6803
+ }
6804
+ } else {
6805
+ body += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
6806
+ }
6807
+ }
6808
+ return new RegExp(`^${body}$`);
6809
+ }
6810
+ function matchString(value, matcher, options = {}) {
6811
+ if (matcher === undefined)
6812
+ return true;
6813
+ if (value === undefined)
6814
+ return false;
6815
+ const matchers = Array.isArray(matcher) ? matcher : [matcher];
6816
+ return matchers.some((item) => wildcardToRegExp(item, options).test(value));
6817
+ }
6818
+ function matchRecord(input, matcher) {
6819
+ if (!matcher)
6820
+ return true;
6821
+ return Object.entries(matcher).every(([path, expected]) => {
6822
+ const actual = getPathValue(input, path);
6823
+ if (typeof expected === "string" || Array.isArray(expected)) {
6824
+ return matchString(actual === undefined ? undefined : String(actual), expected, {
6825
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
6826
+ });
6827
+ }
6828
+ return actual === expected;
6829
+ });
6830
+ }
6831
+ function eventMatchesFilter(event, filter) {
6832
+ 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);
6833
+ }
6834
+ function channelMatchesEvent(channel, event) {
6835
+ if (!channel.enabled)
6836
+ return false;
6837
+ if (!channel.filters || channel.filters.length === 0)
6838
+ return true;
6839
+ return channel.filters.some((filter) => eventMatchesFilter(event, filter));
6840
+ }
6841
+ var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
6842
+ var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
6843
+ function getEventsDataDir(override) {
6844
+ return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join5(homedir(), ".hasna", "events");
6845
+ }
6846
+ class JsonEventsStore {
6847
+ dataDir;
6848
+ channelsPath;
6849
+ eventsPath;
6850
+ deliveriesPath;
6851
+ constructor(dataDir = getEventsDataDir()) {
6852
+ this.dataDir = dataDir;
6853
+ this.channelsPath = join5(dataDir, "channels.json");
6854
+ this.eventsPath = join5(dataDir, "events.json");
6855
+ this.deliveriesPath = join5(dataDir, "deliveries.json");
6856
+ }
6857
+ async init() {
6858
+ await mkdir(this.dataDir, { recursive: true, mode: 448 });
6859
+ await chmod(this.dataDir, 448).catch(() => {
6860
+ return;
6861
+ });
6862
+ await this.ensureArrayFile(this.channelsPath);
6863
+ await this.ensureArrayFile(this.eventsPath);
6864
+ await this.ensureArrayFile(this.deliveriesPath);
6865
+ }
6866
+ async addChannel(channel) {
6867
+ await this.init();
6868
+ const channels = await this.readJson(this.channelsPath, []);
6869
+ const index = channels.findIndex((item) => item.id === channel.id);
6870
+ if (index >= 0) {
6871
+ channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
6872
+ } else {
6873
+ channels.push(channel);
6874
+ }
6875
+ await this.writeJson(this.channelsPath, channels);
6876
+ return index >= 0 ? channels[index] : channel;
6877
+ }
6878
+ async listChannels() {
6879
+ await this.init();
6880
+ return this.readJson(this.channelsPath, []);
6881
+ }
6882
+ async getChannel(id) {
6883
+ const channels = await this.listChannels();
6884
+ return channels.find((channel) => channel.id === id);
6885
+ }
6886
+ async removeChannel(id) {
6887
+ await this.init();
6888
+ const channels = await this.readJson(this.channelsPath, []);
6889
+ const next = channels.filter((channel) => channel.id !== id);
6890
+ await this.writeJson(this.channelsPath, next);
6891
+ return next.length !== channels.length;
6892
+ }
6893
+ async appendEvent(event) {
6894
+ await this.init();
6895
+ const events = await this.readJson(this.eventsPath, []);
6896
+ events.push(event);
6897
+ await this.writeJson(this.eventsPath, events);
6898
+ return event;
6899
+ }
6900
+ async listEvents() {
6901
+ await this.init();
6902
+ return this.readJson(this.eventsPath, []);
6903
+ }
6904
+ async findEventByIdentity(identity) {
6905
+ const events = await this.listEvents();
6906
+ return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
6907
+ }
6908
+ async appendDelivery(result) {
6909
+ await this.init();
6910
+ const deliveries = await this.readJson(this.deliveriesPath, []);
6911
+ deliveries.push(result);
6912
+ await this.writeJson(this.deliveriesPath, deliveries);
6913
+ return result;
6914
+ }
6915
+ async listDeliveries() {
6916
+ await this.init();
6917
+ return this.readJson(this.deliveriesPath, []);
6918
+ }
6919
+ async exportData() {
6920
+ return {
6921
+ channels: await this.listChannels(),
6922
+ events: await this.listEvents(),
6923
+ deliveries: await this.listDeliveries()
6924
+ };
6925
+ }
6926
+ async ensureArrayFile(path) {
6927
+ if (!existsSync6(path)) {
6928
+ await writeFile(path, `[]
6929
+ `, { encoding: "utf-8", mode: 384 });
6930
+ }
6931
+ await chmod(path, 384).catch(() => {
6932
+ return;
6933
+ });
6934
+ }
6935
+ async readJson(path, fallback) {
6936
+ try {
6937
+ const raw = await readFile(path, "utf-8");
6938
+ if (!raw.trim())
6939
+ return fallback;
6940
+ return JSON.parse(raw);
6941
+ } catch (error) {
6942
+ if (error.code === "ENOENT")
6943
+ return fallback;
6944
+ throw error;
6945
+ }
6946
+ }
6947
+ async writeJson(path, value) {
6948
+ const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
6949
+ await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
6950
+ `, { encoding: "utf-8", mode: 384 });
6951
+ await rename(tempPath, path);
6952
+ await chmod(path, 384).catch(() => {
6953
+ return;
6954
+ });
6955
+ }
6956
+ }
6957
+ var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
6958
+ function buildSignatureBase(timestamp, body) {
6959
+ return `${timestamp}.${body}`;
6960
+ }
6961
+ function signPayload(secret, timestamp, body) {
6962
+ const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
6963
+ return `sha256=${digest}`;
6964
+ }
6965
+ function now2() {
6966
+ return new Date().toISOString();
6967
+ }
6968
+ function truncate(value, max = 4096) {
6969
+ return value.length > max ? `${value.slice(0, max)}...` : value;
6970
+ }
6971
+ function buildWebhookRequest(event, channel) {
6972
+ if (!channel.webhook)
6973
+ throw new Error(`Channel ${channel.id} has no webhook config`);
6974
+ const body = JSON.stringify(event);
6975
+ const timestamp = event.time;
6976
+ const headers = {
6977
+ "Content-Type": "application/json",
6978
+ "User-Agent": "@hasna/events",
6979
+ "X-Hasna-Event-Id": event.id,
6980
+ "X-Hasna-Event-Type": event.type,
6981
+ "X-Hasna-Timestamp": timestamp,
6982
+ ...channel.webhook.headers
6983
+ };
6984
+ if (channel.webhook.secret) {
6985
+ headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
6986
+ }
6987
+ return { body, headers };
6988
+ }
6989
+ async function dispatchWebhook(event, channel, options = {}) {
6990
+ if (!channel.webhook)
6991
+ throw new Error(`Channel ${channel.id} has no webhook config`);
6992
+ const startedAt = now2();
6993
+ const { body, headers } = buildWebhookRequest(event, channel);
6994
+ const controller = new AbortController;
6995
+ const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
6996
+ try {
6997
+ const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
6998
+ method: "POST",
6999
+ headers,
7000
+ body,
7001
+ signal: controller.signal
7002
+ });
7003
+ const responseBody = truncate(await response.text());
7004
+ return {
7005
+ attempt: 1,
7006
+ status: response.ok ? "success" : "failed",
7007
+ startedAt,
7008
+ completedAt: now2(),
7009
+ responseStatus: response.status,
7010
+ responseBody,
7011
+ error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
7012
+ };
7013
+ } catch (error) {
7014
+ return {
7015
+ attempt: 1,
7016
+ status: "failed",
7017
+ startedAt,
7018
+ completedAt: now2(),
7019
+ error: error instanceof Error ? error.message : String(error)
7020
+ };
7021
+ } finally {
7022
+ clearTimeout(timeout);
7023
+ }
7024
+ }
7025
+ async function dispatchCommand(event, channel) {
7026
+ if (!channel.command)
7027
+ throw new Error(`Channel ${channel.id} has no command config`);
7028
+ const startedAt = now2();
7029
+ const eventJson = JSON.stringify(event);
7030
+ const env = {
7031
+ ...process.env,
7032
+ ...channel.command.env,
7033
+ HASNA_CHANNEL_ID: channel.id,
7034
+ HASNA_EVENT_ID: event.id,
7035
+ HASNA_EVENT_TYPE: event.type,
7036
+ HASNA_EVENT_SOURCE: event.source,
7037
+ HASNA_EVENT_SUBJECT: event.subject ?? "",
7038
+ HASNA_EVENT_SEVERITY: event.severity,
7039
+ HASNA_EVENT_TIME: event.time,
7040
+ HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
7041
+ HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
7042
+ HASNA_EVENT_JSON: eventJson
7043
+ };
7044
+ return new Promise((resolve6) => {
7045
+ const child = spawn(channel.command.command, channel.command.args ?? [], {
7046
+ cwd: channel.command.cwd,
7047
+ env,
7048
+ stdio: ["pipe", "pipe", "pipe"]
7049
+ });
7050
+ let stdout = "";
7051
+ let stderr = "";
7052
+ const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
7053
+ child.stdin.end(eventJson);
7054
+ child.stdout.on("data", (chunk) => {
7055
+ stdout += chunk.toString();
7056
+ });
7057
+ child.stderr.on("data", (chunk) => {
7058
+ stderr += chunk.toString();
7059
+ });
7060
+ child.on("error", (error) => {
7061
+ clearTimeout(timeout);
7062
+ resolve6({
7063
+ attempt: 1,
7064
+ status: "failed",
7065
+ startedAt,
7066
+ completedAt: now2(),
7067
+ stdout: truncate(stdout),
7068
+ stderr: truncate(stderr),
7069
+ error: error.message
7070
+ });
7071
+ });
7072
+ child.on("close", (code, signal) => {
7073
+ clearTimeout(timeout);
7074
+ const success = code === 0;
7075
+ resolve6({
7076
+ attempt: 1,
7077
+ status: success ? "success" : "failed",
7078
+ startedAt,
7079
+ completedAt: now2(),
7080
+ stdout: truncate(stdout),
7081
+ stderr: truncate(stderr),
7082
+ error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
7083
+ });
7084
+ });
7085
+ });
7086
+ }
7087
+ async function dispatchChannel(event, channel, options = {}) {
7088
+ if (channel.transport === "webhook")
7089
+ return dispatchWebhook(event, channel, options);
7090
+ if (channel.transport === "command")
7091
+ return dispatchCommand(event, channel);
7092
+ return {
7093
+ attempt: 1,
7094
+ status: "skipped",
7095
+ startedAt: now2(),
7096
+ completedAt: now2(),
7097
+ error: `Unsupported transport: ${channel.transport}`
7098
+ };
7099
+ }
7100
+ function createDeliveryResult(event, channel, attempts) {
7101
+ const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
7102
+ return {
7103
+ id: randomUUID2(),
7104
+ eventId: event.id,
7105
+ channelId: channel.id,
7106
+ transport: channel.transport,
7107
+ status,
7108
+ attempts,
7109
+ createdAt: attempts[0]?.startedAt ?? now2(),
7110
+ completedAt: attempts.at(-1)?.completedAt ?? now2()
7111
+ };
7112
+ }
7113
+ function createEvent(input) {
7114
+ return {
7115
+ id: input.id ?? randomUUID22(),
7116
+ source: input.source,
7117
+ type: input.type,
7118
+ time: normalizeTime(input.time),
7119
+ subject: input.subject,
7120
+ severity: input.severity ?? "info",
7121
+ data: input.data ?? {},
7122
+ message: input.message,
7123
+ dedupeKey: input.dedupeKey,
7124
+ schemaVersion: input.schemaVersion ?? "1.0",
7125
+ metadata: input.metadata ?? {}
7126
+ };
7127
+ }
7128
+
7129
+ class EventsClient {
7130
+ store;
7131
+ redactors;
7132
+ transportOptions;
7133
+ constructor(options = {}) {
7134
+ this.store = options.store ?? new JsonEventsStore(options.dataDir);
7135
+ this.redactors = options.redactors ?? [];
7136
+ this.transportOptions = { fetchImpl: options.fetchImpl };
7137
+ }
7138
+ async addChannel(input) {
7139
+ const timestamp = new Date().toISOString();
7140
+ return this.store.addChannel({
7141
+ ...input,
7142
+ createdAt: input.createdAt ?? timestamp,
7143
+ updatedAt: input.updatedAt ?? timestamp
7144
+ });
7145
+ }
7146
+ async listChannels() {
7147
+ return this.store.listChannels();
7148
+ }
7149
+ async removeChannel(id) {
7150
+ return this.store.removeChannel(id);
7151
+ }
7152
+ async emit(input, options = {}) {
7153
+ const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
7154
+ if (options.dedupe !== false) {
7155
+ const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
7156
+ if (existing) {
7157
+ return { event: existing, deliveries: [], deduped: true };
7158
+ }
7159
+ }
7160
+ await this.store.appendEvent(event);
7161
+ const deliveries = options.deliver === false ? [] : await this.deliver(event);
7162
+ return { event, deliveries, deduped: false };
7163
+ }
7164
+ async listEvents() {
7165
+ return this.store.listEvents();
7166
+ }
7167
+ async listDeliveries() {
7168
+ return this.store.listDeliveries();
7169
+ }
7170
+ async deliver(event) {
7171
+ const channels = await this.store.listChannels();
7172
+ const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
7173
+ const deliveries = [];
7174
+ for (const channel of selected) {
7175
+ const eventForChannel = await this.applyRedaction(event, channel);
7176
+ const result = await this.deliverWithRetry(eventForChannel, channel);
7177
+ await this.store.appendDelivery(result);
7178
+ deliveries.push(result);
7179
+ }
7180
+ return deliveries;
7181
+ }
7182
+ async matchChannel(id, input = {}) {
7183
+ const channel = await this.store.getChannel(id);
7184
+ if (!channel)
7185
+ throw new Error(`Channel not found: ${id}`);
7186
+ const event = createEvent({
7187
+ source: input.source ?? "hasna.events",
7188
+ type: input.type ?? "events.test",
7189
+ subject: input.subject ?? id,
7190
+ severity: input.severity ?? "info",
7191
+ data: input.data ?? { test: true },
7192
+ message: input.message ?? "Hasna events test delivery",
7193
+ dedupeKey: input.dedupeKey,
7194
+ schemaVersion: input.schemaVersion,
7195
+ metadata: input.metadata,
7196
+ time: input.time,
7197
+ id: input.id
7198
+ });
7199
+ const matched = channelMatchesEvent(channel, event);
7200
+ return {
7201
+ channelId: channel.id,
7202
+ matched,
7203
+ event,
7204
+ filters: channel.filters,
7205
+ reason: matched ? undefined : channel.enabled ? "event did not match channel filters" : "channel is disabled"
7206
+ };
7207
+ }
7208
+ async testChannel(id, input = {}, options = {}) {
7209
+ const channel = await this.store.getChannel(id);
7210
+ if (!channel)
7211
+ throw new Error(`Channel not found: ${id}`);
7212
+ const match = await this.matchChannel(id, input);
7213
+ const event = match.event;
7214
+ if (options.honorFilters && !match.matched) {
7215
+ const timestamp = new Date().toISOString();
7216
+ const result2 = createDeliveryResult(event, channel, [{
7217
+ attempt: 1,
7218
+ status: "skipped",
7219
+ startedAt: timestamp,
7220
+ completedAt: timestamp,
7221
+ error: match.reason
7222
+ }]);
7223
+ result2.metadata = { reason: "filter_mismatch" };
7224
+ await this.store.appendDelivery(result2);
7225
+ return result2;
7226
+ }
7227
+ const eventForChannel = await this.applyRedaction(event, channel);
7228
+ const result = await this.deliverWithRetry(eventForChannel, channel);
7229
+ await this.store.appendDelivery(result);
7230
+ return result;
7231
+ }
7232
+ async replay(options = {}) {
7233
+ const events = (await this.store.listEvents()).filter((event) => {
7234
+ if (options.eventId && event.id !== options.eventId)
7235
+ return false;
7236
+ if (options.source && event.source !== options.source)
7237
+ return false;
7238
+ if (options.type && event.type !== options.type)
7239
+ return false;
7240
+ return true;
7241
+ });
7242
+ if (options.dryRun)
7243
+ return { events, deliveries: [] };
7244
+ const deliveries = [];
7245
+ for (const event of events) {
7246
+ deliveries.push(...await this.deliver(event));
7247
+ }
7248
+ return { events, deliveries };
7249
+ }
7250
+ async applyRedaction(event, channel) {
7251
+ let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
7252
+ for (const redactor of this.redactors) {
7253
+ next = await redactor(next, channel);
7254
+ }
7255
+ return next;
7256
+ }
7257
+ async deliverWithRetry(event, channel) {
7258
+ const policy = normalizeRetryPolicy(channel.retry);
7259
+ const attempts = [];
7260
+ for (let index = 0;index < policy.maxAttempts; index += 1) {
7261
+ const attempt = await dispatchChannel(event, channel, this.transportOptions);
7262
+ attempt.attempt = index + 1;
7263
+ if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
7264
+ attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
7265
+ }
7266
+ attempts.push(attempt);
7267
+ if (attempt.status !== "failed")
7268
+ break;
7269
+ if (attempt.nextBackoffMs)
7270
+ await Bun.sleep(attempt.nextBackoffMs);
7271
+ }
7272
+ return createDeliveryResult(event, channel, attempts);
7273
+ }
7274
+ }
7275
+ function redactPaths(event, paths, replacement = "[REDACTED]") {
7276
+ if (paths.length === 0)
7277
+ return event;
7278
+ const copy = structuredClone(event);
7279
+ for (const path of paths) {
7280
+ setPath(copy, path, replacement);
7281
+ }
7282
+ return copy;
7283
+ }
7284
+ function redactSensitiveKeys(event, replacement = "[REDACTED]") {
7285
+ return redactValue2(event, replacement);
7286
+ }
7287
+ function shouldRedactKey(key) {
7288
+ return /secret|token|password|api[_-]?key|authorization/i.test(key);
7289
+ }
7290
+ function redactValue2(value, replacement) {
7291
+ if (Array.isArray(value))
7292
+ return value.map((item) => redactValue2(item, replacement));
7293
+ if (!value || typeof value !== "object")
7294
+ return value;
7295
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
7296
+ key,
7297
+ shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
7298
+ ]));
7299
+ }
7300
+ function setPath(input, path, replacement) {
7301
+ const parts = path.split(".");
7302
+ let cursor = input;
7303
+ for (const part of parts.slice(0, -1)) {
7304
+ const next = cursor[part];
7305
+ if (!next || typeof next !== "object")
7306
+ return;
7307
+ cursor = next;
7308
+ }
7309
+ const last = parts.at(-1);
7310
+ if (last && last in cursor)
7311
+ cursor[last] = replacement;
7312
+ }
7313
+ function normalizeTime(value) {
7314
+ if (!value)
7315
+ return new Date().toISOString();
7316
+ return value instanceof Date ? value.toISOString() : value;
7317
+ }
7318
+ function normalizeRetryPolicy(policy) {
7319
+ return {
7320
+ maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
7321
+ backoffMs: Math.max(0, policy?.backoffMs ?? 250),
7322
+ multiplier: Math.max(1, policy?.multiplier ?? 2)
7323
+ };
7324
+ }
7325
+
7326
+ // src/lib/shared-events.ts
7327
+ init_database();
7328
+
7329
+ // src/db/task-lists.ts
7330
+ init_types();
7331
+ init_database();
7332
+ function rowToTaskList(row) {
7333
+ return {
7334
+ ...row,
7335
+ metadata: JSON.parse(row.metadata || "{}")
7336
+ };
7337
+ }
7338
+ function createTaskList(input, db) {
7339
+ const d = db || getDatabase();
7340
+ const id = uuid();
7341
+ const timestamp = now();
7342
+ const slug = input.slug || slugify(input.name);
7343
+ if (!input.project_id) {
7344
+ const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
7345
+ if (existing) {
7346
+ throw new Error(`Standalone task list with slug "${slug}" already exists`);
7347
+ }
7348
+ }
7349
+ d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
7350
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
7351
+ return getTaskList(id, d);
7352
+ }
7353
+ function getTaskList(id, db) {
7354
+ const d = db || getDatabase();
7355
+ const row = d.query("SELECT * FROM task_lists WHERE id = ?").get(id);
7356
+ return row ? rowToTaskList(row) : null;
7357
+ }
7358
+ function getTaskListBySlug(slug, projectId, db) {
7359
+ const d = db || getDatabase();
7360
+ let row;
7361
+ if (projectId) {
7362
+ row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id = ?").get(slug, projectId);
7363
+ } else {
7364
+ row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id IS NULL").get(slug);
7365
+ }
7366
+ return row ? rowToTaskList(row) : null;
7367
+ }
7368
+ function listTaskLists(projectId, db) {
7369
+ const d = db || getDatabase();
7370
+ if (projectId) {
7371
+ return d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(projectId).map(rowToTaskList);
7372
+ }
7373
+ return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
7374
+ }
7375
+ function updateTaskList(id, input, db) {
7376
+ const d = db || getDatabase();
7377
+ const existing = getTaskList(id, d);
7378
+ if (!existing)
7379
+ throw new TaskListNotFoundError(id);
7380
+ const sets = ["updated_at = ?"];
7381
+ const params = [now()];
7382
+ if (input.name !== undefined) {
7383
+ sets.push("name = ?");
7384
+ params.push(input.name);
7385
+ }
7386
+ if (input.description !== undefined) {
7387
+ sets.push("description = ?");
7388
+ params.push(input.description);
7389
+ }
7390
+ if (input.metadata !== undefined) {
7391
+ sets.push("metadata = ?");
7392
+ params.push(JSON.stringify(input.metadata));
7393
+ }
7394
+ params.push(id);
7395
+ d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
7396
+ return getTaskList(id, d);
7397
+ }
7398
+ function deleteTaskList(id, db) {
7399
+ const d = db || getDatabase();
7400
+ return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
7401
+ }
7402
+ function ensureTaskList(name, slug, projectId, db) {
7403
+ const d = db || getDatabase();
7404
+ const existing = getTaskListBySlug(slug, projectId, d);
7405
+ if (existing)
7406
+ return existing;
7407
+ return createTaskList({ name, slug, project_id: projectId }, d);
7408
+ }
7409
+
7410
+ // src/lib/shared-events.ts
7411
+ var SOURCE = "todos";
7412
+ function taskEventData(task, extra = {}) {
7413
+ return {
7414
+ id: task.id,
7415
+ task_id: task.id,
7416
+ short_id: task.short_id,
7417
+ title: task.title,
7418
+ description: task.description,
7419
+ status: task.status,
7420
+ priority: task.priority,
7421
+ project_id: task.project_id,
7422
+ parent_id: task.parent_id,
7423
+ plan_id: task.plan_id,
7424
+ task_list_id: task.task_list_id,
7425
+ agent_id: task.agent_id,
7426
+ assigned_to: task.assigned_to,
7427
+ session_id: task.session_id,
7428
+ working_dir: task.working_dir,
7429
+ tags: task.tags,
7430
+ metadata: task.metadata,
7431
+ version: task.version,
7432
+ created_at: task.created_at,
7433
+ updated_at: task.updated_at,
7434
+ started_at: task.started_at,
7435
+ completed_at: task.completed_at,
7436
+ due_at: task.due_at,
7437
+ ...extra
7438
+ };
7439
+ }
7440
+ function taskEventMetadata(task) {
7441
+ const metadata = {
7442
+ package: "@hasna/todos",
7443
+ todos_event_schema_version: 1,
7444
+ task_id: task.id,
7445
+ task_short_id: task.short_id,
7446
+ project_id: task.project_id,
7447
+ task_list_id: task.task_list_id,
7448
+ working_dir: task.working_dir
7449
+ };
7450
+ try {
7451
+ const project = task.project_id ? getProject(task.project_id) : null;
7452
+ const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
7453
+ if (project) {
7454
+ metadata.project_id = project.id;
7455
+ metadata.project_name = project.name;
7456
+ metadata.project_path = projectPath;
7457
+ metadata.project_canonical_path = project.path;
7458
+ metadata.project_default_task_list_slug = project.task_list_id;
7459
+ metadata.root_project_id = inferRootProjectId(project);
7460
+ } else if (projectPath) {
7461
+ metadata.project_path = projectPath;
7462
+ metadata.project_canonical_path = projectPath;
7463
+ }
7464
+ if (projectPath) {
7465
+ metadata.project_kind = classifyProjectKind(projectPath);
7466
+ metadata.project_is_worktree = isWorktreePath(projectPath);
7467
+ if (typeof task.metadata.route_enabled === "boolean") {
7468
+ metadata.route_enabled = task.metadata.route_enabled;
7469
+ }
7470
+ metadata.working_dir = task.working_dir ?? projectPath;
7471
+ }
7472
+ const taskList = task.task_list_id ? getTaskList(task.task_list_id) ?? (project ? getTaskListBySlug(task.task_list_id, project.id) : null) : project?.task_list_id ? getTaskListBySlug(project.task_list_id, project.id) : null;
7473
+ if (taskList) {
7474
+ metadata.task_list_id = taskList.id;
7475
+ metadata.task_list_slug = taskList.slug;
7476
+ metadata.task_list_name = taskList.name;
7477
+ metadata.task_list_project_id = taskList.project_id;
7478
+ metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
7479
+ }
7480
+ } catch {}
7481
+ return metadata;
7482
+ }
7483
+ function classifyProjectKind(path) {
7484
+ return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
7485
+ }
7486
+ function isWorktreePath(path) {
7487
+ return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
7488
+ }
7489
+ function inferRootProjectId(project) {
7490
+ return isWorktreePath(project.path) ? null : project.id;
7491
+ }
7492
+ function readMachineLocalPath(project) {
7493
+ const machineId = process.env["TODOS_MACHINE_ID"];
7494
+ if (!machineId)
7495
+ return null;
7496
+ try {
7497
+ const row = getDatabase().query("SELECT path FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(project.id, machineId);
7498
+ return row?.path ?? null;
7499
+ } catch {
7500
+ return null;
7501
+ }
7502
+ }
7503
+ async function emitSharedTaskEvent(input) {
7504
+ const data = taskEventData(input.task, input.data);
7505
+ await new EventsClient().emit({
7506
+ source: SOURCE,
7507
+ type: input.type,
7508
+ subject: input.task.id,
7509
+ severity: input.severity ?? "info",
7510
+ message: input.message ?? `${input.type}: ${input.task.title}`,
7511
+ data,
7512
+ dedupeKey: input.dedupeKey ?? `${input.type}:${input.task.id}:${input.task.version}`,
7513
+ metadata: taskEventMetadata(input.task)
7514
+ }, { deliver: true, dedupe: true });
7515
+ }
7516
+ function emitSharedTaskEventQuiet(input) {
7517
+ emitSharedTaskEvent(input).catch(() => {
7518
+ return;
7519
+ });
7520
+ }
7521
+
6714
7522
  // src/db/audit.ts
6715
7523
  init_database();
6716
7524
  function logTaskChange(taskId, action, field2, oldValue, newValue, agentId, db) {
@@ -6973,7 +7781,7 @@ async function deliverWebhook(wh, event, body, attempt, db) {
6973
7781
  activeDeliveries--;
6974
7782
  }
6975
7783
  }
6976
- async function dispatchWebhook(event, payload, db) {
7784
+ async function dispatchWebhook2(event, payload, db) {
6977
7785
  const d = db || getDatabase();
6978
7786
  const webhooks = listWebhooks(d).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
6979
7787
  const payloadObj = typeof payload === "object" && payload !== null ? payload : {};
@@ -7127,7 +7935,10 @@ function createTask(input, db) {
7127
7935
  insertTaskTags(id, tags, d);
7128
7936
  }
7129
7937
  const task = getTask(id, d);
7130
- 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(() => {});
7938
+ const payload = taskEventData(task);
7939
+ dispatchWebhook2("task.created", payload, d).catch(() => {});
7940
+ emitLocalEventHooksQuiet({ type: "task.created", payload });
7941
+ emitSharedTaskEventQuiet({ type: "task.created", task });
7131
7942
  return task;
7132
7943
  }
7133
7944
  function getTask(id, db) {
@@ -7471,18 +8282,7 @@ function updateTask(id, input, db) {
7471
8282
  logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
7472
8283
  if (input.approved_by !== undefined)
7473
8284
  logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
7474
- if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
7475
- dispatchWebhook("task.assigned", { id, assigned_to: input.assigned_to, title: task.title }, d).catch(() => {});
7476
- emitLocalEventHooksQuiet({ type: "task.assigned", payload: { id, assigned_to: input.assigned_to, title: task.title } });
7477
- }
7478
- if (input.status !== undefined && input.status !== task.status) {
7479
- dispatchWebhook("task.status_changed", { id, old_status: task.status, new_status: input.status, title: task.title }, d).catch(() => {});
7480
- emitLocalEventHooksQuiet({ type: "task.status_changed", payload: { id, old_status: task.status, new_status: input.status, title: task.title } });
7481
- }
7482
- if (input.approved_by !== undefined) {
7483
- emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
7484
- }
7485
- return {
8285
+ const updatedTask = {
7486
8286
  ...task,
7487
8287
  ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
7488
8288
  tags: input.tags ?? task.tags,
@@ -7500,6 +8300,22 @@ function updateTask(id, input, db) {
7500
8300
  approved_by: input.approved_by ?? task.approved_by,
7501
8301
  approved_at: input.approved_by ? timestamp : task.approved_at
7502
8302
  };
8303
+ if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
8304
+ const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
8305
+ dispatchWebhook2("task.assigned", payload, d).catch(() => {});
8306
+ emitLocalEventHooksQuiet({ type: "task.assigned", payload });
8307
+ emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to } });
8308
+ }
8309
+ if (input.status !== undefined && input.status !== task.status) {
8310
+ const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
8311
+ dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
8312
+ emitLocalEventHooksQuiet({ type: "task.status_changed", payload });
8313
+ emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status } });
8314
+ }
8315
+ if (input.approved_by !== undefined) {
8316
+ emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
8317
+ }
8318
+ return updatedTask;
7503
8319
  }
7504
8320
  function deleteTask(id, db) {
7505
8321
  const d = db || getDatabase();
@@ -8232,9 +9048,12 @@ function startTask(id, agentId, db) {
8232
9048
  throw new Error(`Task ${id} could not be started because it changed during claim`);
8233
9049
  }
8234
9050
  logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
8235
- dispatchWebhook("task.started", { id, agent_id: agentId, title: task.title }, d).catch(() => {});
8236
- emitLocalEventHooksQuiet({ type: "task.started", payload: { id, agent_id: agentId, title: task.title } });
8237
- 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 };
9051
+ 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 };
9052
+ const payload = taskEventData(startedTask, { agent_id: agentId });
9053
+ dispatchWebhook2("task.started", payload, d).catch(() => {});
9054
+ emitLocalEventHooksQuiet({ type: "task.started", payload });
9055
+ emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId } });
9056
+ return startedTask;
8238
9057
  }
8239
9058
  function completeTask(id, agentId, db, options) {
8240
9059
  const d = db || getDatabase();
@@ -8270,8 +9089,21 @@ function completeTask(id, agentId, db, options) {
8270
9089
  });
8271
9090
  tx();
8272
9091
  logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
8273
- dispatchWebhook("task.completed", { id, agent_id: agentId, title: task.title, completed_at: timestamp }, d).catch(() => {});
8274
- emitLocalEventHooksQuiet({ type: "task.completed", payload: { id, agent_id: agentId, title: task.title, completed_at: timestamp } });
9092
+ const completedTaskForEvent = {
9093
+ ...task,
9094
+ status: "completed",
9095
+ locked_by: null,
9096
+ locked_at: null,
9097
+ completed_at: timestamp,
9098
+ confidence,
9099
+ version: task.version + 1,
9100
+ updated_at: timestamp,
9101
+ metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
9102
+ };
9103
+ const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
9104
+ dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
9105
+ emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload });
9106
+ emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp } });
8275
9107
  let spawnedTask = null;
8276
9108
  if (task.recurrence_rule && !options?.skip_recurrence) {
8277
9109
  spawnedTask = spawnNextRecurrence(task, d, timestamp);
@@ -8312,8 +9144,12 @@ function completeTask(id, agentId, db, options) {
8312
9144
  if (unblockedDeps.length > 0) {
8313
9145
  meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
8314
9146
  for (const dep of unblockedDeps) {
8315
- dispatchWebhook("task.unblocked", { id: dep.id, unblocked_by: id, title: dep.title }, d).catch(() => {});
8316
- emitLocalEventHooksQuiet({ type: "task.unblocked", payload: { id: dep.id, unblocked_by: id, title: dep.title } });
9147
+ const depTask = getTask(dep.id, d);
9148
+ const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
9149
+ dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
9150
+ emitLocalEventHooksQuiet({ type: "task.unblocked", payload });
9151
+ if (depTask)
9152
+ emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id } });
8317
9153
  }
8318
9154
  }
8319
9155
  return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
@@ -8499,9 +9335,6 @@ function failTask(id, agentId, reason, options, db) {
8499
9335
  const timestamp = now();
8500
9336
  d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
8501
9337
  WHERE id = ?`, [JSON.stringify(meta), timestamp, id]);
8502
- logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
8503
- dispatchWebhook("task.failed", { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title }, d).catch(() => {});
8504
- emitLocalEventHooksQuiet({ type: "task.failed", payload: { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title } });
8505
9338
  const failedTask = {
8506
9339
  ...task,
8507
9340
  status: "failed",
@@ -8511,6 +9344,11 @@ function failTask(id, agentId, reason, options, db) {
8511
9344
  version: task.version + 1,
8512
9345
  updated_at: timestamp
8513
9346
  };
9347
+ logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
9348
+ const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
9349
+ dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
9350
+ emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload });
9351
+ emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning" });
8514
9352
  let retryTask;
8515
9353
  if (options?.retry) {
8516
9354
  const retryCount = (task.retry_count || 0) + 1;
@@ -8585,9 +9423,12 @@ function stealTask(agentId, opts, db) {
8585
9423
  return null;
8586
9424
  logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
8587
9425
  logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
8588
- dispatchWebhook("task.assigned", { id: target.id, agent_id: agentId, title: target.title, stolen_from: target.assigned_to }, d).catch(() => {});
8589
- emitLocalEventHooksQuiet({ type: "task.assigned", payload: { id: target.id, agent_id: agentId, title: target.title, stolen_from: target.assigned_to } });
8590
- return { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
9426
+ const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
9427
+ const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
9428
+ dispatchWebhook2("task.assigned", payload, d).catch(() => {});
9429
+ emitLocalEventHooksQuiet({ type: "task.assigned", payload });
9430
+ emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to } });
9431
+ return stolenTask;
8591
9432
  }
8592
9433
  function claimOrSteal(agentId, filters, db) {
8593
9434
  const d = db || getDatabase();
@@ -9646,8 +10487,8 @@ init_database();
9646
10487
  init_database();
9647
10488
  init_redaction();
9648
10489
  import { createHash as createHash2 } from "crypto";
9649
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
9650
- import { basename, dirname as dirname5, join as join5, resolve as resolve6 } from "path";
10490
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
10491
+ import { basename, dirname as dirname5, join as join6, resolve as resolve6 } from "path";
9651
10492
  import { tmpdir } from "os";
9652
10493
  function isInMemoryDb2(path) {
9653
10494
  return path === ":memory:" || path.startsWith("file::memory:");
@@ -9659,15 +10500,15 @@ function artifactStoreRoot() {
9659
10500
  return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
9660
10501
  const dbPath = getDatabasePath();
9661
10502
  if (isInMemoryDb2(dbPath))
9662
- return join5(tmpdir(), "hasna-todos-artifacts");
9663
- return join5(dirname5(resolve6(dbPath)), "artifacts");
10503
+ return join6(tmpdir(), "hasna-todos-artifacts");
10504
+ return join6(dirname5(resolve6(dbPath)), "artifacts");
9664
10505
  }
9665
10506
  function artifactStorePath(relativePath) {
9666
10507
  const normalized = relativePath.replace(/\\/g, "/");
9667
10508
  if (normalized.includes("..") || normalized.startsWith("/") || normalized.length === 0) {
9668
10509
  throw new Error("Invalid artifact store path");
9669
10510
  }
9670
- return join5(artifactStoreRoot(), normalized);
10511
+ return join6(artifactStoreRoot(), normalized);
9671
10512
  }
9672
10513
  function sha256(buffer) {
9673
10514
  return createHash2("sha256").update(buffer).digest("hex");
@@ -9708,7 +10549,7 @@ function mediaTypeFor(path, textLike) {
9708
10549
  }
9709
10550
  function storeArtifactContent(input) {
9710
10551
  const sourcePath = resolve6(input.path);
9711
- if (!existsSync7(sourcePath))
10552
+ if (!existsSync8(sourcePath))
9712
10553
  return null;
9713
10554
  const sourceStat = statSync2(sourcePath);
9714
10555
  if (!sourceStat.isFile())
@@ -9725,9 +10566,9 @@ function storeArtifactContent(input) {
9725
10566
  redactionStatus = "redacted";
9726
10567
  }
9727
10568
  const storedSha = sha256(storedBuffer);
9728
- const relativePath = join5("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
10569
+ const relativePath = join6("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
9729
10570
  const destination = artifactStorePath(relativePath);
9730
- if (!existsSync7(destination)) {
10571
+ if (!existsSync8(destination)) {
9731
10572
  mkdirSync4(dirname5(destination), { recursive: true });
9732
10573
  writeFileSync2(destination, storedBuffer);
9733
10574
  }
@@ -9787,7 +10628,7 @@ function verifyStoredArtifact(input) {
9787
10628
  };
9788
10629
  }
9789
10630
  const storedPath = artifactStorePath(store.relative_path);
9790
- if (!existsSync7(storedPath)) {
10631
+ if (!existsSync8(storedPath)) {
9791
10632
  return {
9792
10633
  id: input.id,
9793
10634
  path: input.path,
@@ -9868,15 +10709,15 @@ function getArtifactStoreRoot(dbPath) {
9868
10709
  return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
9869
10710
  const path = dbPath ?? getDatabasePath();
9870
10711
  if (isInMemoryDb2(path))
9871
- return join5(tmpdir(), "hasna-todos-artifacts");
9872
- return join5(dirname5(resolve6(path)), "artifacts");
10712
+ return join6(tmpdir(), "hasna-todos-artifacts");
10713
+ return join6(dirname5(resolve6(path)), "artifacts");
9873
10714
  }
9874
10715
  function computeContentHash(path) {
9875
10716
  return sha256(readFileSync4(resolve6(path)));
9876
10717
  }
9877
10718
  function storeArtifactFile(input) {
9878
10719
  const sourcePath = resolve6(input.sourcePath);
9879
- if (!existsSync7(sourcePath)) {
10720
+ if (!existsSync8(sourcePath)) {
9880
10721
  throw new Error(`Source file not found: ${input.sourcePath}`);
9881
10722
  }
9882
10723
  if (!statSync2(sourcePath).isFile()) {
@@ -9889,7 +10730,7 @@ function storeArtifactFile(input) {
9889
10730
  let localPath = sourcePath;
9890
10731
  if (storageMode === "copy") {
9891
10732
  const fileName = input.name && input.name.trim().length > 0 ? basename(input.name) : basename(sourcePath);
9892
- const destination = join5(getArtifactStoreRoot(input.dbPath), input.artifactId, fileName);
10733
+ const destination = join6(getArtifactStoreRoot(input.dbPath), input.artifactId, fileName);
9893
10734
  mkdirSync4(dirname5(destination), { recursive: true });
9894
10735
  writeFileSync2(destination, buffer);
9895
10736
  localPath = destination;
@@ -9899,7 +10740,7 @@ function storeArtifactFile(input) {
9899
10740
  function deleteStoredArtifactFile(localPath, storageMode, _dbPath2) {
9900
10741
  if (storageMode === "reference")
9901
10742
  return false;
9902
- if (!localPath || !existsSync7(localPath))
10743
+ if (!localPath || !existsSync8(localPath))
9903
10744
  return false;
9904
10745
  rmSync(localPath, { force: true });
9905
10746
  try {
@@ -9911,8 +10752,8 @@ function isArtifactExpired(deletedAt, policy = {}) {
9911
10752
  if (!deletedAt)
9912
10753
  return false;
9913
10754
  const retentionDays = policy.deleted_retention_days ?? DEFAULT_DELETED_RETENTION_DAYS;
9914
- const now2 = policy.now ?? new Date;
9915
- const ageMs = now2.getTime() - new Date(deletedAt).getTime();
10755
+ const now3 = policy.now ?? new Date;
10756
+ const ageMs = now3.getTime() - new Date(deletedAt).getTime();
9916
10757
  return ageMs > retentionDays * 24 * 60 * 60 * 1000;
9917
10758
  }
9918
10759
  function buildArtifactExportManifest(artifacts, dbPath) {
@@ -10987,7 +11828,7 @@ function rowToTask2(row) {
10987
11828
  requires_approval: Boolean(row.requires_approval)
10988
11829
  };
10989
11830
  }
10990
- function rowToTaskList(row) {
11831
+ function rowToTaskList2(row) {
10991
11832
  return { ...row, metadata: parseJsonObject3(row.metadata) };
10992
11833
  }
10993
11834
  function rowWithMetadata(row) {
@@ -11023,7 +11864,7 @@ function createLocalBridgeBundle(options = {}, db) {
11023
11864
  const project = options.project_id ? d.query("SELECT * FROM projects WHERE id = ?").get(options.project_id) : null;
11024
11865
  const data = redactValue({
11025
11866
  projects: options.project_id ? project ? [project] : [] : d.query("SELECT * FROM projects ORDER BY name").all(),
11026
- task_lists: (options.project_id ? d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(options.project_id) : d.query("SELECT * FROM task_lists ORDER BY name").all()).map(rowToTaskList),
11867
+ task_lists: (options.project_id ? d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(options.project_id) : d.query("SELECT * FROM task_lists ORDER BY name").all()).map(rowToTaskList2),
11027
11868
  plans: options.project_id ? d.query("SELECT * FROM plans WHERE project_id = ? ORDER BY created_at").all(options.project_id) : d.query("SELECT * FROM plans ORDER BY created_at").all(),
11028
11869
  tasks: queryByTaskIds(d, "SELECT * FROM tasks WHERE id IN (__TASK_IDS__) ORDER BY created_at", taskIds).map(rowToTask2),
11029
11870
  task_dependencies: queryByTaskIds(d, "SELECT task_id, depends_on, external_project_id, external_task_id FROM task_dependencies WHERE task_id IN (__TASK_IDS__) ORDER BY task_id, depends_on", taskIds),
@@ -11678,7 +12519,7 @@ function writeOnboardingFixtureFiles(directory) {
11678
12519
  mkdirSync5(directory, { recursive: true });
11679
12520
  const files = [];
11680
12521
  for (const fixture of allFixtures()) {
11681
- const path = join6(directory, `${fixture.summary.name}.bridge.json`);
12522
+ const path = join7(directory, `${fixture.summary.name}.bridge.json`);
11682
12523
  writeFileSync3(path, `${JSON.stringify(fixture.bundle, null, 2)}
11683
12524
  `, "utf-8");
11684
12525
  files.push(path);
@@ -12504,7 +13345,7 @@ function renderLocalSnapshotMarkdown(snapshot) {
12504
13345
  }
12505
13346
  // src/lib/sdk-integration-fixtures.ts
12506
13347
  import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync5 } from "fs";
12507
- import { join as join7 } from "path";
13348
+ import { join as join8 } from "path";
12508
13349
 
12509
13350
  // src/cli-mcp-parity.ts
12510
13351
  function source4(version) {
@@ -13814,7 +14655,7 @@ function limits(input) {
13814
14655
  stale_after_hours: clamp(input.stale_after_hours, DEFAULT_LIMITS.stale_after_hours, 24 * 365)
13815
14656
  };
13816
14657
  }
13817
- function truncate(value, max) {
14658
+ function truncate2(value, max) {
13818
14659
  if (!value)
13819
14660
  return value ?? null;
13820
14661
  const redacted = redactEvidenceText(value);
@@ -13835,9 +14676,9 @@ function acceptanceCriteria(task2, maxText) {
13835
14676
  const metadata = task2.metadata || {};
13836
14677
  const raw = metadata["acceptance_criteria"] ?? metadata["acceptanceCriteria"] ?? metadata["criteria"];
13837
14678
  if (Array.isArray(raw))
13838
- return raw.map((item) => truncate(String(item), maxText)).filter((item) => Boolean(item));
14679
+ return raw.map((item) => truncate2(String(item), maxText)).filter((item) => Boolean(item));
13839
14680
  if (typeof raw === "string") {
13840
- return raw.split(/\r?\n/).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).map((line) => truncate(line, maxText)).filter((item) => Boolean(item));
14681
+ return raw.split(/\r?\n/).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).map((line) => truncate2(line, maxText)).filter((item) => Boolean(item));
13841
14682
  }
13842
14683
  return [];
13843
14684
  }
@@ -13860,7 +14701,7 @@ function addFile(files, path, source5, base) {
13860
14701
  path,
13861
14702
  status: base?.status || "active",
13862
14703
  agent_id: base?.agent_id ?? null,
13863
- note: truncate(base?.note, 240),
14704
+ note: truncate2(base?.note, 240),
13864
14705
  updated_at: base?.updated_at || "",
13865
14706
  sources: [source5]
13866
14707
  });
@@ -13918,7 +14759,7 @@ function estimateTokens(value) {
13918
14759
  return Math.max(1, Math.ceil((text || "").length / 4));
13919
14760
  }
13920
14761
  function summarizeStrings(values, maxChars) {
13921
- return truncate(values.filter(Boolean).join("; "), maxChars) || "No local details were available before this section was omitted.";
14762
+ return truncate2(values.filter(Boolean).join("; "), maxChars) || "No local details were available before this section was omitted.";
13922
14763
  }
13923
14764
  function summarizeSection(pack, section, maxChars) {
13924
14765
  if (section === "project")
@@ -14109,8 +14950,8 @@ function createAgentContextPack(input, db) {
14109
14950
  ...taskFiles.map((file) => file.updated_at)
14110
14951
  ], task2.updated_at);
14111
14952
  const warnings = [];
14112
- const now2 = input.now ? new Date(input.now) : new Date;
14113
- if (Date.parse(task2.updated_at) < now2.getTime() - limit.stale_after_hours * 60 * 60 * 1000) {
14953
+ const now3 = input.now ? new Date(input.now) : new Date;
14954
+ if (Date.parse(task2.updated_at) < now3.getTime() - limit.stale_after_hours * 60 * 60 * 1000) {
14114
14955
  warnings.push(`task state is older than ${limit.stale_after_hours} hours`);
14115
14956
  }
14116
14957
  if (comments.length > recentComments.length)
@@ -14125,7 +14966,7 @@ function createAgentContextPack(input, db) {
14125
14966
  id: task2.id,
14126
14967
  short_id: task2.short_id,
14127
14968
  title: redactEvidenceText(task2.title),
14128
- description: truncate(task2.description, limit.max_text_chars),
14969
+ description: truncate2(task2.description, limit.max_text_chars),
14129
14970
  status: task2.status,
14130
14971
  priority: task2.priority,
14131
14972
  assigned_to: task2.assigned_to,
@@ -14149,7 +14990,7 @@ function createAgentContextPack(input, db) {
14149
14990
  plan: plan ? {
14150
14991
  id: plan.id,
14151
14992
  name: plan.name,
14152
- description: truncate(plan.description, limit.max_text_chars),
14993
+ description: truncate2(plan.description, limit.max_text_chars),
14153
14994
  status: plan.status,
14154
14995
  agent_id: plan.agent_id,
14155
14996
  tasks: planTasks.slice(0, limit.plan_task_limit).map(taskSummary).filter((item) => Boolean(item)),
@@ -14168,7 +15009,7 @@ function createAgentContextPack(input, db) {
14168
15009
  type: comment.type,
14169
15010
  progress_pct: comment.progress_pct,
14170
15011
  created_at: comment.created_at,
14171
- content: truncate(comment.content, limit.max_text_chars) || ""
15012
+ content: truncate2(comment.content, limit.max_text_chars) || ""
14172
15013
  })),
14173
15014
  omitted: Math.max(0, comments.length - recentComments.length)
14174
15015
  },
@@ -14176,7 +15017,7 @@ function createAgentContextPack(input, db) {
14176
15017
  traceability: {
14177
15018
  commits: traceability.commits.map((commit) => ({
14178
15019
  sha: commit.sha,
14179
- message: truncate(commit.message, 240),
15020
+ message: truncate2(commit.message, 240),
14180
15021
  files_changed: commit.files_changed,
14181
15022
  committed_at: commit.committed_at
14182
15023
  })),
@@ -14184,7 +15025,7 @@ function createAgentContextPack(input, db) {
14184
15025
  verifications: verifications.map((verification) => ({
14185
15026
  command: verification.command,
14186
15027
  status: verification.status,
14187
- output_summary: truncate(verification.output_summary, limit.max_text_chars),
15028
+ output_summary: truncate2(verification.output_summary, limit.max_text_chars),
14188
15029
  artifact_path: verification.artifact_path,
14189
15030
  run_at: verification.run_at
14190
15031
  })),
@@ -14195,14 +15036,14 @@ function createAgentContextPack(input, db) {
14195
15036
  id: ledger.run.id,
14196
15037
  title: ledger.run.title,
14197
15038
  status: ledger.run.status,
14198
- summary: truncate(ledger.run.summary, limit.max_text_chars),
15039
+ summary: truncate2(ledger.run.summary, limit.max_text_chars),
14199
15040
  agent_id: ledger.run.agent_id,
14200
15041
  started_at: ledger.run.started_at,
14201
15042
  completed_at: ledger.run.completed_at,
14202
- events: ledger.events.map((event) => ({ event_type: event.event_type, message: truncate(event.message, 500), created_at: event.created_at })),
14203
- 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 })),
14204
- files: ledger.files.map((file) => ({ path: file.path, status: file.status, note: truncate(file.note, 240) })),
14205
- artifacts: ledger.artifacts.map((artifact) => ({ path: artifact.path, artifact_type: artifact.artifact_type, description: truncate(artifact.description, 240), sha256: artifact.sha256 }))
15043
+ events: ledger.events.map((event) => ({ event_type: event.event_type, message: truncate2(event.message, 500), created_at: event.created_at })),
15044
+ 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 })),
15045
+ files: ledger.files.map((file) => ({ path: file.path, status: file.status, note: truncate2(file.note, 240) })),
15046
+ artifacts: ledger.artifacts.map((artifact) => ({ path: artifact.path, artifact_type: artifact.artifact_type, description: truncate2(artifact.description, 240), sha256: artifact.sha256 }))
14206
15047
  })),
14207
15048
  omitted: Math.max(0, runs.length - selectedRuns.length)
14208
15049
  },
@@ -14307,7 +15148,7 @@ function renderAgentContextPackCompactMarkdown(pack) {
14307
15148
  const lines = [
14308
15149
  `# Context: ${pack.task.title}`,
14309
15150
  `${pack.task.status} | ${pack.task.priority} | ${pack.task.short_id || pack.task.id.slice(0, 8)}`,
14310
- pack.task.description ? truncate(pack.task.description, Math.min(pack.limits.summary_char_limit, 700)) : null,
15151
+ pack.task.description ? truncate2(pack.task.description, Math.min(pack.limits.summary_char_limit, 700)) : null,
14311
15152
  "",
14312
15153
  "## Must Know",
14313
15154
  bullet([
@@ -14453,7 +15294,7 @@ function writeSdkIntegrationFixtures(directory, options = {}) {
14453
15294
  ];
14454
15295
  const written = [];
14455
15296
  for (const [name, payload] of files) {
14456
- const file = join7(directory, name);
15297
+ const file = join8(directory, name);
14457
15298
  writeFileSync5(file, `${JSON.stringify(payload, null, 2)}
14458
15299
  `, "utf-8");
14459
15300
  written.push(file);
@@ -15170,7 +16011,7 @@ function createRoadmap(input) {
15170
16011
  if (!name)
15171
16012
  throw new Error("Roadmap name is required");
15172
16013
  const store = readStore();
15173
- const now2 = timestamp();
16014
+ const now3 = timestamp();
15174
16015
  const roadmap = {
15175
16016
  id: newId("roadmap"),
15176
16017
  name,
@@ -15181,8 +16022,8 @@ function createRoadmap(input) {
15181
16022
  agent_id: cleanString(input.agent_id),
15182
16023
  release: cleanString(input.release),
15183
16024
  milestone_ids: [],
15184
- created_at: now2,
15185
- updated_at: now2
16025
+ created_at: now3,
16026
+ updated_at: now3
15186
16027
  };
15187
16028
  store.roadmaps[roadmap.id] = roadmap;
15188
16029
  writeStore(store);
@@ -15242,7 +16083,7 @@ function createMilestone(input) {
15242
16083
  const title = input.title.trim();
15243
16084
  if (!title)
15244
16085
  throw new Error("Milestone title is required");
15245
- const now2 = timestamp();
16086
+ const now3 = timestamp();
15246
16087
  const milestone = {
15247
16088
  id: newId("milestone"),
15248
16089
  roadmap_id: roadmapId,
@@ -15257,11 +16098,11 @@ function createMilestone(input) {
15257
16098
  run_ids: cleanList3(input.run_ids),
15258
16099
  release: cleanString(input.release ?? roadmap.release ?? undefined),
15259
16100
  tags: cleanList3(input.tags),
15260
- created_at: now2,
15261
- updated_at: now2
16101
+ created_at: now3,
16102
+ updated_at: now3
15262
16103
  };
15263
16104
  store.milestones[milestone.id] = milestone;
15264
- store.roadmaps[roadmapId] = { ...roadmap, milestone_ids: cleanList3([...roadmap.milestone_ids, milestone.id]), updated_at: now2 };
16105
+ store.roadmaps[roadmapId] = { ...roadmap, milestone_ids: cleanList3([...roadmap.milestone_ids, milestone.id]), updated_at: now3 };
15265
16106
  writeStore(store);
15266
16107
  return milestone;
15267
16108
  }
@@ -15321,7 +16162,7 @@ function upsertReleaseGroup(input) {
15321
16162
  throw new Error("Release group name is required");
15322
16163
  const key = releaseKey(roadmapId, name);
15323
16164
  const existing = store.releases[key];
15324
- const now2 = timestamp();
16165
+ const now3 = timestamp();
15325
16166
  const release = {
15326
16167
  name,
15327
16168
  version: input.version === undefined ? existing?.version ?? null : cleanString(input.version),
@@ -15332,8 +16173,8 @@ function upsertReleaseGroup(input) {
15332
16173
  plan_ids: input.plan_ids === undefined ? existing?.plan_ids ?? [] : cleanList3(input.plan_ids),
15333
16174
  run_ids: input.run_ids === undefined ? existing?.run_ids ?? [] : cleanList3(input.run_ids),
15334
16175
  notes: input.notes === undefined ? existing?.notes ?? null : cleanString(input.notes),
15335
- created_at: existing?.created_at ?? now2,
15336
- updated_at: now2
16176
+ created_at: existing?.created_at ?? now3,
16177
+ updated_at: now3
15337
16178
  };
15338
16179
  store.releases[key] = release;
15339
16180
  writeStore(store);
@@ -15724,7 +16565,7 @@ function renderLocalAuditLedgerMarkdown(ledger) {
15724
16565
  init_migrations();
15725
16566
  init_schema();
15726
16567
  import { readFileSync as readFileSync6 } from "fs";
15727
- import { join as join8, resolve as resolve8 } from "path";
16568
+ import { join as join9, resolve as resolve8 } from "path";
15728
16569
  import { Database as Database2 } from "bun:sqlite";
15729
16570
  var LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION = 1;
15730
16571
  var EXPECTED_PACKAGE_NAME = "@hasna/todos";
@@ -15770,7 +16611,7 @@ function warn(id, message, details) {
15770
16611
  return { id, status: "warning", message, details };
15771
16612
  }
15772
16613
  function readPackageJson(root) {
15773
- return JSON.parse(readFileSync6(join8(root, "package.json"), "utf8"));
16614
+ return JSON.parse(readFileSync6(join9(root, "package.json"), "utf8"));
15774
16615
  }
15775
16616
  function sortedKeys(value) {
15776
16617
  return Object.keys(value ?? {}).sort((left, right) => left.localeCompare(right));
@@ -17182,6 +18023,470 @@ function importExternalIssues(input, db) {
17182
18023
  ]
17183
18024
  };
17184
18025
  }
18026
+ // src/lib/tester-issue-reports.ts
18027
+ init_database();
18028
+ import { createHash as createHash7 } from "crypto";
18029
+ init_redaction();
18030
+ var TESTERS_ISSUE_REPORT_SCHEMA_VERSION = "testers.issue_report.v1";
18031
+ var TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION = "todos.tester_issue_report_result.v1";
18032
+ var TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION = "todos.tester_issue_report_batch_result.v1";
18033
+ var PRIORITIES3 = ["low", "medium", "high", "critical"];
18034
+ var SEVERITIES = new Set(PRIORITIES3);
18035
+ var KINDS = new Set([
18036
+ "assertion_failure",
18037
+ "runtime_error",
18038
+ "console_error",
18039
+ "network_error",
18040
+ "visual_regression",
18041
+ "accessibility",
18042
+ "performance",
18043
+ "broken_link",
18044
+ "security",
18045
+ "unknown"
18046
+ ]);
18047
+ function asObject3(value) {
18048
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
18049
+ }
18050
+ function asString3(value) {
18051
+ if (typeof value === "string" && value.trim())
18052
+ return value.trim();
18053
+ if (typeof value === "number" && Number.isFinite(value))
18054
+ return String(value);
18055
+ return null;
18056
+ }
18057
+ function stringArray(value, limit = 20) {
18058
+ if (!Array.isArray(value))
18059
+ return [];
18060
+ return [...new Set(value.map((item) => asString3(item)).filter((item) => Boolean(item)))].slice(0, limit);
18061
+ }
18062
+ function objectArray(value, limit = 20) {
18063
+ if (!Array.isArray(value))
18064
+ return [];
18065
+ return value.map(asObject3).filter((item) => Object.keys(item).length > 0).slice(0, limit);
18066
+ }
18067
+ function cleanKey(value) {
18068
+ return value.toLowerCase().trim().replace(/[^a-z0-9._:-]+/g, "-").replace(/^-+|-+$/g, "");
18069
+ }
18070
+ function truncate3(value, max) {
18071
+ if (!value)
18072
+ return;
18073
+ const redacted = redactEvidenceText(value).trim();
18074
+ if (!redacted)
18075
+ return;
18076
+ return redacted.length > max ? `${redacted.slice(0, max - 3)}...` : redacted;
18077
+ }
18078
+ function normalizeText2(value) {
18079
+ return (value || "").toLowerCase().replace(/https?:\/\/\S+/g, " ").replace(/[`"'()[\]{}.,:;!?/#\\_-]+/g, " ").replace(/\s+/g, " ").trim();
18080
+ }
18081
+ function normalizeUrlPattern(value) {
18082
+ if (!value)
18083
+ return "";
18084
+ try {
18085
+ const url = new URL(value);
18086
+ return `${url.origin.toLowerCase()}${url.pathname.replace(/\/+$/, "") || "/"}`;
18087
+ } catch {
18088
+ return value.split(/[?#]/, 1)[0].replace(/\/+$/, "").toLowerCase();
18089
+ }
18090
+ }
18091
+ function normalizeKind(value) {
18092
+ const raw = cleanKey(asString3(value) || "unknown").replace(/-/g, "_");
18093
+ return KINDS.has(raw) ? raw : raw || "unknown";
18094
+ }
18095
+ function normalizeSeverity(value, fallback) {
18096
+ const raw = cleanKey(asString3(value) || fallback);
18097
+ if (SEVERITIES.has(raw))
18098
+ return raw;
18099
+ if (/^(p0|blocker|urgent|highest)$/.test(raw))
18100
+ return "critical";
18101
+ if (/^(p1|major)$/.test(raw))
18102
+ return "high";
18103
+ if (/^(p3|minor|info)$/.test(raw))
18104
+ return "low";
18105
+ return fallback;
18106
+ }
18107
+ function reportSource(input) {
18108
+ const source6 = asObject3(input["source"]);
18109
+ const normalized = {
18110
+ tool: asString3(source6["tool"]) || asString3(input["tool"]) || "testers",
18111
+ run_id: asString3(source6["run_id"]) || asString3(input["run_id"]) || undefined,
18112
+ result_id: asString3(source6["result_id"]) || asString3(input["result_id"]) || undefined,
18113
+ scenario_id: asString3(source6["scenario_id"]) || asString3(input["scenario_id"]) || undefined,
18114
+ scenario_name: asString3(source6["scenario_name"]) || asString3(input["scenario_name"]) || undefined,
18115
+ project_id: asString3(source6["project_id"]) || asString3(input["project_id"]) || undefined,
18116
+ url: asString3(source6["url"]) || asString3(input["url"]) || undefined,
18117
+ page_url: asString3(source6["page_url"]) || asString3(input["page_url"]) || undefined,
18118
+ artifact_url: asString3(source6["artifact_url"]) || undefined,
18119
+ screenshot_url: asString3(source6["screenshot_url"]) || undefined,
18120
+ commit: asString3(source6["commit"]) || undefined,
18121
+ branch: asString3(source6["branch"]) || undefined
18122
+ };
18123
+ return Object.values(normalized).some(Boolean) ? normalized : undefined;
18124
+ }
18125
+ function reportTarget(input) {
18126
+ const target = asObject3(input["target"]);
18127
+ const normalized = {
18128
+ url: asString3(target["url"]) || asString3(input["target_url"]) || undefined,
18129
+ route: asString3(target["route"]) || undefined,
18130
+ selector: asString3(target["selector"]) || undefined,
18131
+ component: asString3(target["component"]) || undefined,
18132
+ browser: asString3(target["browser"]) || undefined,
18133
+ viewport: asString3(target["viewport"]) || undefined
18134
+ };
18135
+ return Object.values(normalized).some(Boolean) ? normalized : undefined;
18136
+ }
18137
+ function reportFailure(input) {
18138
+ const failure = asObject3(input["failure"]);
18139
+ const steps = stringArray(failure["steps"] ?? input["steps"], 50);
18140
+ const normalized = {
18141
+ message: truncate3(asString3(failure["message"]) || asString3(input["error"]) || asString3(input["message"]), 1000),
18142
+ expected: truncate3(asString3(failure["expected"]), 1000),
18143
+ actual: truncate3(asString3(failure["actual"]), 1000),
18144
+ stack: truncate3(asString3(failure["stack"]) || asString3(input["stack"]), 3000),
18145
+ reasoning: truncate3(asString3(failure["reasoning"]) || asString3(input["reasoning"]), 1500),
18146
+ steps: steps.length > 0 ? steps.map((step) => truncate3(step, 400)).filter(Boolean) : undefined
18147
+ };
18148
+ return Object.values(normalized).some(Boolean) ? normalized : undefined;
18149
+ }
18150
+ function artifactArray(value) {
18151
+ return objectArray(value, 12).map((item) => ({
18152
+ kind: asString3(item["kind"]) || undefined,
18153
+ label: asString3(item["label"]) || asString3(item["name"]) || undefined,
18154
+ path: asString3(item["path"]) || asString3(item["file_path"]) || undefined,
18155
+ url: asString3(item["url"]) || undefined
18156
+ })).filter((item) => item.kind || item.label || item.path || item.url);
18157
+ }
18158
+ function reportEvidence(input) {
18159
+ const evidence = asObject3(input["evidence"]);
18160
+ const logs = stringArray(evidence["logs"], 8).map((log) => truncate3(log, 1000)).filter(Boolean);
18161
+ const screenshots = artifactArray(evidence["screenshots"] ?? input["screenshots"]);
18162
+ const artifacts = artifactArray(evidence["artifacts"] ?? input["artifacts"]);
18163
+ const normalized = {
18164
+ logs: logs.length > 0 ? logs : undefined,
18165
+ screenshots: screenshots.length > 0 ? screenshots : undefined,
18166
+ artifacts: artifacts.length > 0 ? artifacts : undefined
18167
+ };
18168
+ return Object.values(normalized).some(Boolean) ? normalized : undefined;
18169
+ }
18170
+ function normalizeTesterIssueReport(value, fallbackPriority = "medium") {
18171
+ const input = asObject3(value);
18172
+ if (input["schema_version"] !== TESTERS_ISSUE_REPORT_SCHEMA_VERSION) {
18173
+ throw new Error(`Expected schema_version ${TESTERS_ISSUE_REPORT_SCHEMA_VERSION}`);
18174
+ }
18175
+ const failure = reportFailure(input);
18176
+ const source6 = reportSource(input);
18177
+ const target = reportTarget(input);
18178
+ const title = truncate3(asString3(input["title"]) || asString3(input["summary"]) || failure?.message || source6?.scenario_name || "Tester issue report", 220);
18179
+ if (!title)
18180
+ throw new Error("Tester issue report requires a title");
18181
+ const labels = [
18182
+ ...stringArray(input["labels"], 20),
18183
+ ...stringArray(input["tags"], 20)
18184
+ ].map(cleanKey).filter(Boolean);
18185
+ const report = {
18186
+ schema_version: TESTERS_ISSUE_REPORT_SCHEMA_VERSION,
18187
+ id: asString3(input["id"]) || undefined,
18188
+ fingerprint: asString3(input["fingerprint"]) || undefined,
18189
+ title,
18190
+ summary: truncate3(asString3(input["summary"]), 1000) ?? null,
18191
+ kind: normalizeKind(input["kind"] ?? input["type"]),
18192
+ severity: normalizeSeverity(input["severity"] ?? input["priority"], fallbackPriority),
18193
+ source: source6,
18194
+ target,
18195
+ failure,
18196
+ evidence: reportEvidence(input),
18197
+ labels: labels.length > 0 ? [...new Set(labels)].slice(0, 20) : undefined,
18198
+ metadata: redactValue(asObject3(input["metadata"])),
18199
+ occurred_at: asString3(input["occurred_at"]) || asString3(input["timestamp"]) || undefined
18200
+ };
18201
+ return redactValue(report);
18202
+ }
18203
+ function fingerprintTesterIssueReport(report) {
18204
+ if (report.fingerprint)
18205
+ return `testers:${cleanKey(report.fingerprint)}`;
18206
+ const stackTop = report.failure?.stack?.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || "";
18207
+ const url = normalizeUrlPattern(report.target?.url || report.source?.page_url || report.source?.url || "");
18208
+ const raw = [
18209
+ report.kind || "unknown",
18210
+ report.source?.project_id || "",
18211
+ report.source?.scenario_id || report.source?.scenario_name || "",
18212
+ url,
18213
+ report.target?.route || "",
18214
+ report.target?.selector || report.target?.component || "",
18215
+ normalizeText2(report.failure?.message || report.summary || report.title).slice(0, 240),
18216
+ normalizeText2(stackTop).slice(0, 160)
18217
+ ].join("::");
18218
+ return `testers:${createHash7("sha256").update(raw).digest("hex").slice(0, 16)}`;
18219
+ }
18220
+ function priorityForSeverity(severity, fallback) {
18221
+ return PRIORITIES3.includes(severity) ? severity : fallback;
18222
+ }
18223
+ function maxPriority(left, right) {
18224
+ return PRIORITIES3.indexOf(right) > PRIORITIES3.indexOf(left) ? right : left;
18225
+ }
18226
+ function taskTitle2(report) {
18227
+ const title = report.title.replace(/^BUG:\s*/i, "").replace(/^\[testers\]\s*/i, "");
18228
+ return `BUG: [testers] ${title}`.slice(0, 240);
18229
+ }
18230
+ function evidenceLines(report) {
18231
+ const lines = [];
18232
+ for (const item of report.evidence?.screenshots || []) {
18233
+ lines.push(`Screenshot: ${item.label || item.kind || item.path || item.url}${item.path ? ` (${item.path})` : item.url ? ` (${item.url})` : ""}`);
18234
+ }
18235
+ for (const item of report.evidence?.artifacts || []) {
18236
+ lines.push(`Artifact: ${item.label || item.kind || item.path || item.url}${item.path ? ` (${item.path})` : item.url ? ` (${item.url})` : ""}`);
18237
+ }
18238
+ for (const log of report.evidence?.logs || [])
18239
+ lines.push(`Log: ${log}`);
18240
+ return lines.slice(0, 12);
18241
+ }
18242
+ function taskDescription2(report, fingerprint2) {
18243
+ const failure = report.failure;
18244
+ const lines = [
18245
+ "Tester issue report.",
18246
+ "",
18247
+ `Schema: ${report.schema_version}`,
18248
+ `Fingerprint: ${fingerprint2}`,
18249
+ `Kind: ${report.kind || "unknown"}`,
18250
+ `Severity: ${report.severity || "medium"}`,
18251
+ report.source?.run_id ? `Run: ${report.source.run_id}` : null,
18252
+ report.source?.result_id ? `Result: ${report.source.result_id}` : null,
18253
+ report.source?.scenario_name || report.source?.scenario_id ? `Scenario: ${report.source.scenario_name || report.source.scenario_id}` : null,
18254
+ report.target?.url || report.source?.page_url || report.source?.url ? `URL: ${report.target?.url || report.source?.page_url || report.source?.url}` : null,
18255
+ report.target?.route ? `Route: ${report.target.route}` : null,
18256
+ report.target?.selector ? `Selector: ${report.target.selector}` : null,
18257
+ report.occurred_at ? `Occurred at: ${report.occurred_at}` : null,
18258
+ "",
18259
+ report.summary ? `Summary:
18260
+ ${report.summary}` : null,
18261
+ failure?.message ? `Failure:
18262
+ ${failure.message}` : null,
18263
+ failure?.expected ? `Expected:
18264
+ ${failure.expected}` : null,
18265
+ failure?.actual ? `Actual:
18266
+ ${failure.actual}` : null,
18267
+ failure?.reasoning ? `Reasoning:
18268
+ ${failure.reasoning}` : null,
18269
+ failure?.steps?.length ? `Steps:
18270
+ ${failure.steps.map((step, index) => `${index + 1}. ${step}`).join(`
18271
+ `)}` : null,
18272
+ evidenceLines(report).length ? `Evidence:
18273
+ ${evidenceLines(report).map((line) => `- ${line}`).join(`
18274
+ `)}` : null,
18275
+ failure?.stack ? `Stack:
18276
+ ${failure.stack}` : null
18277
+ ].filter((line) => line !== null);
18278
+ return lines.join(`
18279
+ `).replace(/\n{3,}/g, `
18280
+
18281
+ `).slice(0, 6000);
18282
+ }
18283
+ function taskTags(report) {
18284
+ return [...new Set([
18285
+ "bug",
18286
+ "testers",
18287
+ "tester-report",
18288
+ report.kind ? cleanKey(String(report.kind)).replace(/_/g, "-") : "unknown",
18289
+ ...report.labels || []
18290
+ ].filter(Boolean))].slice(0, 16);
18291
+ }
18292
+ function storedReportSummary(report) {
18293
+ return {
18294
+ id: report.id ?? null,
18295
+ title: report.title,
18296
+ kind: report.kind ?? "unknown",
18297
+ severity: report.severity ?? "medium",
18298
+ run_id: report.source?.run_id ?? null,
18299
+ result_id: report.source?.result_id ?? null,
18300
+ scenario_id: report.source?.scenario_id ?? null,
18301
+ scenario_name: report.source?.scenario_name ?? null,
18302
+ url: report.target?.url ?? report.source?.page_url ?? report.source?.url ?? null,
18303
+ occurred_at: report.occurred_at ?? null
18304
+ };
18305
+ }
18306
+ function testerMetadata(report, fingerprint2, previous, timestamp2) {
18307
+ const occurrenceCount = typeof previous?.["occurrence_count"] === "number" ? previous["occurrence_count"] + 1 : 1;
18308
+ const previousRecent = previous?.["recent_reports"];
18309
+ const recent = Array.isArray(previousRecent) ? previousRecent : [];
18310
+ return {
18311
+ schema_version: TESTERS_ISSUE_REPORT_SCHEMA_VERSION,
18312
+ fingerprint: fingerprint2,
18313
+ first_seen_at: asString3(previous?.["first_seen_at"]) || timestamp2,
18314
+ last_seen_at: timestamp2,
18315
+ occurrence_count: occurrenceCount,
18316
+ latest_report: storedReportSummary(report),
18317
+ recent_reports: [...recent.slice(-4), storedReportSummary(report)]
18318
+ };
18319
+ }
18320
+ function sourceMetadata(report) {
18321
+ const url = report.target?.url || report.source?.page_url || report.source?.url || null;
18322
+ return {
18323
+ ...url ? { source_url: url, external_url: url, issue_url: url } : {},
18324
+ ...report.source?.run_id ? { tester_run_id: report.source.run_id } : {},
18325
+ ...report.source?.result_id ? { tester_result_id: report.source.result_id } : {},
18326
+ ...report.source?.scenario_id ? { tester_scenario_id: report.source.scenario_id } : {},
18327
+ ...report.source?.project_id ? { tester_project_id: report.source.project_id } : {}
18328
+ };
18329
+ }
18330
+ function findExistingTask2(fingerprint2, input, db) {
18331
+ for (const task2 of listTasks({
18332
+ include_archived: true,
18333
+ project_id: input.project_id,
18334
+ task_list_id: input.task_list_id
18335
+ }, db)) {
18336
+ const metadata = task2.metadata || {};
18337
+ const tester = asObject3(metadata["tester_issue_report"]);
18338
+ if (tester["fingerprint"] === fingerprint2)
18339
+ return task2;
18340
+ if (metadata["tester_issue_fingerprint"] === fingerprint2)
18341
+ return task2;
18342
+ if (metadata["external_ref"] === fingerprint2)
18343
+ return task2;
18344
+ }
18345
+ return null;
18346
+ }
18347
+ function commandsFor(task2) {
18348
+ return [
18349
+ "todos issues report --file tester-report.json --apply --json",
18350
+ task2 ? `todos show ${task2.id.slice(0, 8)}` : `todos list --tags tester-report --json`,
18351
+ `todos dedupe scan --threshold 0.8 --json`
18352
+ ];
18353
+ }
18354
+ function updateExistingTask(task2, report, fingerprint2, input, timestamp2, db) {
18355
+ if (input.update_existing === false)
18356
+ return { action: "matched", task: task2 };
18357
+ const previous = asObject3(task2.metadata["tester_issue_report"]);
18358
+ const severityPriority = priorityForSeverity(report.severity, input.default_priority || "medium");
18359
+ const nextStatus = task2.status === "completed" || task2.status === "cancelled" ? "pending" : task2.status;
18360
+ const action = nextStatus !== task2.status ? "regressed" : "updated";
18361
+ const updated = updateTask(task2.id, {
18362
+ version: task2.version,
18363
+ title: taskTitle2(report),
18364
+ description: taskDescription2(report, fingerprint2),
18365
+ priority: maxPriority(task2.priority, severityPriority),
18366
+ status: nextStatus,
18367
+ completed_at: nextStatus !== task2.status ? null : undefined,
18368
+ tags: [...new Set([...task2.tags, ...taskTags(report)])],
18369
+ metadata: {
18370
+ ...task2.metadata,
18371
+ ...sourceMetadata(report),
18372
+ external_ref: fingerprint2,
18373
+ tester_issue_fingerprint: fingerprint2,
18374
+ tester_issue_report: testerMetadata(report, fingerprint2, previous, timestamp2)
18375
+ },
18376
+ ...input.assigned_to !== undefined ? { assigned_to: input.assigned_to } : {},
18377
+ task_type: task2.task_type || "bug"
18378
+ }, db);
18379
+ return { action, task: updated };
18380
+ }
18381
+ function upsertTesterIssueReport(input, db) {
18382
+ const d = db || getDatabase();
18383
+ const timestamp2 = now();
18384
+ const warnings = [];
18385
+ const report = normalizeTesterIssueReport(input.report, input.default_priority || "medium");
18386
+ const fingerprint2 = fingerprintTesterIssueReport(report);
18387
+ const existing = findExistingTask2(fingerprint2, input, d);
18388
+ if (!input.apply) {
18389
+ const action = existing ? "matched" : "preview";
18390
+ return {
18391
+ schema_version: TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
18392
+ local_only: true,
18393
+ dry_run: true,
18394
+ processed_at: timestamp2,
18395
+ action,
18396
+ fingerprint: fingerprint2,
18397
+ report,
18398
+ task: existing,
18399
+ warnings,
18400
+ commands: commandsFor(existing)
18401
+ };
18402
+ }
18403
+ if (existing) {
18404
+ const updated = updateExistingTask(existing, report, fingerprint2, input, timestamp2, d);
18405
+ return {
18406
+ schema_version: TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
18407
+ local_only: true,
18408
+ dry_run: false,
18409
+ processed_at: timestamp2,
18410
+ action: updated.action,
18411
+ fingerprint: fingerprint2,
18412
+ report,
18413
+ task: updated.task,
18414
+ warnings,
18415
+ commands: commandsFor(updated.task)
18416
+ };
18417
+ }
18418
+ const priority = priorityForSeverity(report.severity, input.default_priority || "medium");
18419
+ const task2 = createTask({
18420
+ title: taskTitle2(report),
18421
+ description: taskDescription2(report, fingerprint2),
18422
+ priority,
18423
+ status: "pending",
18424
+ tags: taskTags(report),
18425
+ metadata: {
18426
+ ...sourceMetadata(report),
18427
+ external_ref: fingerprint2,
18428
+ tester_issue_fingerprint: fingerprint2,
18429
+ tester_issue_report: testerMetadata(report, fingerprint2, null, timestamp2),
18430
+ tester_issue_report_raw: redactValue({
18431
+ ...report,
18432
+ evidence: report.evidence ? {
18433
+ screenshots: report.evidence.screenshots,
18434
+ artifacts: report.evidence.artifacts
18435
+ } : undefined
18436
+ })
18437
+ },
18438
+ project_id: input.project_id,
18439
+ task_list_id: input.task_list_id,
18440
+ agent_id: input.agent_id,
18441
+ assigned_to: input.assigned_to,
18442
+ task_type: "bug"
18443
+ }, d);
18444
+ return {
18445
+ schema_version: TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
18446
+ local_only: true,
18447
+ dry_run: false,
18448
+ processed_at: timestamp2,
18449
+ action: "created",
18450
+ fingerprint: fingerprint2,
18451
+ report,
18452
+ task: task2,
18453
+ warnings,
18454
+ commands: commandsFor(task2)
18455
+ };
18456
+ }
18457
+ function upsertTesterIssueReports(input, db) {
18458
+ const d = db || getDatabase();
18459
+ const run = () => input.reports.map((report) => upsertTesterIssueReport({ ...input, report }, d));
18460
+ const results = input.apply ? d.transaction(run)() : run();
18461
+ const summary = {
18462
+ total: results.length,
18463
+ preview: 0,
18464
+ matched: 0,
18465
+ created: 0,
18466
+ updated: 0,
18467
+ regressed: 0
18468
+ };
18469
+ for (const result of results)
18470
+ summary[result.action]++;
18471
+ return {
18472
+ schema_version: TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION,
18473
+ local_only: true,
18474
+ dry_run: !input.apply,
18475
+ processed_at: now(),
18476
+ results,
18477
+ summary
18478
+ };
18479
+ }
18480
+ function readTesterIssueReportsPayload(value) {
18481
+ if (Array.isArray(value))
18482
+ return value;
18483
+ const record = asObject3(value);
18484
+ if (Array.isArray(record["reports"]))
18485
+ return record["reports"];
18486
+ if (Array.isArray(record["issues"]))
18487
+ return record["issues"];
18488
+ return [value];
18489
+ }
17185
18490
  // src/lib/local-notifications.ts
17186
18491
  init_database();
17187
18492
 
@@ -17284,13 +18589,13 @@ function eventSeverity(eventType) {
17284
18589
  function payloadText(payload) {
17285
18590
  return JSON.stringify(payload).toLowerCase();
17286
18591
  }
17287
- function asString3(value) {
18592
+ function asString4(value) {
17288
18593
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
17289
18594
  }
17290
18595
  function fieldMatches(allowed, value) {
17291
18596
  if (!allowed || allowed.length === 0)
17292
18597
  return true;
17293
- const stringValue = asString3(value);
18598
+ const stringValue = asString4(value);
17294
18599
  return Boolean(stringValue && allowed.includes(stringValue));
17295
18600
  }
17296
18601
  function containsMatches(needles, payload) {
@@ -17356,8 +18661,8 @@ function evaluateTerminalWatchRules(input, rules = listTerminalNotificationRules
17356
18661
  if (isQuietTime(rule.quiet_hours, timestamp2))
17357
18662
  skipped.push("quiet hours active");
17358
18663
  const matched = skipped.length === 0;
17359
- const title = asString3(payload["title"]) || asString3(payload["name"]) || input.type;
17360
- const taskId = asString3(payload["id"]) || asString3(payload["task_id"]);
18664
+ const title = asString4(payload["title"]) || asString4(payload["name"]) || input.type;
18665
+ const taskId = asString4(payload["id"]) || asString4(payload["task_id"]);
17361
18666
  const notification = {
17362
18667
  rule: rule.name,
17363
18668
  event_type: input.type,
@@ -17366,8 +18671,8 @@ function evaluateTerminalWatchRules(input, rules = listTerminalNotificationRules
17366
18671
  message: `${input.type}: ${title}`,
17367
18672
  timestamp: timestamp2,
17368
18673
  task_id: taskId,
17369
- project_id: asString3(payload["project_id"]),
17370
- agent_id: asString3(payload["agent_id"]) || asString3(payload["assigned_to"]),
18674
+ project_id: asString4(payload["project_id"]),
18675
+ agent_id: asString4(payload["agent_id"]) || asString4(payload["assigned_to"]),
17371
18676
  bell: rule.bell && severity === "critical",
17372
18677
  payload
17373
18678
  };
@@ -18017,12 +19322,12 @@ function summarizeTask(task2) {
18017
19322
  };
18018
19323
  }
18019
19324
  function overdueTasks(tasks, nowIso) {
18020
- const now2 = Date.parse(nowIso);
19325
+ const now3 = Date.parse(nowIso);
18021
19326
  return tasks.filter((task2) => {
18022
19327
  if (isTerminal(task2) || !task2.due_at)
18023
19328
  return false;
18024
19329
  const due = Date.parse(task2.due_at);
18025
- return Number.isFinite(due) && due < now2;
19330
+ return Number.isFinite(due) && due < now3;
18026
19331
  });
18027
19332
  }
18028
19333
  function isReady(task2, db) {
@@ -18355,7 +19660,7 @@ function renderLocalReportMarkdown(report) {
18355
19660
  // src/lib/local-encryption.ts
18356
19661
  init_config();
18357
19662
  init_redaction();
18358
- import { createCipheriv, createDecipheriv, createHash as createHash7, randomBytes, scryptSync, timingSafeEqual } from "crypto";
19663
+ import { createCipheriv, createDecipheriv, createHash as createHash8, randomBytes, scryptSync, timingSafeEqual as timingSafeEqual2 } from "crypto";
18359
19664
  var TODOS_ENCRYPTED_VALUE_KIND = "hasna.todos.encrypted-value";
18360
19665
  var TODOS_ENCRYPTED_BRIDGE_KIND = "hasna.todos.encrypted-bridge";
18361
19666
  var TODOS_ENCRYPTION_SCHEMA_VERSION = 1;
@@ -18377,11 +19682,11 @@ class EncryptedPayloadError extends Error {
18377
19682
  super(message);
18378
19683
  }
18379
19684
  }
18380
- function now2() {
19685
+ function now3() {
18381
19686
  return new Date().toISOString();
18382
19687
  }
18383
19688
  function sha2564(value) {
18384
- return createHash7("sha256").update(value).digest("hex");
19689
+ return createHash8("sha256").update(value).digest("hex");
18385
19690
  }
18386
19691
  function normalizeProfileName(value) {
18387
19692
  const name = (value || DEFAULT_ENCRYPTION_PROFILE).trim();
@@ -18414,7 +19719,7 @@ function upsertEncryptionProfile(input) {
18414
19719
  const name = normalizeProfileName(input.name);
18415
19720
  const config = loadConfig();
18416
19721
  const existing = config.encryption_profiles?.[name];
18417
- const timestamp2 = now2();
19722
+ const timestamp2 = now3();
18418
19723
  const profile = {
18419
19724
  name,
18420
19725
  algorithm: "aes-256-gcm",
@@ -18471,7 +19776,7 @@ function encryptString(plaintext, options = {}) {
18471
19776
  return {
18472
19777
  schemaVersion: TODOS_ENCRYPTION_SCHEMA_VERSION,
18473
19778
  kind: TODOS_ENCRYPTED_VALUE_KIND,
18474
- encryptedAt: options.encryptedAt ?? now2(),
19779
+ encryptedAt: options.encryptedAt ?? now3(),
18475
19780
  profile: profile.name,
18476
19781
  key_env: profile.key_env,
18477
19782
  algorithm: "aes-256-gcm",
@@ -18506,7 +19811,7 @@ function decryptString(envelope, env = process.env) {
18506
19811
  ]).toString("utf8");
18507
19812
  const expected = Buffer.from(envelope.plaintext_sha256, "hex");
18508
19813
  const actual = Buffer.from(sha2564(plaintext), "hex");
18509
- if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
19814
+ if (expected.length !== actual.length || !timingSafeEqual2(expected, actual)) {
18510
19815
  throw new EncryptedPayloadError("decrypted payload checksum mismatch");
18511
19816
  }
18512
19817
  return plaintext;
@@ -18884,6 +20189,8 @@ export {
18884
20189
  verifyLocalAuditLedger,
18885
20190
  validateLocalBridgeBundle,
18886
20191
  validateJsonContract,
20192
+ upsertTesterIssueReports,
20193
+ upsertTesterIssueReport,
18887
20194
  upsertReviewRoutingRule,
18888
20195
  upsertReleaseGroup,
18889
20196
  upsertEncryptionProfile,
@@ -18904,8 +20211,10 @@ export {
18904
20211
  renderLocalAuditLedgerMarkdown,
18905
20212
  removeReviewRoutingRule,
18906
20213
  removeEncryptionProfile,
20214
+ readTesterIssueReportsPayload,
18907
20215
  readLocalBackupFile,
18908
20216
  pollLocalSnapshots,
20217
+ normalizeTesterIssueReport,
18909
20218
  listSdkIntegrationExamples,
18910
20219
  listRoadmaps,
18911
20220
  listReviewRoutingRules,
@@ -18929,6 +20238,7 @@ export {
18929
20238
  getLocalSnapshot,
18930
20239
  getLocalAuditLedger,
18931
20240
  getJsonContract,
20241
+ fingerprintTesterIssueReport,
18932
20242
  exportRoadmapBundle,
18933
20243
  encryptionProfileStatus,
18934
20244
  encryptValue,
@@ -18973,6 +20283,9 @@ export {
18973
20283
  TODOS_ENCRYPTED_BRIDGE_KIND,
18974
20284
  TODOS_CONTRACTS,
18975
20285
  TODOS_API_ROUTES,
20286
+ TESTERS_ISSUE_REPORT_SCHEMA_VERSION,
20287
+ TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
20288
+ TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION,
18976
20289
  LOCAL_USAGE_LEDGER_SCHEMA_VERSION,
18977
20290
  LOCAL_ROADMAP_SCHEMA_VERSION,
18978
20291
  LOCAL_REPORT_TYPES,