@hasna/todos 0.11.56 → 0.11.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/dist/cli/commands/dispatch.d.ts.map +1 -1
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/helpers.d.ts +0 -2
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/index.js +1887 -1124
- package/dist/contracts.d.ts +2 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +1408 -95
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/db/task-lifecycle.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1808 -374
- package/dist/json-contracts.d.ts.map +1 -1
- package/dist/lib/dispatch.d.ts +3 -0
- package/dist/lib/dispatch.d.ts.map +1 -1
- package/dist/lib/event-hooks.d.ts +1 -1
- package/dist/lib/event-hooks.d.ts.map +1 -1
- package/dist/lib/json-schemas.d.ts +1 -1
- package/dist/lib/json-schemas.d.ts.map +1 -1
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/lib/shared-events.d.ts +14 -0
- package/dist/lib/shared-events.d.ts.map +1 -0
- package/dist/lib/tester-issue-reports.d.ts +105 -0
- package/dist/lib/tester-issue-reports.d.ts.map +1 -0
- package/dist/lib/tmux.d.ts +19 -1
- package/dist/lib/tmux.d.ts.map +1 -1
- package/dist/mcp/index.js +1053 -256
- package/dist/mcp/tools/dispatch.d.ts.map +1 -1
- package/dist/registry.js +1400 -95
- package/dist/release-provenance.json +7 -0
- package/dist/server/index.js +1049 -252
- package/dist/storage.js +845 -146
- package/package.json +2 -2
package/dist/registry.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
|
|
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 (!
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8236
|
-
|
|
8237
|
-
|
|
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
|
-
|
|
8274
|
-
|
|
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
|
-
|
|
8316
|
-
|
|
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
|
-
|
|
8589
|
-
|
|
8590
|
-
|
|
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
|
|
9650
|
-
import { basename, dirname as dirname5, join as
|
|
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
|
|
9663
|
-
return
|
|
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
|
|
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 (!
|
|
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 =
|
|
10569
|
+
const relativePath = join6("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
|
|
9729
10570
|
const destination = artifactStorePath(relativePath);
|
|
9730
|
-
if (!
|
|
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 (!
|
|
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
|
|
9872
|
-
return
|
|
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 (!
|
|
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 =
|
|
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 || !
|
|
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
|
|
9915
|
-
const ageMs =
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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) =>
|
|
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) =>
|
|
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:
|
|
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
|
|
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
|
|
14113
|
-
if (Date.parse(task2.updated_at) <
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
14203
|
-
commands: ledger.commands.map((command) => ({ command: command.command, status: command.status, output_summary:
|
|
14204
|
-
files: ledger.files.map((file) => ({ path: file.path, status: file.status, note:
|
|
14205
|
-
artifacts: ledger.artifacts.map((artifact) => ({ path: artifact.path, artifact_type: artifact.artifact_type, description:
|
|
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 ?
|
|
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 =
|
|
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
|
|
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:
|
|
15185
|
-
updated_at:
|
|
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
|
|
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:
|
|
15261
|
-
updated_at:
|
|
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:
|
|
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
|
|
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 ??
|
|
15336
|
-
updated_at:
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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 =
|
|
17360
|
-
const taskId =
|
|
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:
|
|
17370
|
-
agent_id:
|
|
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
|
|
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 <
|
|
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
|
|
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
|
|
19685
|
+
function now3() {
|
|
18381
19686
|
return new Date().toISOString();
|
|
18382
19687
|
}
|
|
18383
19688
|
function sha2564(value) {
|
|
18384
|
-
return
|
|
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 =
|
|
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 ??
|
|
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 || !
|
|
19814
|
+
if (expected.length !== actual.length || !timingSafeEqual2(expected, actual)) {
|
|
18510
19815
|
throw new EncryptedPayloadError("decrypted payload checksum mismatch");
|
|
18511
19816
|
}
|
|
18512
19817
|
return plaintext;
|