@hasna/todos 0.11.55 → 0.11.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,568 @@ async function testLocalEventHook(name, input) {
6711
6773
  return emitLocalEventHooks({ ...input, hooks: [hook] });
6712
6774
  }
6713
6775
 
6776
+ // node_modules/.bun/@hasna+events@0.1.7/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) {
6794
+ const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
6795
+ return new RegExp(`^${escaped}$`);
6796
+ }
6797
+ function matchString(value, matcher) {
6798
+ if (matcher === undefined)
6799
+ return true;
6800
+ if (value === undefined)
6801
+ return false;
6802
+ const matchers = Array.isArray(matcher) ? matcher : [matcher];
6803
+ return matchers.some((item) => wildcardToRegExp(item).test(value));
6804
+ }
6805
+ function matchRecord(input, matcher) {
6806
+ if (!matcher)
6807
+ return true;
6808
+ return Object.entries(matcher).every(([path, expected]) => {
6809
+ const actual = getPathValue(input, path);
6810
+ if (typeof expected === "string" || Array.isArray(expected)) {
6811
+ return matchString(actual === undefined ? undefined : String(actual), expected);
6812
+ }
6813
+ return actual === expected;
6814
+ });
6815
+ }
6816
+ function eventMatchesFilter(event, filter) {
6817
+ 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);
6818
+ }
6819
+ function channelMatchesEvent(channel, event) {
6820
+ if (!channel.enabled)
6821
+ return false;
6822
+ if (!channel.filters || channel.filters.length === 0)
6823
+ return true;
6824
+ return channel.filters.some((filter) => eventMatchesFilter(event, filter));
6825
+ }
6826
+ var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
6827
+ var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
6828
+ function getEventsDataDir(override) {
6829
+ return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join5(homedir(), ".hasna", "events");
6830
+ }
6831
+
6832
+ class JsonEventsStore {
6833
+ dataDir;
6834
+ channelsPath;
6835
+ eventsPath;
6836
+ deliveriesPath;
6837
+ constructor(dataDir = getEventsDataDir()) {
6838
+ this.dataDir = dataDir;
6839
+ this.channelsPath = join5(dataDir, "channels.json");
6840
+ this.eventsPath = join5(dataDir, "events.json");
6841
+ this.deliveriesPath = join5(dataDir, "deliveries.json");
6842
+ }
6843
+ async init() {
6844
+ await mkdir(this.dataDir, { recursive: true, mode: 448 });
6845
+ await chmod(this.dataDir, 448).catch(() => {
6846
+ return;
6847
+ });
6848
+ await this.ensureArrayFile(this.channelsPath);
6849
+ await this.ensureArrayFile(this.eventsPath);
6850
+ await this.ensureArrayFile(this.deliveriesPath);
6851
+ }
6852
+ async addChannel(channel) {
6853
+ await this.init();
6854
+ const channels = await this.readJson(this.channelsPath, []);
6855
+ const index = channels.findIndex((item) => item.id === channel.id);
6856
+ if (index >= 0) {
6857
+ channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
6858
+ } else {
6859
+ channels.push(channel);
6860
+ }
6861
+ await this.writeJson(this.channelsPath, channels);
6862
+ return index >= 0 ? channels[index] : channel;
6863
+ }
6864
+ async listChannels() {
6865
+ await this.init();
6866
+ return this.readJson(this.channelsPath, []);
6867
+ }
6868
+ async getChannel(id) {
6869
+ const channels = await this.listChannels();
6870
+ return channels.find((channel) => channel.id === id);
6871
+ }
6872
+ async removeChannel(id) {
6873
+ await this.init();
6874
+ const channels = await this.readJson(this.channelsPath, []);
6875
+ const next = channels.filter((channel) => channel.id !== id);
6876
+ await this.writeJson(this.channelsPath, next);
6877
+ return next.length !== channels.length;
6878
+ }
6879
+ async appendEvent(event) {
6880
+ await this.init();
6881
+ const events = await this.readJson(this.eventsPath, []);
6882
+ events.push(event);
6883
+ await this.writeJson(this.eventsPath, events);
6884
+ return event;
6885
+ }
6886
+ async listEvents() {
6887
+ await this.init();
6888
+ return this.readJson(this.eventsPath, []);
6889
+ }
6890
+ async findEventByIdentity(identity) {
6891
+ const events = await this.listEvents();
6892
+ return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
6893
+ }
6894
+ async appendDelivery(result) {
6895
+ await this.init();
6896
+ const deliveries = await this.readJson(this.deliveriesPath, []);
6897
+ deliveries.push(result);
6898
+ await this.writeJson(this.deliveriesPath, deliveries);
6899
+ return result;
6900
+ }
6901
+ async listDeliveries() {
6902
+ await this.init();
6903
+ return this.readJson(this.deliveriesPath, []);
6904
+ }
6905
+ async exportData() {
6906
+ return {
6907
+ channels: await this.listChannels(),
6908
+ events: await this.listEvents(),
6909
+ deliveries: await this.listDeliveries()
6910
+ };
6911
+ }
6912
+ async ensureArrayFile(path) {
6913
+ if (!existsSync6(path)) {
6914
+ await writeFile(path, `[]
6915
+ `, { encoding: "utf-8", mode: 384 });
6916
+ }
6917
+ await chmod(path, 384).catch(() => {
6918
+ return;
6919
+ });
6920
+ }
6921
+ async readJson(path, fallback) {
6922
+ try {
6923
+ const raw = await readFile(path, "utf-8");
6924
+ if (!raw.trim())
6925
+ return fallback;
6926
+ return JSON.parse(raw);
6927
+ } catch (error) {
6928
+ if (error.code === "ENOENT")
6929
+ return fallback;
6930
+ throw error;
6931
+ }
6932
+ }
6933
+ async writeJson(path, value) {
6934
+ const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
6935
+ await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
6936
+ `, { encoding: "utf-8", mode: 384 });
6937
+ await rename(tempPath, path);
6938
+ await chmod(path, 384).catch(() => {
6939
+ return;
6940
+ });
6941
+ }
6942
+ }
6943
+ var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
6944
+ function buildSignatureBase(timestamp, body) {
6945
+ return `${timestamp}.${body}`;
6946
+ }
6947
+ function signPayload(secret, timestamp, body) {
6948
+ const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
6949
+ return `sha256=${digest}`;
6950
+ }
6951
+ function now2() {
6952
+ return new Date().toISOString();
6953
+ }
6954
+ function truncate(value, max = 4096) {
6955
+ return value.length > max ? `${value.slice(0, max)}...` : value;
6956
+ }
6957
+ function buildWebhookRequest(event, channel) {
6958
+ if (!channel.webhook)
6959
+ throw new Error(`Channel ${channel.id} has no webhook config`);
6960
+ const body = JSON.stringify(event);
6961
+ const timestamp = event.time;
6962
+ const headers = {
6963
+ "Content-Type": "application/json",
6964
+ "User-Agent": "@hasna/events",
6965
+ "X-Hasna-Event-Id": event.id,
6966
+ "X-Hasna-Event-Type": event.type,
6967
+ "X-Hasna-Timestamp": timestamp,
6968
+ ...channel.webhook.headers
6969
+ };
6970
+ if (channel.webhook.secret) {
6971
+ headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
6972
+ }
6973
+ return { body, headers };
6974
+ }
6975
+ async function dispatchWebhook(event, channel, options = {}) {
6976
+ if (!channel.webhook)
6977
+ throw new Error(`Channel ${channel.id} has no webhook config`);
6978
+ const startedAt = now2();
6979
+ const { body, headers } = buildWebhookRequest(event, channel);
6980
+ const controller = new AbortController;
6981
+ const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
6982
+ try {
6983
+ const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
6984
+ method: "POST",
6985
+ headers,
6986
+ body,
6987
+ signal: controller.signal
6988
+ });
6989
+ const responseBody = truncate(await response.text());
6990
+ return {
6991
+ attempt: 1,
6992
+ status: response.ok ? "success" : "failed",
6993
+ startedAt,
6994
+ completedAt: now2(),
6995
+ responseStatus: response.status,
6996
+ responseBody,
6997
+ error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
6998
+ };
6999
+ } catch (error) {
7000
+ return {
7001
+ attempt: 1,
7002
+ status: "failed",
7003
+ startedAt,
7004
+ completedAt: now2(),
7005
+ error: error instanceof Error ? error.message : String(error)
7006
+ };
7007
+ } finally {
7008
+ clearTimeout(timeout);
7009
+ }
7010
+ }
7011
+ async function dispatchCommand(event, channel) {
7012
+ if (!channel.command)
7013
+ throw new Error(`Channel ${channel.id} has no command config`);
7014
+ const startedAt = now2();
7015
+ const eventJson = JSON.stringify(event);
7016
+ const env = {
7017
+ ...process.env,
7018
+ ...channel.command.env,
7019
+ HASNA_CHANNEL_ID: channel.id,
7020
+ HASNA_EVENT_ID: event.id,
7021
+ HASNA_EVENT_TYPE: event.type,
7022
+ HASNA_EVENT_SOURCE: event.source,
7023
+ HASNA_EVENT_SUBJECT: event.subject ?? "",
7024
+ HASNA_EVENT_SEVERITY: event.severity,
7025
+ HASNA_EVENT_TIME: event.time,
7026
+ HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
7027
+ HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
7028
+ HASNA_EVENT_JSON: eventJson
7029
+ };
7030
+ return new Promise((resolve6) => {
7031
+ const child = spawn(channel.command.command, channel.command.args ?? [], {
7032
+ cwd: channel.command.cwd,
7033
+ env,
7034
+ stdio: ["pipe", "pipe", "pipe"]
7035
+ });
7036
+ let stdout = "";
7037
+ let stderr = "";
7038
+ const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
7039
+ child.stdin.end(eventJson);
7040
+ child.stdout.on("data", (chunk) => {
7041
+ stdout += chunk.toString();
7042
+ });
7043
+ child.stderr.on("data", (chunk) => {
7044
+ stderr += chunk.toString();
7045
+ });
7046
+ child.on("error", (error) => {
7047
+ clearTimeout(timeout);
7048
+ resolve6({
7049
+ attempt: 1,
7050
+ status: "failed",
7051
+ startedAt,
7052
+ completedAt: now2(),
7053
+ stdout: truncate(stdout),
7054
+ stderr: truncate(stderr),
7055
+ error: error.message
7056
+ });
7057
+ });
7058
+ child.on("close", (code, signal) => {
7059
+ clearTimeout(timeout);
7060
+ const success = code === 0;
7061
+ resolve6({
7062
+ attempt: 1,
7063
+ status: success ? "success" : "failed",
7064
+ startedAt,
7065
+ completedAt: now2(),
7066
+ stdout: truncate(stdout),
7067
+ stderr: truncate(stderr),
7068
+ error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
7069
+ });
7070
+ });
7071
+ });
7072
+ }
7073
+ async function dispatchChannel(event, channel, options = {}) {
7074
+ if (channel.transport === "webhook")
7075
+ return dispatchWebhook(event, channel, options);
7076
+ if (channel.transport === "command")
7077
+ return dispatchCommand(event, channel);
7078
+ return {
7079
+ attempt: 1,
7080
+ status: "skipped",
7081
+ startedAt: now2(),
7082
+ completedAt: now2(),
7083
+ error: `Unsupported transport: ${channel.transport}`
7084
+ };
7085
+ }
7086
+ function createDeliveryResult(event, channel, attempts) {
7087
+ const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
7088
+ return {
7089
+ id: randomUUID2(),
7090
+ eventId: event.id,
7091
+ channelId: channel.id,
7092
+ transport: channel.transport,
7093
+ status,
7094
+ attempts,
7095
+ createdAt: attempts[0]?.startedAt ?? now2(),
7096
+ completedAt: attempts.at(-1)?.completedAt ?? now2()
7097
+ };
7098
+ }
7099
+ function createEvent(input) {
7100
+ return {
7101
+ id: input.id ?? randomUUID22(),
7102
+ source: input.source,
7103
+ type: input.type,
7104
+ time: normalizeTime(input.time),
7105
+ subject: input.subject,
7106
+ severity: input.severity ?? "info",
7107
+ data: input.data ?? {},
7108
+ message: input.message,
7109
+ dedupeKey: input.dedupeKey,
7110
+ schemaVersion: input.schemaVersion ?? "1.0",
7111
+ metadata: input.metadata ?? {}
7112
+ };
7113
+ }
7114
+
7115
+ class EventsClient {
7116
+ store;
7117
+ redactors;
7118
+ transportOptions;
7119
+ constructor(options = {}) {
7120
+ this.store = options.store ?? new JsonEventsStore(options.dataDir);
7121
+ this.redactors = options.redactors ?? [];
7122
+ this.transportOptions = { fetchImpl: options.fetchImpl };
7123
+ }
7124
+ async addChannel(input) {
7125
+ const timestamp = new Date().toISOString();
7126
+ return this.store.addChannel({
7127
+ ...input,
7128
+ createdAt: input.createdAt ?? timestamp,
7129
+ updatedAt: input.updatedAt ?? timestamp
7130
+ });
7131
+ }
7132
+ async listChannels() {
7133
+ return this.store.listChannels();
7134
+ }
7135
+ async removeChannel(id) {
7136
+ return this.store.removeChannel(id);
7137
+ }
7138
+ async emit(input, options = {}) {
7139
+ const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
7140
+ if (options.dedupe !== false) {
7141
+ const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
7142
+ if (existing) {
7143
+ return { event: existing, deliveries: [], deduped: true };
7144
+ }
7145
+ }
7146
+ await this.store.appendEvent(event);
7147
+ const deliveries = options.deliver === false ? [] : await this.deliver(event);
7148
+ return { event, deliveries, deduped: false };
7149
+ }
7150
+ async listEvents() {
7151
+ return this.store.listEvents();
7152
+ }
7153
+ async listDeliveries() {
7154
+ return this.store.listDeliveries();
7155
+ }
7156
+ async deliver(event) {
7157
+ const channels = await this.store.listChannels();
7158
+ const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
7159
+ const deliveries = [];
7160
+ for (const channel of selected) {
7161
+ const eventForChannel = await this.applyRedaction(event, channel);
7162
+ const result = await this.deliverWithRetry(eventForChannel, channel);
7163
+ await this.store.appendDelivery(result);
7164
+ deliveries.push(result);
7165
+ }
7166
+ return deliveries;
7167
+ }
7168
+ async testChannel(id, input = {}) {
7169
+ const channel = await this.store.getChannel(id);
7170
+ if (!channel)
7171
+ throw new Error(`Channel not found: ${id}`);
7172
+ const event = createEvent({
7173
+ source: input.source ?? "hasna.events",
7174
+ type: input.type ?? "events.test",
7175
+ subject: input.subject ?? id,
7176
+ severity: input.severity ?? "info",
7177
+ data: input.data ?? { test: true },
7178
+ message: input.message ?? "Hasna events test delivery",
7179
+ dedupeKey: input.dedupeKey,
7180
+ schemaVersion: input.schemaVersion,
7181
+ metadata: input.metadata,
7182
+ time: input.time,
7183
+ id: input.id
7184
+ });
7185
+ const eventForChannel = await this.applyRedaction(event, channel);
7186
+ const result = await this.deliverWithRetry(eventForChannel, channel);
7187
+ await this.store.appendDelivery(result);
7188
+ return result;
7189
+ }
7190
+ async replay(options = {}) {
7191
+ const events = (await this.store.listEvents()).filter((event) => {
7192
+ if (options.eventId && event.id !== options.eventId)
7193
+ return false;
7194
+ if (options.source && event.source !== options.source)
7195
+ return false;
7196
+ if (options.type && event.type !== options.type)
7197
+ return false;
7198
+ return true;
7199
+ });
7200
+ if (options.dryRun)
7201
+ return { events, deliveries: [] };
7202
+ const deliveries = [];
7203
+ for (const event of events) {
7204
+ deliveries.push(...await this.deliver(event));
7205
+ }
7206
+ return { events, deliveries };
7207
+ }
7208
+ async applyRedaction(event, channel) {
7209
+ let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
7210
+ for (const redactor of this.redactors) {
7211
+ next = await redactor(next, channel);
7212
+ }
7213
+ return next;
7214
+ }
7215
+ async deliverWithRetry(event, channel) {
7216
+ const policy = normalizeRetryPolicy(channel.retry);
7217
+ const attempts = [];
7218
+ for (let index = 0;index < policy.maxAttempts; index += 1) {
7219
+ const attempt = await dispatchChannel(event, channel, this.transportOptions);
7220
+ attempt.attempt = index + 1;
7221
+ if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
7222
+ attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
7223
+ }
7224
+ attempts.push(attempt);
7225
+ if (attempt.status !== "failed")
7226
+ break;
7227
+ if (attempt.nextBackoffMs)
7228
+ await Bun.sleep(attempt.nextBackoffMs);
7229
+ }
7230
+ return createDeliveryResult(event, channel, attempts);
7231
+ }
7232
+ }
7233
+ function redactPaths(event, paths, replacement = "[REDACTED]") {
7234
+ if (paths.length === 0)
7235
+ return event;
7236
+ const copy = structuredClone(event);
7237
+ for (const path of paths) {
7238
+ setPath(copy, path, replacement);
7239
+ }
7240
+ return copy;
7241
+ }
7242
+ function redactSensitiveKeys(event, replacement = "[REDACTED]") {
7243
+ return redactValue2(event, replacement);
7244
+ }
7245
+ function shouldRedactKey(key) {
7246
+ return /secret|token|password|api[_-]?key|authorization/i.test(key);
7247
+ }
7248
+ function redactValue2(value, replacement) {
7249
+ if (Array.isArray(value))
7250
+ return value.map((item) => redactValue2(item, replacement));
7251
+ if (!value || typeof value !== "object")
7252
+ return value;
7253
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
7254
+ key,
7255
+ shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
7256
+ ]));
7257
+ }
7258
+ function setPath(input, path, replacement) {
7259
+ const parts = path.split(".");
7260
+ let cursor = input;
7261
+ for (const part of parts.slice(0, -1)) {
7262
+ const next = cursor[part];
7263
+ if (!next || typeof next !== "object")
7264
+ return;
7265
+ cursor = next;
7266
+ }
7267
+ const last = parts.at(-1);
7268
+ if (last && last in cursor)
7269
+ cursor[last] = replacement;
7270
+ }
7271
+ function normalizeTime(value) {
7272
+ if (!value)
7273
+ return new Date().toISOString();
7274
+ return value instanceof Date ? value.toISOString() : value;
7275
+ }
7276
+ function normalizeRetryPolicy(policy) {
7277
+ return {
7278
+ maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
7279
+ backoffMs: Math.max(0, policy?.backoffMs ?? 250),
7280
+ multiplier: Math.max(1, policy?.multiplier ?? 2)
7281
+ };
7282
+ }
7283
+
7284
+ // src/lib/shared-events.ts
7285
+ var SOURCE = "todos";
7286
+ function taskEventData(task, extra = {}) {
7287
+ return {
7288
+ id: task.id,
7289
+ task_id: task.id,
7290
+ short_id: task.short_id,
7291
+ title: task.title,
7292
+ description: task.description,
7293
+ status: task.status,
7294
+ priority: task.priority,
7295
+ project_id: task.project_id,
7296
+ parent_id: task.parent_id,
7297
+ plan_id: task.plan_id,
7298
+ task_list_id: task.task_list_id,
7299
+ agent_id: task.agent_id,
7300
+ assigned_to: task.assigned_to,
7301
+ session_id: task.session_id,
7302
+ working_dir: task.working_dir,
7303
+ tags: task.tags,
7304
+ metadata: task.metadata,
7305
+ version: task.version,
7306
+ created_at: task.created_at,
7307
+ updated_at: task.updated_at,
7308
+ started_at: task.started_at,
7309
+ completed_at: task.completed_at,
7310
+ due_at: task.due_at,
7311
+ ...extra
7312
+ };
7313
+ }
7314
+ async function emitSharedTaskEvent(input) {
7315
+ const data = taskEventData(input.task, input.data);
7316
+ await new EventsClient().emit({
7317
+ source: SOURCE,
7318
+ type: input.type,
7319
+ subject: input.task.id,
7320
+ severity: input.severity ?? "info",
7321
+ message: input.message ?? `${input.type}: ${input.task.title}`,
7322
+ data,
7323
+ dedupeKey: input.dedupeKey ?? `${input.type}:${input.task.id}:${input.task.version}`,
7324
+ metadata: {
7325
+ package: "@hasna/todos",
7326
+ task_id: input.task.id,
7327
+ project_id: input.task.project_id,
7328
+ task_list_id: input.task.task_list_id
7329
+ }
7330
+ }, { deliver: true, dedupe: true });
7331
+ }
7332
+ function emitSharedTaskEventQuiet(input) {
7333
+ emitSharedTaskEvent(input).catch(() => {
7334
+ return;
7335
+ });
7336
+ }
7337
+
6714
7338
  // src/db/audit.ts
6715
7339
  init_database();
6716
7340
  function logTaskChange(taskId, action, field2, oldValue, newValue, agentId, db) {
@@ -6973,7 +7597,7 @@ async function deliverWebhook(wh, event, body, attempt, db) {
6973
7597
  activeDeliveries--;
6974
7598
  }
6975
7599
  }
6976
- async function dispatchWebhook(event, payload, db) {
7600
+ async function dispatchWebhook2(event, payload, db) {
6977
7601
  const d = db || getDatabase();
6978
7602
  const webhooks = listWebhooks(d).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
6979
7603
  const payloadObj = typeof payload === "object" && payload !== null ? payload : {};
@@ -7127,7 +7751,10 @@ function createTask(input, db) {
7127
7751
  insertTaskTags(id, tags, d);
7128
7752
  }
7129
7753
  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(() => {});
7754
+ const payload = taskEventData(task);
7755
+ dispatchWebhook2("task.created", payload, d).catch(() => {});
7756
+ emitLocalEventHooksQuiet({ type: "task.created", payload });
7757
+ emitSharedTaskEventQuiet({ type: "task.created", task });
7131
7758
  return task;
7132
7759
  }
7133
7760
  function getTask(id, db) {
@@ -7471,18 +8098,7 @@ function updateTask(id, input, db) {
7471
8098
  logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
7472
8099
  if (input.approved_by !== undefined)
7473
8100
  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 {
8101
+ const updatedTask = {
7486
8102
  ...task,
7487
8103
  ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
7488
8104
  tags: input.tags ?? task.tags,
@@ -7500,6 +8116,22 @@ function updateTask(id, input, db) {
7500
8116
  approved_by: input.approved_by ?? task.approved_by,
7501
8117
  approved_at: input.approved_by ? timestamp : task.approved_at
7502
8118
  };
8119
+ if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
8120
+ const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
8121
+ dispatchWebhook2("task.assigned", payload, d).catch(() => {});
8122
+ emitLocalEventHooksQuiet({ type: "task.assigned", payload });
8123
+ emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to } });
8124
+ }
8125
+ if (input.status !== undefined && input.status !== task.status) {
8126
+ const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
8127
+ dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
8128
+ emitLocalEventHooksQuiet({ type: "task.status_changed", payload });
8129
+ emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status } });
8130
+ }
8131
+ if (input.approved_by !== undefined) {
8132
+ emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
8133
+ }
8134
+ return updatedTask;
7503
8135
  }
7504
8136
  function deleteTask(id, db) {
7505
8137
  const d = db || getDatabase();
@@ -8232,9 +8864,12 @@ function startTask(id, agentId, db) {
8232
8864
  throw new Error(`Task ${id} could not be started because it changed during claim`);
8233
8865
  }
8234
8866
  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 };
8867
+ 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 };
8868
+ const payload = taskEventData(startedTask, { agent_id: agentId });
8869
+ dispatchWebhook2("task.started", payload, d).catch(() => {});
8870
+ emitLocalEventHooksQuiet({ type: "task.started", payload });
8871
+ emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId } });
8872
+ return startedTask;
8238
8873
  }
8239
8874
  function completeTask(id, agentId, db, options) {
8240
8875
  const d = db || getDatabase();
@@ -8270,8 +8905,21 @@ function completeTask(id, agentId, db, options) {
8270
8905
  });
8271
8906
  tx();
8272
8907
  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 } });
8908
+ const completedTaskForEvent = {
8909
+ ...task,
8910
+ status: "completed",
8911
+ locked_by: null,
8912
+ locked_at: null,
8913
+ completed_at: timestamp,
8914
+ confidence,
8915
+ version: task.version + 1,
8916
+ updated_at: timestamp,
8917
+ metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
8918
+ };
8919
+ const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
8920
+ dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
8921
+ emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload });
8922
+ emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp } });
8275
8923
  let spawnedTask = null;
8276
8924
  if (task.recurrence_rule && !options?.skip_recurrence) {
8277
8925
  spawnedTask = spawnNextRecurrence(task, d, timestamp);
@@ -8312,8 +8960,12 @@ function completeTask(id, agentId, db, options) {
8312
8960
  if (unblockedDeps.length > 0) {
8313
8961
  meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
8314
8962
  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 } });
8963
+ const depTask = getTask(dep.id, d);
8964
+ const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
8965
+ dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
8966
+ emitLocalEventHooksQuiet({ type: "task.unblocked", payload });
8967
+ if (depTask)
8968
+ emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id } });
8317
8969
  }
8318
8970
  }
8319
8971
  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 +9151,6 @@ function failTask(id, agentId, reason, options, db) {
8499
9151
  const timestamp = now();
8500
9152
  d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
8501
9153
  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
9154
  const failedTask = {
8506
9155
  ...task,
8507
9156
  status: "failed",
@@ -8511,6 +9160,11 @@ function failTask(id, agentId, reason, options, db) {
8511
9160
  version: task.version + 1,
8512
9161
  updated_at: timestamp
8513
9162
  };
9163
+ logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
9164
+ const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
9165
+ dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
9166
+ emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload });
9167
+ emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning" });
8514
9168
  let retryTask;
8515
9169
  if (options?.retry) {
8516
9170
  const retryCount = (task.retry_count || 0) + 1;
@@ -8585,9 +9239,12 @@ function stealTask(agentId, opts, db) {
8585
9239
  return null;
8586
9240
  logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
8587
9241
  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 };
9242
+ const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
9243
+ const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
9244
+ dispatchWebhook2("task.assigned", payload, d).catch(() => {});
9245
+ emitLocalEventHooksQuiet({ type: "task.assigned", payload });
9246
+ emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to } });
9247
+ return stolenTask;
8591
9248
  }
8592
9249
  function claimOrSteal(agentId, filters, db) {
8593
9250
  const d = db || getDatabase();
@@ -9646,8 +10303,8 @@ init_database();
9646
10303
  init_database();
9647
10304
  init_redaction();
9648
10305
  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";
10306
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
10307
+ import { basename, dirname as dirname5, join as join6, resolve as resolve6 } from "path";
9651
10308
  import { tmpdir } from "os";
9652
10309
  function isInMemoryDb2(path) {
9653
10310
  return path === ":memory:" || path.startsWith("file::memory:");
@@ -9659,15 +10316,15 @@ function artifactStoreRoot() {
9659
10316
  return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
9660
10317
  const dbPath = getDatabasePath();
9661
10318
  if (isInMemoryDb2(dbPath))
9662
- return join5(tmpdir(), "hasna-todos-artifacts");
9663
- return join5(dirname5(resolve6(dbPath)), "artifacts");
10319
+ return join6(tmpdir(), "hasna-todos-artifacts");
10320
+ return join6(dirname5(resolve6(dbPath)), "artifacts");
9664
10321
  }
9665
10322
  function artifactStorePath(relativePath) {
9666
10323
  const normalized = relativePath.replace(/\\/g, "/");
9667
10324
  if (normalized.includes("..") || normalized.startsWith("/") || normalized.length === 0) {
9668
10325
  throw new Error("Invalid artifact store path");
9669
10326
  }
9670
- return join5(artifactStoreRoot(), normalized);
10327
+ return join6(artifactStoreRoot(), normalized);
9671
10328
  }
9672
10329
  function sha256(buffer) {
9673
10330
  return createHash2("sha256").update(buffer).digest("hex");
@@ -9708,7 +10365,7 @@ function mediaTypeFor(path, textLike) {
9708
10365
  }
9709
10366
  function storeArtifactContent(input) {
9710
10367
  const sourcePath = resolve6(input.path);
9711
- if (!existsSync7(sourcePath))
10368
+ if (!existsSync8(sourcePath))
9712
10369
  return null;
9713
10370
  const sourceStat = statSync2(sourcePath);
9714
10371
  if (!sourceStat.isFile())
@@ -9725,9 +10382,9 @@ function storeArtifactContent(input) {
9725
10382
  redactionStatus = "redacted";
9726
10383
  }
9727
10384
  const storedSha = sha256(storedBuffer);
9728
- const relativePath = join5("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
10385
+ const relativePath = join6("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
9729
10386
  const destination = artifactStorePath(relativePath);
9730
- if (!existsSync7(destination)) {
10387
+ if (!existsSync8(destination)) {
9731
10388
  mkdirSync4(dirname5(destination), { recursive: true });
9732
10389
  writeFileSync2(destination, storedBuffer);
9733
10390
  }
@@ -9787,7 +10444,7 @@ function verifyStoredArtifact(input) {
9787
10444
  };
9788
10445
  }
9789
10446
  const storedPath = artifactStorePath(store.relative_path);
9790
- if (!existsSync7(storedPath)) {
10447
+ if (!existsSync8(storedPath)) {
9791
10448
  return {
9792
10449
  id: input.id,
9793
10450
  path: input.path,
@@ -9868,15 +10525,15 @@ function getArtifactStoreRoot(dbPath) {
9868
10525
  return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
9869
10526
  const path = dbPath ?? getDatabasePath();
9870
10527
  if (isInMemoryDb2(path))
9871
- return join5(tmpdir(), "hasna-todos-artifacts");
9872
- return join5(dirname5(resolve6(path)), "artifacts");
10528
+ return join6(tmpdir(), "hasna-todos-artifacts");
10529
+ return join6(dirname5(resolve6(path)), "artifacts");
9873
10530
  }
9874
10531
  function computeContentHash(path) {
9875
10532
  return sha256(readFileSync4(resolve6(path)));
9876
10533
  }
9877
10534
  function storeArtifactFile(input) {
9878
10535
  const sourcePath = resolve6(input.sourcePath);
9879
- if (!existsSync7(sourcePath)) {
10536
+ if (!existsSync8(sourcePath)) {
9880
10537
  throw new Error(`Source file not found: ${input.sourcePath}`);
9881
10538
  }
9882
10539
  if (!statSync2(sourcePath).isFile()) {
@@ -9889,7 +10546,7 @@ function storeArtifactFile(input) {
9889
10546
  let localPath = sourcePath;
9890
10547
  if (storageMode === "copy") {
9891
10548
  const fileName = input.name && input.name.trim().length > 0 ? basename(input.name) : basename(sourcePath);
9892
- const destination = join5(getArtifactStoreRoot(input.dbPath), input.artifactId, fileName);
10549
+ const destination = join6(getArtifactStoreRoot(input.dbPath), input.artifactId, fileName);
9893
10550
  mkdirSync4(dirname5(destination), { recursive: true });
9894
10551
  writeFileSync2(destination, buffer);
9895
10552
  localPath = destination;
@@ -9899,7 +10556,7 @@ function storeArtifactFile(input) {
9899
10556
  function deleteStoredArtifactFile(localPath, storageMode, _dbPath2) {
9900
10557
  if (storageMode === "reference")
9901
10558
  return false;
9902
- if (!localPath || !existsSync7(localPath))
10559
+ if (!localPath || !existsSync8(localPath))
9903
10560
  return false;
9904
10561
  rmSync(localPath, { force: true });
9905
10562
  try {
@@ -9911,8 +10568,8 @@ function isArtifactExpired(deletedAt, policy = {}) {
9911
10568
  if (!deletedAt)
9912
10569
  return false;
9913
10570
  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();
10571
+ const now3 = policy.now ?? new Date;
10572
+ const ageMs = now3.getTime() - new Date(deletedAt).getTime();
9916
10573
  return ageMs > retentionDays * 24 * 60 * 60 * 1000;
9917
10574
  }
9918
10575
  function buildArtifactExportManifest(artifacts, dbPath) {
@@ -11678,7 +12335,7 @@ function writeOnboardingFixtureFiles(directory) {
11678
12335
  mkdirSync5(directory, { recursive: true });
11679
12336
  const files = [];
11680
12337
  for (const fixture of allFixtures()) {
11681
- const path = join6(directory, `${fixture.summary.name}.bridge.json`);
12338
+ const path = join7(directory, `${fixture.summary.name}.bridge.json`);
11682
12339
  writeFileSync3(path, `${JSON.stringify(fixture.bundle, null, 2)}
11683
12340
  `, "utf-8");
11684
12341
  files.push(path);
@@ -12504,7 +13161,7 @@ function renderLocalSnapshotMarkdown(snapshot) {
12504
13161
  }
12505
13162
  // src/lib/sdk-integration-fixtures.ts
12506
13163
  import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync5 } from "fs";
12507
- import { join as join7 } from "path";
13164
+ import { join as join8 } from "path";
12508
13165
 
12509
13166
  // src/cli-mcp-parity.ts
12510
13167
  function source4(version) {
@@ -13814,7 +14471,7 @@ function limits(input) {
13814
14471
  stale_after_hours: clamp(input.stale_after_hours, DEFAULT_LIMITS.stale_after_hours, 24 * 365)
13815
14472
  };
13816
14473
  }
13817
- function truncate(value, max) {
14474
+ function truncate2(value, max) {
13818
14475
  if (!value)
13819
14476
  return value ?? null;
13820
14477
  const redacted = redactEvidenceText(value);
@@ -13835,9 +14492,9 @@ function acceptanceCriteria(task2, maxText) {
13835
14492
  const metadata = task2.metadata || {};
13836
14493
  const raw = metadata["acceptance_criteria"] ?? metadata["acceptanceCriteria"] ?? metadata["criteria"];
13837
14494
  if (Array.isArray(raw))
13838
- return raw.map((item) => truncate(String(item), maxText)).filter((item) => Boolean(item));
14495
+ return raw.map((item) => truncate2(String(item), maxText)).filter((item) => Boolean(item));
13839
14496
  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));
14497
+ return raw.split(/\r?\n/).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).map((line) => truncate2(line, maxText)).filter((item) => Boolean(item));
13841
14498
  }
13842
14499
  return [];
13843
14500
  }
@@ -13860,7 +14517,7 @@ function addFile(files, path, source5, base) {
13860
14517
  path,
13861
14518
  status: base?.status || "active",
13862
14519
  agent_id: base?.agent_id ?? null,
13863
- note: truncate(base?.note, 240),
14520
+ note: truncate2(base?.note, 240),
13864
14521
  updated_at: base?.updated_at || "",
13865
14522
  sources: [source5]
13866
14523
  });
@@ -13918,7 +14575,7 @@ function estimateTokens(value) {
13918
14575
  return Math.max(1, Math.ceil((text || "").length / 4));
13919
14576
  }
13920
14577
  function summarizeStrings(values, maxChars) {
13921
- return truncate(values.filter(Boolean).join("; "), maxChars) || "No local details were available before this section was omitted.";
14578
+ return truncate2(values.filter(Boolean).join("; "), maxChars) || "No local details were available before this section was omitted.";
13922
14579
  }
13923
14580
  function summarizeSection(pack, section, maxChars) {
13924
14581
  if (section === "project")
@@ -14109,8 +14766,8 @@ function createAgentContextPack(input, db) {
14109
14766
  ...taskFiles.map((file) => file.updated_at)
14110
14767
  ], task2.updated_at);
14111
14768
  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) {
14769
+ const now3 = input.now ? new Date(input.now) : new Date;
14770
+ if (Date.parse(task2.updated_at) < now3.getTime() - limit.stale_after_hours * 60 * 60 * 1000) {
14114
14771
  warnings.push(`task state is older than ${limit.stale_after_hours} hours`);
14115
14772
  }
14116
14773
  if (comments.length > recentComments.length)
@@ -14125,7 +14782,7 @@ function createAgentContextPack(input, db) {
14125
14782
  id: task2.id,
14126
14783
  short_id: task2.short_id,
14127
14784
  title: redactEvidenceText(task2.title),
14128
- description: truncate(task2.description, limit.max_text_chars),
14785
+ description: truncate2(task2.description, limit.max_text_chars),
14129
14786
  status: task2.status,
14130
14787
  priority: task2.priority,
14131
14788
  assigned_to: task2.assigned_to,
@@ -14149,7 +14806,7 @@ function createAgentContextPack(input, db) {
14149
14806
  plan: plan ? {
14150
14807
  id: plan.id,
14151
14808
  name: plan.name,
14152
- description: truncate(plan.description, limit.max_text_chars),
14809
+ description: truncate2(plan.description, limit.max_text_chars),
14153
14810
  status: plan.status,
14154
14811
  agent_id: plan.agent_id,
14155
14812
  tasks: planTasks.slice(0, limit.plan_task_limit).map(taskSummary).filter((item) => Boolean(item)),
@@ -14168,7 +14825,7 @@ function createAgentContextPack(input, db) {
14168
14825
  type: comment.type,
14169
14826
  progress_pct: comment.progress_pct,
14170
14827
  created_at: comment.created_at,
14171
- content: truncate(comment.content, limit.max_text_chars) || ""
14828
+ content: truncate2(comment.content, limit.max_text_chars) || ""
14172
14829
  })),
14173
14830
  omitted: Math.max(0, comments.length - recentComments.length)
14174
14831
  },
@@ -14176,7 +14833,7 @@ function createAgentContextPack(input, db) {
14176
14833
  traceability: {
14177
14834
  commits: traceability.commits.map((commit) => ({
14178
14835
  sha: commit.sha,
14179
- message: truncate(commit.message, 240),
14836
+ message: truncate2(commit.message, 240),
14180
14837
  files_changed: commit.files_changed,
14181
14838
  committed_at: commit.committed_at
14182
14839
  })),
@@ -14184,7 +14841,7 @@ function createAgentContextPack(input, db) {
14184
14841
  verifications: verifications.map((verification) => ({
14185
14842
  command: verification.command,
14186
14843
  status: verification.status,
14187
- output_summary: truncate(verification.output_summary, limit.max_text_chars),
14844
+ output_summary: truncate2(verification.output_summary, limit.max_text_chars),
14188
14845
  artifact_path: verification.artifact_path,
14189
14846
  run_at: verification.run_at
14190
14847
  })),
@@ -14195,14 +14852,14 @@ function createAgentContextPack(input, db) {
14195
14852
  id: ledger.run.id,
14196
14853
  title: ledger.run.title,
14197
14854
  status: ledger.run.status,
14198
- summary: truncate(ledger.run.summary, limit.max_text_chars),
14855
+ summary: truncate2(ledger.run.summary, limit.max_text_chars),
14199
14856
  agent_id: ledger.run.agent_id,
14200
14857
  started_at: ledger.run.started_at,
14201
14858
  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 }))
14859
+ events: ledger.events.map((event) => ({ event_type: event.event_type, message: truncate2(event.message, 500), created_at: event.created_at })),
14860
+ 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 })),
14861
+ files: ledger.files.map((file) => ({ path: file.path, status: file.status, note: truncate2(file.note, 240) })),
14862
+ artifacts: ledger.artifacts.map((artifact) => ({ path: artifact.path, artifact_type: artifact.artifact_type, description: truncate2(artifact.description, 240), sha256: artifact.sha256 }))
14206
14863
  })),
14207
14864
  omitted: Math.max(0, runs.length - selectedRuns.length)
14208
14865
  },
@@ -14307,7 +14964,7 @@ function renderAgentContextPackCompactMarkdown(pack) {
14307
14964
  const lines = [
14308
14965
  `# Context: ${pack.task.title}`,
14309
14966
  `${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,
14967
+ pack.task.description ? truncate2(pack.task.description, Math.min(pack.limits.summary_char_limit, 700)) : null,
14311
14968
  "",
14312
14969
  "## Must Know",
14313
14970
  bullet([
@@ -14453,7 +15110,7 @@ function writeSdkIntegrationFixtures(directory, options = {}) {
14453
15110
  ];
14454
15111
  const written = [];
14455
15112
  for (const [name, payload] of files) {
14456
- const file = join7(directory, name);
15113
+ const file = join8(directory, name);
14457
15114
  writeFileSync5(file, `${JSON.stringify(payload, null, 2)}
14458
15115
  `, "utf-8");
14459
15116
  written.push(file);
@@ -15170,7 +15827,7 @@ function createRoadmap(input) {
15170
15827
  if (!name)
15171
15828
  throw new Error("Roadmap name is required");
15172
15829
  const store = readStore();
15173
- const now2 = timestamp();
15830
+ const now3 = timestamp();
15174
15831
  const roadmap = {
15175
15832
  id: newId("roadmap"),
15176
15833
  name,
@@ -15181,8 +15838,8 @@ function createRoadmap(input) {
15181
15838
  agent_id: cleanString(input.agent_id),
15182
15839
  release: cleanString(input.release),
15183
15840
  milestone_ids: [],
15184
- created_at: now2,
15185
- updated_at: now2
15841
+ created_at: now3,
15842
+ updated_at: now3
15186
15843
  };
15187
15844
  store.roadmaps[roadmap.id] = roadmap;
15188
15845
  writeStore(store);
@@ -15242,7 +15899,7 @@ function createMilestone(input) {
15242
15899
  const title = input.title.trim();
15243
15900
  if (!title)
15244
15901
  throw new Error("Milestone title is required");
15245
- const now2 = timestamp();
15902
+ const now3 = timestamp();
15246
15903
  const milestone = {
15247
15904
  id: newId("milestone"),
15248
15905
  roadmap_id: roadmapId,
@@ -15257,11 +15914,11 @@ function createMilestone(input) {
15257
15914
  run_ids: cleanList3(input.run_ids),
15258
15915
  release: cleanString(input.release ?? roadmap.release ?? undefined),
15259
15916
  tags: cleanList3(input.tags),
15260
- created_at: now2,
15261
- updated_at: now2
15917
+ created_at: now3,
15918
+ updated_at: now3
15262
15919
  };
15263
15920
  store.milestones[milestone.id] = milestone;
15264
- store.roadmaps[roadmapId] = { ...roadmap, milestone_ids: cleanList3([...roadmap.milestone_ids, milestone.id]), updated_at: now2 };
15921
+ store.roadmaps[roadmapId] = { ...roadmap, milestone_ids: cleanList3([...roadmap.milestone_ids, milestone.id]), updated_at: now3 };
15265
15922
  writeStore(store);
15266
15923
  return milestone;
15267
15924
  }
@@ -15321,7 +15978,7 @@ function upsertReleaseGroup(input) {
15321
15978
  throw new Error("Release group name is required");
15322
15979
  const key = releaseKey(roadmapId, name);
15323
15980
  const existing = store.releases[key];
15324
- const now2 = timestamp();
15981
+ const now3 = timestamp();
15325
15982
  const release = {
15326
15983
  name,
15327
15984
  version: input.version === undefined ? existing?.version ?? null : cleanString(input.version),
@@ -15332,8 +15989,8 @@ function upsertReleaseGroup(input) {
15332
15989
  plan_ids: input.plan_ids === undefined ? existing?.plan_ids ?? [] : cleanList3(input.plan_ids),
15333
15990
  run_ids: input.run_ids === undefined ? existing?.run_ids ?? [] : cleanList3(input.run_ids),
15334
15991
  notes: input.notes === undefined ? existing?.notes ?? null : cleanString(input.notes),
15335
- created_at: existing?.created_at ?? now2,
15336
- updated_at: now2
15992
+ created_at: existing?.created_at ?? now3,
15993
+ updated_at: now3
15337
15994
  };
15338
15995
  store.releases[key] = release;
15339
15996
  writeStore(store);
@@ -15724,7 +16381,7 @@ function renderLocalAuditLedgerMarkdown(ledger) {
15724
16381
  init_migrations();
15725
16382
  init_schema();
15726
16383
  import { readFileSync as readFileSync6 } from "fs";
15727
- import { join as join8, resolve as resolve8 } from "path";
16384
+ import { join as join9, resolve as resolve8 } from "path";
15728
16385
  import { Database as Database2 } from "bun:sqlite";
15729
16386
  var LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION = 1;
15730
16387
  var EXPECTED_PACKAGE_NAME = "@hasna/todos";
@@ -15770,7 +16427,7 @@ function warn(id, message, details) {
15770
16427
  return { id, status: "warning", message, details };
15771
16428
  }
15772
16429
  function readPackageJson(root) {
15773
- return JSON.parse(readFileSync6(join8(root, "package.json"), "utf8"));
16430
+ return JSON.parse(readFileSync6(join9(root, "package.json"), "utf8"));
15774
16431
  }
15775
16432
  function sortedKeys(value) {
15776
16433
  return Object.keys(value ?? {}).sort((left, right) => left.localeCompare(right));
@@ -17182,6 +17839,470 @@ function importExternalIssues(input, db) {
17182
17839
  ]
17183
17840
  };
17184
17841
  }
17842
+ // src/lib/tester-issue-reports.ts
17843
+ init_database();
17844
+ import { createHash as createHash7 } from "crypto";
17845
+ init_redaction();
17846
+ var TESTERS_ISSUE_REPORT_SCHEMA_VERSION = "testers.issue_report.v1";
17847
+ var TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION = "todos.tester_issue_report_result.v1";
17848
+ var TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION = "todos.tester_issue_report_batch_result.v1";
17849
+ var PRIORITIES3 = ["low", "medium", "high", "critical"];
17850
+ var SEVERITIES = new Set(PRIORITIES3);
17851
+ var KINDS = new Set([
17852
+ "assertion_failure",
17853
+ "runtime_error",
17854
+ "console_error",
17855
+ "network_error",
17856
+ "visual_regression",
17857
+ "accessibility",
17858
+ "performance",
17859
+ "broken_link",
17860
+ "security",
17861
+ "unknown"
17862
+ ]);
17863
+ function asObject3(value) {
17864
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
17865
+ }
17866
+ function asString3(value) {
17867
+ if (typeof value === "string" && value.trim())
17868
+ return value.trim();
17869
+ if (typeof value === "number" && Number.isFinite(value))
17870
+ return String(value);
17871
+ return null;
17872
+ }
17873
+ function stringArray(value, limit = 20) {
17874
+ if (!Array.isArray(value))
17875
+ return [];
17876
+ return [...new Set(value.map((item) => asString3(item)).filter((item) => Boolean(item)))].slice(0, limit);
17877
+ }
17878
+ function objectArray(value, limit = 20) {
17879
+ if (!Array.isArray(value))
17880
+ return [];
17881
+ return value.map(asObject3).filter((item) => Object.keys(item).length > 0).slice(0, limit);
17882
+ }
17883
+ function cleanKey(value) {
17884
+ return value.toLowerCase().trim().replace(/[^a-z0-9._:-]+/g, "-").replace(/^-+|-+$/g, "");
17885
+ }
17886
+ function truncate3(value, max) {
17887
+ if (!value)
17888
+ return;
17889
+ const redacted = redactEvidenceText(value).trim();
17890
+ if (!redacted)
17891
+ return;
17892
+ return redacted.length > max ? `${redacted.slice(0, max - 3)}...` : redacted;
17893
+ }
17894
+ function normalizeText2(value) {
17895
+ return (value || "").toLowerCase().replace(/https?:\/\/\S+/g, " ").replace(/[`"'()[\]{}.,:;!?/#\\_-]+/g, " ").replace(/\s+/g, " ").trim();
17896
+ }
17897
+ function normalizeUrlPattern(value) {
17898
+ if (!value)
17899
+ return "";
17900
+ try {
17901
+ const url = new URL(value);
17902
+ return `${url.origin.toLowerCase()}${url.pathname.replace(/\/+$/, "") || "/"}`;
17903
+ } catch {
17904
+ return value.split(/[?#]/, 1)[0].replace(/\/+$/, "").toLowerCase();
17905
+ }
17906
+ }
17907
+ function normalizeKind(value) {
17908
+ const raw = cleanKey(asString3(value) || "unknown").replace(/-/g, "_");
17909
+ return KINDS.has(raw) ? raw : raw || "unknown";
17910
+ }
17911
+ function normalizeSeverity(value, fallback) {
17912
+ const raw = cleanKey(asString3(value) || fallback);
17913
+ if (SEVERITIES.has(raw))
17914
+ return raw;
17915
+ if (/^(p0|blocker|urgent|highest)$/.test(raw))
17916
+ return "critical";
17917
+ if (/^(p1|major)$/.test(raw))
17918
+ return "high";
17919
+ if (/^(p3|minor|info)$/.test(raw))
17920
+ return "low";
17921
+ return fallback;
17922
+ }
17923
+ function reportSource(input) {
17924
+ const source6 = asObject3(input["source"]);
17925
+ const normalized = {
17926
+ tool: asString3(source6["tool"]) || asString3(input["tool"]) || "testers",
17927
+ run_id: asString3(source6["run_id"]) || asString3(input["run_id"]) || undefined,
17928
+ result_id: asString3(source6["result_id"]) || asString3(input["result_id"]) || undefined,
17929
+ scenario_id: asString3(source6["scenario_id"]) || asString3(input["scenario_id"]) || undefined,
17930
+ scenario_name: asString3(source6["scenario_name"]) || asString3(input["scenario_name"]) || undefined,
17931
+ project_id: asString3(source6["project_id"]) || asString3(input["project_id"]) || undefined,
17932
+ url: asString3(source6["url"]) || asString3(input["url"]) || undefined,
17933
+ page_url: asString3(source6["page_url"]) || asString3(input["page_url"]) || undefined,
17934
+ artifact_url: asString3(source6["artifact_url"]) || undefined,
17935
+ screenshot_url: asString3(source6["screenshot_url"]) || undefined,
17936
+ commit: asString3(source6["commit"]) || undefined,
17937
+ branch: asString3(source6["branch"]) || undefined
17938
+ };
17939
+ return Object.values(normalized).some(Boolean) ? normalized : undefined;
17940
+ }
17941
+ function reportTarget(input) {
17942
+ const target = asObject3(input["target"]);
17943
+ const normalized = {
17944
+ url: asString3(target["url"]) || asString3(input["target_url"]) || undefined,
17945
+ route: asString3(target["route"]) || undefined,
17946
+ selector: asString3(target["selector"]) || undefined,
17947
+ component: asString3(target["component"]) || undefined,
17948
+ browser: asString3(target["browser"]) || undefined,
17949
+ viewport: asString3(target["viewport"]) || undefined
17950
+ };
17951
+ return Object.values(normalized).some(Boolean) ? normalized : undefined;
17952
+ }
17953
+ function reportFailure(input) {
17954
+ const failure = asObject3(input["failure"]);
17955
+ const steps = stringArray(failure["steps"] ?? input["steps"], 50);
17956
+ const normalized = {
17957
+ message: truncate3(asString3(failure["message"]) || asString3(input["error"]) || asString3(input["message"]), 1000),
17958
+ expected: truncate3(asString3(failure["expected"]), 1000),
17959
+ actual: truncate3(asString3(failure["actual"]), 1000),
17960
+ stack: truncate3(asString3(failure["stack"]) || asString3(input["stack"]), 3000),
17961
+ reasoning: truncate3(asString3(failure["reasoning"]) || asString3(input["reasoning"]), 1500),
17962
+ steps: steps.length > 0 ? steps.map((step) => truncate3(step, 400)).filter(Boolean) : undefined
17963
+ };
17964
+ return Object.values(normalized).some(Boolean) ? normalized : undefined;
17965
+ }
17966
+ function artifactArray(value) {
17967
+ return objectArray(value, 12).map((item) => ({
17968
+ kind: asString3(item["kind"]) || undefined,
17969
+ label: asString3(item["label"]) || asString3(item["name"]) || undefined,
17970
+ path: asString3(item["path"]) || asString3(item["file_path"]) || undefined,
17971
+ url: asString3(item["url"]) || undefined
17972
+ })).filter((item) => item.kind || item.label || item.path || item.url);
17973
+ }
17974
+ function reportEvidence(input) {
17975
+ const evidence = asObject3(input["evidence"]);
17976
+ const logs = stringArray(evidence["logs"], 8).map((log) => truncate3(log, 1000)).filter(Boolean);
17977
+ const screenshots = artifactArray(evidence["screenshots"] ?? input["screenshots"]);
17978
+ const artifacts = artifactArray(evidence["artifacts"] ?? input["artifacts"]);
17979
+ const normalized = {
17980
+ logs: logs.length > 0 ? logs : undefined,
17981
+ screenshots: screenshots.length > 0 ? screenshots : undefined,
17982
+ artifacts: artifacts.length > 0 ? artifacts : undefined
17983
+ };
17984
+ return Object.values(normalized).some(Boolean) ? normalized : undefined;
17985
+ }
17986
+ function normalizeTesterIssueReport(value, fallbackPriority = "medium") {
17987
+ const input = asObject3(value);
17988
+ if (input["schema_version"] !== TESTERS_ISSUE_REPORT_SCHEMA_VERSION) {
17989
+ throw new Error(`Expected schema_version ${TESTERS_ISSUE_REPORT_SCHEMA_VERSION}`);
17990
+ }
17991
+ const failure = reportFailure(input);
17992
+ const source6 = reportSource(input);
17993
+ const target = reportTarget(input);
17994
+ const title = truncate3(asString3(input["title"]) || asString3(input["summary"]) || failure?.message || source6?.scenario_name || "Tester issue report", 220);
17995
+ if (!title)
17996
+ throw new Error("Tester issue report requires a title");
17997
+ const labels = [
17998
+ ...stringArray(input["labels"], 20),
17999
+ ...stringArray(input["tags"], 20)
18000
+ ].map(cleanKey).filter(Boolean);
18001
+ const report = {
18002
+ schema_version: TESTERS_ISSUE_REPORT_SCHEMA_VERSION,
18003
+ id: asString3(input["id"]) || undefined,
18004
+ fingerprint: asString3(input["fingerprint"]) || undefined,
18005
+ title,
18006
+ summary: truncate3(asString3(input["summary"]), 1000) ?? null,
18007
+ kind: normalizeKind(input["kind"] ?? input["type"]),
18008
+ severity: normalizeSeverity(input["severity"] ?? input["priority"], fallbackPriority),
18009
+ source: source6,
18010
+ target,
18011
+ failure,
18012
+ evidence: reportEvidence(input),
18013
+ labels: labels.length > 0 ? [...new Set(labels)].slice(0, 20) : undefined,
18014
+ metadata: redactValue(asObject3(input["metadata"])),
18015
+ occurred_at: asString3(input["occurred_at"]) || asString3(input["timestamp"]) || undefined
18016
+ };
18017
+ return redactValue(report);
18018
+ }
18019
+ function fingerprintTesterIssueReport(report) {
18020
+ if (report.fingerprint)
18021
+ return `testers:${cleanKey(report.fingerprint)}`;
18022
+ const stackTop = report.failure?.stack?.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || "";
18023
+ const url = normalizeUrlPattern(report.target?.url || report.source?.page_url || report.source?.url || "");
18024
+ const raw = [
18025
+ report.kind || "unknown",
18026
+ report.source?.project_id || "",
18027
+ report.source?.scenario_id || report.source?.scenario_name || "",
18028
+ url,
18029
+ report.target?.route || "",
18030
+ report.target?.selector || report.target?.component || "",
18031
+ normalizeText2(report.failure?.message || report.summary || report.title).slice(0, 240),
18032
+ normalizeText2(stackTop).slice(0, 160)
18033
+ ].join("::");
18034
+ return `testers:${createHash7("sha256").update(raw).digest("hex").slice(0, 16)}`;
18035
+ }
18036
+ function priorityForSeverity(severity, fallback) {
18037
+ return PRIORITIES3.includes(severity) ? severity : fallback;
18038
+ }
18039
+ function maxPriority(left, right) {
18040
+ return PRIORITIES3.indexOf(right) > PRIORITIES3.indexOf(left) ? right : left;
18041
+ }
18042
+ function taskTitle2(report) {
18043
+ const title = report.title.replace(/^BUG:\s*/i, "").replace(/^\[testers\]\s*/i, "");
18044
+ return `BUG: [testers] ${title}`.slice(0, 240);
18045
+ }
18046
+ function evidenceLines(report) {
18047
+ const lines = [];
18048
+ for (const item of report.evidence?.screenshots || []) {
18049
+ lines.push(`Screenshot: ${item.label || item.kind || item.path || item.url}${item.path ? ` (${item.path})` : item.url ? ` (${item.url})` : ""}`);
18050
+ }
18051
+ for (const item of report.evidence?.artifacts || []) {
18052
+ lines.push(`Artifact: ${item.label || item.kind || item.path || item.url}${item.path ? ` (${item.path})` : item.url ? ` (${item.url})` : ""}`);
18053
+ }
18054
+ for (const log of report.evidence?.logs || [])
18055
+ lines.push(`Log: ${log}`);
18056
+ return lines.slice(0, 12);
18057
+ }
18058
+ function taskDescription2(report, fingerprint2) {
18059
+ const failure = report.failure;
18060
+ const lines = [
18061
+ "Tester issue report.",
18062
+ "",
18063
+ `Schema: ${report.schema_version}`,
18064
+ `Fingerprint: ${fingerprint2}`,
18065
+ `Kind: ${report.kind || "unknown"}`,
18066
+ `Severity: ${report.severity || "medium"}`,
18067
+ report.source?.run_id ? `Run: ${report.source.run_id}` : null,
18068
+ report.source?.result_id ? `Result: ${report.source.result_id}` : null,
18069
+ report.source?.scenario_name || report.source?.scenario_id ? `Scenario: ${report.source.scenario_name || report.source.scenario_id}` : null,
18070
+ report.target?.url || report.source?.page_url || report.source?.url ? `URL: ${report.target?.url || report.source?.page_url || report.source?.url}` : null,
18071
+ report.target?.route ? `Route: ${report.target.route}` : null,
18072
+ report.target?.selector ? `Selector: ${report.target.selector}` : null,
18073
+ report.occurred_at ? `Occurred at: ${report.occurred_at}` : null,
18074
+ "",
18075
+ report.summary ? `Summary:
18076
+ ${report.summary}` : null,
18077
+ failure?.message ? `Failure:
18078
+ ${failure.message}` : null,
18079
+ failure?.expected ? `Expected:
18080
+ ${failure.expected}` : null,
18081
+ failure?.actual ? `Actual:
18082
+ ${failure.actual}` : null,
18083
+ failure?.reasoning ? `Reasoning:
18084
+ ${failure.reasoning}` : null,
18085
+ failure?.steps?.length ? `Steps:
18086
+ ${failure.steps.map((step, index) => `${index + 1}. ${step}`).join(`
18087
+ `)}` : null,
18088
+ evidenceLines(report).length ? `Evidence:
18089
+ ${evidenceLines(report).map((line) => `- ${line}`).join(`
18090
+ `)}` : null,
18091
+ failure?.stack ? `Stack:
18092
+ ${failure.stack}` : null
18093
+ ].filter((line) => line !== null);
18094
+ return lines.join(`
18095
+ `).replace(/\n{3,}/g, `
18096
+
18097
+ `).slice(0, 6000);
18098
+ }
18099
+ function taskTags(report) {
18100
+ return [...new Set([
18101
+ "bug",
18102
+ "testers",
18103
+ "tester-report",
18104
+ report.kind ? cleanKey(String(report.kind)).replace(/_/g, "-") : "unknown",
18105
+ ...report.labels || []
18106
+ ].filter(Boolean))].slice(0, 16);
18107
+ }
18108
+ function storedReportSummary(report) {
18109
+ return {
18110
+ id: report.id ?? null,
18111
+ title: report.title,
18112
+ kind: report.kind ?? "unknown",
18113
+ severity: report.severity ?? "medium",
18114
+ run_id: report.source?.run_id ?? null,
18115
+ result_id: report.source?.result_id ?? null,
18116
+ scenario_id: report.source?.scenario_id ?? null,
18117
+ scenario_name: report.source?.scenario_name ?? null,
18118
+ url: report.target?.url ?? report.source?.page_url ?? report.source?.url ?? null,
18119
+ occurred_at: report.occurred_at ?? null
18120
+ };
18121
+ }
18122
+ function testerMetadata(report, fingerprint2, previous, timestamp2) {
18123
+ const occurrenceCount = typeof previous?.["occurrence_count"] === "number" ? previous["occurrence_count"] + 1 : 1;
18124
+ const previousRecent = previous?.["recent_reports"];
18125
+ const recent = Array.isArray(previousRecent) ? previousRecent : [];
18126
+ return {
18127
+ schema_version: TESTERS_ISSUE_REPORT_SCHEMA_VERSION,
18128
+ fingerprint: fingerprint2,
18129
+ first_seen_at: asString3(previous?.["first_seen_at"]) || timestamp2,
18130
+ last_seen_at: timestamp2,
18131
+ occurrence_count: occurrenceCount,
18132
+ latest_report: storedReportSummary(report),
18133
+ recent_reports: [...recent.slice(-4), storedReportSummary(report)]
18134
+ };
18135
+ }
18136
+ function sourceMetadata(report) {
18137
+ const url = report.target?.url || report.source?.page_url || report.source?.url || null;
18138
+ return {
18139
+ ...url ? { source_url: url, external_url: url, issue_url: url } : {},
18140
+ ...report.source?.run_id ? { tester_run_id: report.source.run_id } : {},
18141
+ ...report.source?.result_id ? { tester_result_id: report.source.result_id } : {},
18142
+ ...report.source?.scenario_id ? { tester_scenario_id: report.source.scenario_id } : {},
18143
+ ...report.source?.project_id ? { tester_project_id: report.source.project_id } : {}
18144
+ };
18145
+ }
18146
+ function findExistingTask2(fingerprint2, input, db) {
18147
+ for (const task2 of listTasks({
18148
+ include_archived: true,
18149
+ project_id: input.project_id,
18150
+ task_list_id: input.task_list_id
18151
+ }, db)) {
18152
+ const metadata = task2.metadata || {};
18153
+ const tester = asObject3(metadata["tester_issue_report"]);
18154
+ if (tester["fingerprint"] === fingerprint2)
18155
+ return task2;
18156
+ if (metadata["tester_issue_fingerprint"] === fingerprint2)
18157
+ return task2;
18158
+ if (metadata["external_ref"] === fingerprint2)
18159
+ return task2;
18160
+ }
18161
+ return null;
18162
+ }
18163
+ function commandsFor(task2) {
18164
+ return [
18165
+ "todos issues report --file tester-report.json --apply --json",
18166
+ task2 ? `todos show ${task2.id.slice(0, 8)}` : `todos list --tags tester-report --json`,
18167
+ `todos dedupe scan --threshold 0.8 --json`
18168
+ ];
18169
+ }
18170
+ function updateExistingTask(task2, report, fingerprint2, input, timestamp2, db) {
18171
+ if (input.update_existing === false)
18172
+ return { action: "matched", task: task2 };
18173
+ const previous = asObject3(task2.metadata["tester_issue_report"]);
18174
+ const severityPriority = priorityForSeverity(report.severity, input.default_priority || "medium");
18175
+ const nextStatus = task2.status === "completed" || task2.status === "cancelled" ? "pending" : task2.status;
18176
+ const action = nextStatus !== task2.status ? "regressed" : "updated";
18177
+ const updated = updateTask(task2.id, {
18178
+ version: task2.version,
18179
+ title: taskTitle2(report),
18180
+ description: taskDescription2(report, fingerprint2),
18181
+ priority: maxPriority(task2.priority, severityPriority),
18182
+ status: nextStatus,
18183
+ completed_at: nextStatus !== task2.status ? null : undefined,
18184
+ tags: [...new Set([...task2.tags, ...taskTags(report)])],
18185
+ metadata: {
18186
+ ...task2.metadata,
18187
+ ...sourceMetadata(report),
18188
+ external_ref: fingerprint2,
18189
+ tester_issue_fingerprint: fingerprint2,
18190
+ tester_issue_report: testerMetadata(report, fingerprint2, previous, timestamp2)
18191
+ },
18192
+ ...input.assigned_to !== undefined ? { assigned_to: input.assigned_to } : {},
18193
+ task_type: task2.task_type || "bug"
18194
+ }, db);
18195
+ return { action, task: updated };
18196
+ }
18197
+ function upsertTesterIssueReport(input, db) {
18198
+ const d = db || getDatabase();
18199
+ const timestamp2 = now();
18200
+ const warnings = [];
18201
+ const report = normalizeTesterIssueReport(input.report, input.default_priority || "medium");
18202
+ const fingerprint2 = fingerprintTesterIssueReport(report);
18203
+ const existing = findExistingTask2(fingerprint2, input, d);
18204
+ if (!input.apply) {
18205
+ const action = existing ? "matched" : "preview";
18206
+ return {
18207
+ schema_version: TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
18208
+ local_only: true,
18209
+ dry_run: true,
18210
+ processed_at: timestamp2,
18211
+ action,
18212
+ fingerprint: fingerprint2,
18213
+ report,
18214
+ task: existing,
18215
+ warnings,
18216
+ commands: commandsFor(existing)
18217
+ };
18218
+ }
18219
+ if (existing) {
18220
+ const updated = updateExistingTask(existing, report, fingerprint2, input, timestamp2, d);
18221
+ return {
18222
+ schema_version: TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
18223
+ local_only: true,
18224
+ dry_run: false,
18225
+ processed_at: timestamp2,
18226
+ action: updated.action,
18227
+ fingerprint: fingerprint2,
18228
+ report,
18229
+ task: updated.task,
18230
+ warnings,
18231
+ commands: commandsFor(updated.task)
18232
+ };
18233
+ }
18234
+ const priority = priorityForSeverity(report.severity, input.default_priority || "medium");
18235
+ const task2 = createTask({
18236
+ title: taskTitle2(report),
18237
+ description: taskDescription2(report, fingerprint2),
18238
+ priority,
18239
+ status: "pending",
18240
+ tags: taskTags(report),
18241
+ metadata: {
18242
+ ...sourceMetadata(report),
18243
+ external_ref: fingerprint2,
18244
+ tester_issue_fingerprint: fingerprint2,
18245
+ tester_issue_report: testerMetadata(report, fingerprint2, null, timestamp2),
18246
+ tester_issue_report_raw: redactValue({
18247
+ ...report,
18248
+ evidence: report.evidence ? {
18249
+ screenshots: report.evidence.screenshots,
18250
+ artifacts: report.evidence.artifacts
18251
+ } : undefined
18252
+ })
18253
+ },
18254
+ project_id: input.project_id,
18255
+ task_list_id: input.task_list_id,
18256
+ agent_id: input.agent_id,
18257
+ assigned_to: input.assigned_to,
18258
+ task_type: "bug"
18259
+ }, d);
18260
+ return {
18261
+ schema_version: TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
18262
+ local_only: true,
18263
+ dry_run: false,
18264
+ processed_at: timestamp2,
18265
+ action: "created",
18266
+ fingerprint: fingerprint2,
18267
+ report,
18268
+ task: task2,
18269
+ warnings,
18270
+ commands: commandsFor(task2)
18271
+ };
18272
+ }
18273
+ function upsertTesterIssueReports(input, db) {
18274
+ const d = db || getDatabase();
18275
+ const run = () => input.reports.map((report) => upsertTesterIssueReport({ ...input, report }, d));
18276
+ const results = input.apply ? d.transaction(run)() : run();
18277
+ const summary = {
18278
+ total: results.length,
18279
+ preview: 0,
18280
+ matched: 0,
18281
+ created: 0,
18282
+ updated: 0,
18283
+ regressed: 0
18284
+ };
18285
+ for (const result of results)
18286
+ summary[result.action]++;
18287
+ return {
18288
+ schema_version: TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION,
18289
+ local_only: true,
18290
+ dry_run: !input.apply,
18291
+ processed_at: now(),
18292
+ results,
18293
+ summary
18294
+ };
18295
+ }
18296
+ function readTesterIssueReportsPayload(value) {
18297
+ if (Array.isArray(value))
18298
+ return value;
18299
+ const record = asObject3(value);
18300
+ if (Array.isArray(record["reports"]))
18301
+ return record["reports"];
18302
+ if (Array.isArray(record["issues"]))
18303
+ return record["issues"];
18304
+ return [value];
18305
+ }
17185
18306
  // src/lib/local-notifications.ts
17186
18307
  init_database();
17187
18308
 
@@ -17284,13 +18405,13 @@ function eventSeverity(eventType) {
17284
18405
  function payloadText(payload) {
17285
18406
  return JSON.stringify(payload).toLowerCase();
17286
18407
  }
17287
- function asString3(value) {
18408
+ function asString4(value) {
17288
18409
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
17289
18410
  }
17290
18411
  function fieldMatches(allowed, value) {
17291
18412
  if (!allowed || allowed.length === 0)
17292
18413
  return true;
17293
- const stringValue = asString3(value);
18414
+ const stringValue = asString4(value);
17294
18415
  return Boolean(stringValue && allowed.includes(stringValue));
17295
18416
  }
17296
18417
  function containsMatches(needles, payload) {
@@ -17356,8 +18477,8 @@ function evaluateTerminalWatchRules(input, rules = listTerminalNotificationRules
17356
18477
  if (isQuietTime(rule.quiet_hours, timestamp2))
17357
18478
  skipped.push("quiet hours active");
17358
18479
  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"]);
18480
+ const title = asString4(payload["title"]) || asString4(payload["name"]) || input.type;
18481
+ const taskId = asString4(payload["id"]) || asString4(payload["task_id"]);
17361
18482
  const notification = {
17362
18483
  rule: rule.name,
17363
18484
  event_type: input.type,
@@ -17366,8 +18487,8 @@ function evaluateTerminalWatchRules(input, rules = listTerminalNotificationRules
17366
18487
  message: `${input.type}: ${title}`,
17367
18488
  timestamp: timestamp2,
17368
18489
  task_id: taskId,
17369
- project_id: asString3(payload["project_id"]),
17370
- agent_id: asString3(payload["agent_id"]) || asString3(payload["assigned_to"]),
18490
+ project_id: asString4(payload["project_id"]),
18491
+ agent_id: asString4(payload["agent_id"]) || asString4(payload["assigned_to"]),
17371
18492
  bell: rule.bell && severity === "critical",
17372
18493
  payload
17373
18494
  };
@@ -18017,12 +19138,12 @@ function summarizeTask(task2) {
18017
19138
  };
18018
19139
  }
18019
19140
  function overdueTasks(tasks, nowIso) {
18020
- const now2 = Date.parse(nowIso);
19141
+ const now3 = Date.parse(nowIso);
18021
19142
  return tasks.filter((task2) => {
18022
19143
  if (isTerminal(task2) || !task2.due_at)
18023
19144
  return false;
18024
19145
  const due = Date.parse(task2.due_at);
18025
- return Number.isFinite(due) && due < now2;
19146
+ return Number.isFinite(due) && due < now3;
18026
19147
  });
18027
19148
  }
18028
19149
  function isReady(task2, db) {
@@ -18355,7 +19476,7 @@ function renderLocalReportMarkdown(report) {
18355
19476
  // src/lib/local-encryption.ts
18356
19477
  init_config();
18357
19478
  init_redaction();
18358
- import { createCipheriv, createDecipheriv, createHash as createHash7, randomBytes, scryptSync, timingSafeEqual } from "crypto";
19479
+ import { createCipheriv, createDecipheriv, createHash as createHash8, randomBytes, scryptSync, timingSafeEqual as timingSafeEqual2 } from "crypto";
18359
19480
  var TODOS_ENCRYPTED_VALUE_KIND = "hasna.todos.encrypted-value";
18360
19481
  var TODOS_ENCRYPTED_BRIDGE_KIND = "hasna.todos.encrypted-bridge";
18361
19482
  var TODOS_ENCRYPTION_SCHEMA_VERSION = 1;
@@ -18377,11 +19498,11 @@ class EncryptedPayloadError extends Error {
18377
19498
  super(message);
18378
19499
  }
18379
19500
  }
18380
- function now2() {
19501
+ function now3() {
18381
19502
  return new Date().toISOString();
18382
19503
  }
18383
19504
  function sha2564(value) {
18384
- return createHash7("sha256").update(value).digest("hex");
19505
+ return createHash8("sha256").update(value).digest("hex");
18385
19506
  }
18386
19507
  function normalizeProfileName(value) {
18387
19508
  const name = (value || DEFAULT_ENCRYPTION_PROFILE).trim();
@@ -18414,7 +19535,7 @@ function upsertEncryptionProfile(input) {
18414
19535
  const name = normalizeProfileName(input.name);
18415
19536
  const config = loadConfig();
18416
19537
  const existing = config.encryption_profiles?.[name];
18417
- const timestamp2 = now2();
19538
+ const timestamp2 = now3();
18418
19539
  const profile = {
18419
19540
  name,
18420
19541
  algorithm: "aes-256-gcm",
@@ -18471,7 +19592,7 @@ function encryptString(plaintext, options = {}) {
18471
19592
  return {
18472
19593
  schemaVersion: TODOS_ENCRYPTION_SCHEMA_VERSION,
18473
19594
  kind: TODOS_ENCRYPTED_VALUE_KIND,
18474
- encryptedAt: options.encryptedAt ?? now2(),
19595
+ encryptedAt: options.encryptedAt ?? now3(),
18475
19596
  profile: profile.name,
18476
19597
  key_env: profile.key_env,
18477
19598
  algorithm: "aes-256-gcm",
@@ -18506,7 +19627,7 @@ function decryptString(envelope, env = process.env) {
18506
19627
  ]).toString("utf8");
18507
19628
  const expected = Buffer.from(envelope.plaintext_sha256, "hex");
18508
19629
  const actual = Buffer.from(sha2564(plaintext), "hex");
18509
- if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
19630
+ if (expected.length !== actual.length || !timingSafeEqual2(expected, actual)) {
18510
19631
  throw new EncryptedPayloadError("decrypted payload checksum mismatch");
18511
19632
  }
18512
19633
  return plaintext;
@@ -18884,6 +20005,8 @@ export {
18884
20005
  verifyLocalAuditLedger,
18885
20006
  validateLocalBridgeBundle,
18886
20007
  validateJsonContract,
20008
+ upsertTesterIssueReports,
20009
+ upsertTesterIssueReport,
18887
20010
  upsertReviewRoutingRule,
18888
20011
  upsertReleaseGroup,
18889
20012
  upsertEncryptionProfile,
@@ -18904,8 +20027,10 @@ export {
18904
20027
  renderLocalAuditLedgerMarkdown,
18905
20028
  removeReviewRoutingRule,
18906
20029
  removeEncryptionProfile,
20030
+ readTesterIssueReportsPayload,
18907
20031
  readLocalBackupFile,
18908
20032
  pollLocalSnapshots,
20033
+ normalizeTesterIssueReport,
18909
20034
  listSdkIntegrationExamples,
18910
20035
  listRoadmaps,
18911
20036
  listReviewRoutingRules,
@@ -18929,6 +20054,7 @@ export {
18929
20054
  getLocalSnapshot,
18930
20055
  getLocalAuditLedger,
18931
20056
  getJsonContract,
20057
+ fingerprintTesterIssueReport,
18932
20058
  exportRoadmapBundle,
18933
20059
  encryptionProfileStatus,
18934
20060
  encryptValue,
@@ -18973,6 +20099,9 @@ export {
18973
20099
  TODOS_ENCRYPTED_BRIDGE_KIND,
18974
20100
  TODOS_CONTRACTS,
18975
20101
  TODOS_API_ROUTES,
20102
+ TESTERS_ISSUE_REPORT_SCHEMA_VERSION,
20103
+ TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
20104
+ TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION,
18976
20105
  LOCAL_USAGE_LEDGER_SCHEMA_VERSION,
18977
20106
  LOCAL_ROADMAP_SCHEMA_VERSION,
18978
20107
  LOCAL_REPORT_TYPES,