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