@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/index.js
CHANGED
|
@@ -3050,7 +3050,7 @@ var init_redaction = __esm(() => {
|
|
|
3050
3050
|
});
|
|
3051
3051
|
|
|
3052
3052
|
// src/lib/secret-redaction.ts
|
|
3053
|
-
import { readFileSync as readFileSync3, existsSync as
|
|
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);
|
|
@@ -4874,6 +4874,67 @@ var TODOS_JSON_CONTRACTS = [
|
|
|
4874
4874
|
},
|
|
4875
4875
|
optional: {}
|
|
4876
4876
|
}),
|
|
4877
|
+
contract({
|
|
4878
|
+
id: "tester_issue_report",
|
|
4879
|
+
name: "Tester Issue Report",
|
|
4880
|
+
description: "Generic issue report payload emitted by @hasna/testers or compatible test runners for conversion into local tasks.",
|
|
4881
|
+
surfaces: ["cli", "sdk"],
|
|
4882
|
+
stability: "stable",
|
|
4883
|
+
required: {
|
|
4884
|
+
schema_version: field("string", "Must be testers.issue_report.v1."),
|
|
4885
|
+
title: field("string", "Short human-readable issue title.")
|
|
4886
|
+
},
|
|
4887
|
+
optional: {
|
|
4888
|
+
kind: field("string", "Issue kind such as assertion_failure, console_error, network_error, accessibility, or unknown."),
|
|
4889
|
+
severity: field("string", "Issue severity mapped to task priority: low, medium, high, or critical."),
|
|
4890
|
+
id: idField,
|
|
4891
|
+
fingerprint: field("string", "Stable tester-side fingerprint. If omitted, @hasna/todos derives one from issue fields."),
|
|
4892
|
+
summary: field(["string", "null"], "Short issue summary.", true),
|
|
4893
|
+
source: field("object", "Run, result, scenario, URL, commit, and branch source metadata."),
|
|
4894
|
+
target: field("object", "URL, route, selector, component, browser, and viewport metadata."),
|
|
4895
|
+
failure: field("object", "Failure message, expected/actual values, stack, reasoning, and reproduction steps."),
|
|
4896
|
+
evidence: field("object", "Logs, screenshots, and artifact references."),
|
|
4897
|
+
labels: tagsField,
|
|
4898
|
+
metadata: metadataField,
|
|
4899
|
+
occurred_at: isoDateField
|
|
4900
|
+
}
|
|
4901
|
+
}),
|
|
4902
|
+
contract({
|
|
4903
|
+
id: "tester_issue_report_result",
|
|
4904
|
+
name: "Tester Issue Report Result",
|
|
4905
|
+
description: "Local-only dry-run or applied result from mapping a tester issue report to a task.",
|
|
4906
|
+
surfaces: ["cli", "sdk"],
|
|
4907
|
+
stability: "stable",
|
|
4908
|
+
required: {
|
|
4909
|
+
schema_version: field("string", "Result schema version."),
|
|
4910
|
+
local_only: field("boolean", "Always true; issue reports are applied to local todos state."),
|
|
4911
|
+
dry_run: field("boolean", "True when no task was created or updated."),
|
|
4912
|
+
processed_at: isoDateField,
|
|
4913
|
+
action: field("string", "preview, matched, created, updated, or regressed."),
|
|
4914
|
+
fingerprint: field("string", "Computed or supplied tester issue fingerprint."),
|
|
4915
|
+
report: field("object", "Normalized tester issue report."),
|
|
4916
|
+
task: field(["object", "null"], "Created, updated, matched, or null task.", true),
|
|
4917
|
+
warnings: field("array", "Non-fatal warnings."),
|
|
4918
|
+
commands: field("array", "Follow-up CLI commands for operators and agents.")
|
|
4919
|
+
},
|
|
4920
|
+
optional: {}
|
|
4921
|
+
}),
|
|
4922
|
+
contract({
|
|
4923
|
+
id: "tester_issue_report_batch_result",
|
|
4924
|
+
name: "Tester Issue Report Batch Result",
|
|
4925
|
+
description: "Local-only dry-run or applied batch result from mapping tester issue reports to tasks.",
|
|
4926
|
+
surfaces: ["cli", "sdk"],
|
|
4927
|
+
stability: "stable",
|
|
4928
|
+
required: {
|
|
4929
|
+
schema_version: field("string", "Batch result schema version."),
|
|
4930
|
+
local_only: field("boolean", "Always true; issue reports are applied to local todos state."),
|
|
4931
|
+
dry_run: field("boolean", "True when no tasks were created or updated."),
|
|
4932
|
+
processed_at: isoDateField,
|
|
4933
|
+
results: field("array", "Single report result entries."),
|
|
4934
|
+
summary: field("object", "Counts grouped by action plus total.")
|
|
4935
|
+
},
|
|
4936
|
+
optional: {}
|
|
4937
|
+
}),
|
|
4877
4938
|
contract({
|
|
4878
4939
|
id: "verification_provider",
|
|
4879
4940
|
name: "Verification Provider",
|
|
@@ -5914,7 +5975,7 @@ var TODOS_JSON_CONTRACTS_MANIFEST = createJsonContractsManifest({
|
|
|
5914
5975
|
});
|
|
5915
5976
|
// src/lib/onboarding-fixtures.ts
|
|
5916
5977
|
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
5917
|
-
import { join as
|
|
5978
|
+
import { join as join7 } from "path";
|
|
5918
5979
|
|
|
5919
5980
|
// src/lib/local-bridge.ts
|
|
5920
5981
|
init_database();
|
|
@@ -6574,6 +6635,7 @@ function explainRunnerSandbox(input = {}) {
|
|
|
6574
6635
|
// src/lib/event-hooks.ts
|
|
6575
6636
|
init_config();
|
|
6576
6637
|
var LOCAL_EVENT_TYPES = [
|
|
6638
|
+
"task.created",
|
|
6577
6639
|
"task.assigned",
|
|
6578
6640
|
"task.blocked",
|
|
6579
6641
|
"task.started",
|
|
@@ -6816,6 +6878,752 @@ async function testLocalEventHook(name, input) {
|
|
|
6816
6878
|
return emitLocalEventHooks({ ...input, hooks: [hook] });
|
|
6817
6879
|
}
|
|
6818
6880
|
|
|
6881
|
+
// node_modules/.bun/@hasna+events@0.1.9/node_modules/@hasna/events/dist/index.js
|
|
6882
|
+
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
6883
|
+
import { existsSync as existsSync6 } from "fs";
|
|
6884
|
+
import { homedir } from "os";
|
|
6885
|
+
import { join as join5 } from "path";
|
|
6886
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
6887
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
6888
|
+
import { spawn } from "child_process";
|
|
6889
|
+
import { randomUUID as randomUUID22 } from "crypto";
|
|
6890
|
+
function getPathValue(input, path) {
|
|
6891
|
+
return path.split(".").reduce((value, part) => {
|
|
6892
|
+
if (value && typeof value === "object" && part in value) {
|
|
6893
|
+
return value[part];
|
|
6894
|
+
}
|
|
6895
|
+
return;
|
|
6896
|
+
}, input);
|
|
6897
|
+
}
|
|
6898
|
+
function wildcardToRegExp(pattern, options = {}) {
|
|
6899
|
+
let body = "";
|
|
6900
|
+
for (let index = 0;index < pattern.length; index += 1) {
|
|
6901
|
+
const char = pattern[index];
|
|
6902
|
+
if (char === "*") {
|
|
6903
|
+
if (pattern[index + 1] === "*") {
|
|
6904
|
+
body += ".*";
|
|
6905
|
+
index += 1;
|
|
6906
|
+
} else {
|
|
6907
|
+
body += options.segmentSafe ? "[^/]*" : ".*";
|
|
6908
|
+
}
|
|
6909
|
+
} else {
|
|
6910
|
+
body += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
6911
|
+
}
|
|
6912
|
+
}
|
|
6913
|
+
return new RegExp(`^${body}$`);
|
|
6914
|
+
}
|
|
6915
|
+
function matchString(value, matcher, options = {}) {
|
|
6916
|
+
if (matcher === undefined)
|
|
6917
|
+
return true;
|
|
6918
|
+
if (value === undefined)
|
|
6919
|
+
return false;
|
|
6920
|
+
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
6921
|
+
return matchers.some((item) => wildcardToRegExp(item, options).test(value));
|
|
6922
|
+
}
|
|
6923
|
+
function matchRecord(input, matcher) {
|
|
6924
|
+
if (!matcher)
|
|
6925
|
+
return true;
|
|
6926
|
+
return Object.entries(matcher).every(([path, expected]) => {
|
|
6927
|
+
const actual = getPathValue(input, path);
|
|
6928
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
6929
|
+
return matchString(actual === undefined ? undefined : String(actual), expected, {
|
|
6930
|
+
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
6931
|
+
});
|
|
6932
|
+
}
|
|
6933
|
+
return actual === expected;
|
|
6934
|
+
});
|
|
6935
|
+
}
|
|
6936
|
+
function eventMatchesFilter(event, filter) {
|
|
6937
|
+
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);
|
|
6938
|
+
}
|
|
6939
|
+
function channelMatchesEvent(channel, event) {
|
|
6940
|
+
if (!channel.enabled)
|
|
6941
|
+
return false;
|
|
6942
|
+
if (!channel.filters || channel.filters.length === 0)
|
|
6943
|
+
return true;
|
|
6944
|
+
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
6945
|
+
}
|
|
6946
|
+
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
6947
|
+
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
6948
|
+
function getEventsDataDir(override) {
|
|
6949
|
+
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join5(homedir(), ".hasna", "events");
|
|
6950
|
+
}
|
|
6951
|
+
class JsonEventsStore {
|
|
6952
|
+
dataDir;
|
|
6953
|
+
channelsPath;
|
|
6954
|
+
eventsPath;
|
|
6955
|
+
deliveriesPath;
|
|
6956
|
+
constructor(dataDir = getEventsDataDir()) {
|
|
6957
|
+
this.dataDir = dataDir;
|
|
6958
|
+
this.channelsPath = join5(dataDir, "channels.json");
|
|
6959
|
+
this.eventsPath = join5(dataDir, "events.json");
|
|
6960
|
+
this.deliveriesPath = join5(dataDir, "deliveries.json");
|
|
6961
|
+
}
|
|
6962
|
+
async init() {
|
|
6963
|
+
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
6964
|
+
await chmod(this.dataDir, 448).catch(() => {
|
|
6965
|
+
return;
|
|
6966
|
+
});
|
|
6967
|
+
await this.ensureArrayFile(this.channelsPath);
|
|
6968
|
+
await this.ensureArrayFile(this.eventsPath);
|
|
6969
|
+
await this.ensureArrayFile(this.deliveriesPath);
|
|
6970
|
+
}
|
|
6971
|
+
async addChannel(channel) {
|
|
6972
|
+
await this.init();
|
|
6973
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
6974
|
+
const index = channels.findIndex((item) => item.id === channel.id);
|
|
6975
|
+
if (index >= 0) {
|
|
6976
|
+
channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
|
|
6977
|
+
} else {
|
|
6978
|
+
channels.push(channel);
|
|
6979
|
+
}
|
|
6980
|
+
await this.writeJson(this.channelsPath, channels);
|
|
6981
|
+
return index >= 0 ? channels[index] : channel;
|
|
6982
|
+
}
|
|
6983
|
+
async listChannels() {
|
|
6984
|
+
await this.init();
|
|
6985
|
+
return this.readJson(this.channelsPath, []);
|
|
6986
|
+
}
|
|
6987
|
+
async getChannel(id) {
|
|
6988
|
+
const channels = await this.listChannels();
|
|
6989
|
+
return channels.find((channel) => channel.id === id);
|
|
6990
|
+
}
|
|
6991
|
+
async removeChannel(id) {
|
|
6992
|
+
await this.init();
|
|
6993
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
6994
|
+
const next = channels.filter((channel) => channel.id !== id);
|
|
6995
|
+
await this.writeJson(this.channelsPath, next);
|
|
6996
|
+
return next.length !== channels.length;
|
|
6997
|
+
}
|
|
6998
|
+
async appendEvent(event) {
|
|
6999
|
+
await this.init();
|
|
7000
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
7001
|
+
events.push(event);
|
|
7002
|
+
await this.writeJson(this.eventsPath, events);
|
|
7003
|
+
return event;
|
|
7004
|
+
}
|
|
7005
|
+
async listEvents() {
|
|
7006
|
+
await this.init();
|
|
7007
|
+
return this.readJson(this.eventsPath, []);
|
|
7008
|
+
}
|
|
7009
|
+
async findEventByIdentity(identity) {
|
|
7010
|
+
const events = await this.listEvents();
|
|
7011
|
+
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
7012
|
+
}
|
|
7013
|
+
async appendDelivery(result) {
|
|
7014
|
+
await this.init();
|
|
7015
|
+
const deliveries = await this.readJson(this.deliveriesPath, []);
|
|
7016
|
+
deliveries.push(result);
|
|
7017
|
+
await this.writeJson(this.deliveriesPath, deliveries);
|
|
7018
|
+
return result;
|
|
7019
|
+
}
|
|
7020
|
+
async listDeliveries() {
|
|
7021
|
+
await this.init();
|
|
7022
|
+
return this.readJson(this.deliveriesPath, []);
|
|
7023
|
+
}
|
|
7024
|
+
async exportData() {
|
|
7025
|
+
return {
|
|
7026
|
+
channels: await this.listChannels(),
|
|
7027
|
+
events: await this.listEvents(),
|
|
7028
|
+
deliveries: await this.listDeliveries()
|
|
7029
|
+
};
|
|
7030
|
+
}
|
|
7031
|
+
async ensureArrayFile(path) {
|
|
7032
|
+
if (!existsSync6(path)) {
|
|
7033
|
+
await writeFile(path, `[]
|
|
7034
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
7035
|
+
}
|
|
7036
|
+
await chmod(path, 384).catch(() => {
|
|
7037
|
+
return;
|
|
7038
|
+
});
|
|
7039
|
+
}
|
|
7040
|
+
async readJson(path, fallback) {
|
|
7041
|
+
try {
|
|
7042
|
+
const raw = await readFile(path, "utf-8");
|
|
7043
|
+
if (!raw.trim())
|
|
7044
|
+
return fallback;
|
|
7045
|
+
return JSON.parse(raw);
|
|
7046
|
+
} catch (error) {
|
|
7047
|
+
if (error.code === "ENOENT")
|
|
7048
|
+
return fallback;
|
|
7049
|
+
throw error;
|
|
7050
|
+
}
|
|
7051
|
+
}
|
|
7052
|
+
async writeJson(path, value) {
|
|
7053
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
7054
|
+
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
|
|
7055
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
7056
|
+
await rename(tempPath, path);
|
|
7057
|
+
await chmod(path, 384).catch(() => {
|
|
7058
|
+
return;
|
|
7059
|
+
});
|
|
7060
|
+
}
|
|
7061
|
+
}
|
|
7062
|
+
var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
7063
|
+
function buildSignatureBase(timestamp, body) {
|
|
7064
|
+
return `${timestamp}.${body}`;
|
|
7065
|
+
}
|
|
7066
|
+
function signPayload(secret, timestamp, body) {
|
|
7067
|
+
const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
|
|
7068
|
+
return `sha256=${digest}`;
|
|
7069
|
+
}
|
|
7070
|
+
function now2() {
|
|
7071
|
+
return new Date().toISOString();
|
|
7072
|
+
}
|
|
7073
|
+
function truncate(value, max = 4096) {
|
|
7074
|
+
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
7075
|
+
}
|
|
7076
|
+
function buildWebhookRequest(event, channel) {
|
|
7077
|
+
if (!channel.webhook)
|
|
7078
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
7079
|
+
const body = JSON.stringify(event);
|
|
7080
|
+
const timestamp = event.time;
|
|
7081
|
+
const headers = {
|
|
7082
|
+
"Content-Type": "application/json",
|
|
7083
|
+
"User-Agent": "@hasna/events",
|
|
7084
|
+
"X-Hasna-Event-Id": event.id,
|
|
7085
|
+
"X-Hasna-Event-Type": event.type,
|
|
7086
|
+
"X-Hasna-Timestamp": timestamp,
|
|
7087
|
+
...channel.webhook.headers
|
|
7088
|
+
};
|
|
7089
|
+
if (channel.webhook.secret) {
|
|
7090
|
+
headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
|
|
7091
|
+
}
|
|
7092
|
+
return { body, headers };
|
|
7093
|
+
}
|
|
7094
|
+
async function dispatchWebhook(event, channel, options = {}) {
|
|
7095
|
+
if (!channel.webhook)
|
|
7096
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
7097
|
+
const startedAt = now2();
|
|
7098
|
+
const { body, headers } = buildWebhookRequest(event, channel);
|
|
7099
|
+
const controller = new AbortController;
|
|
7100
|
+
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
7101
|
+
try {
|
|
7102
|
+
const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
|
|
7103
|
+
method: "POST",
|
|
7104
|
+
headers,
|
|
7105
|
+
body,
|
|
7106
|
+
signal: controller.signal
|
|
7107
|
+
});
|
|
7108
|
+
const responseBody = truncate(await response.text());
|
|
7109
|
+
return {
|
|
7110
|
+
attempt: 1,
|
|
7111
|
+
status: response.ok ? "success" : "failed",
|
|
7112
|
+
startedAt,
|
|
7113
|
+
completedAt: now2(),
|
|
7114
|
+
responseStatus: response.status,
|
|
7115
|
+
responseBody,
|
|
7116
|
+
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
7117
|
+
};
|
|
7118
|
+
} catch (error) {
|
|
7119
|
+
return {
|
|
7120
|
+
attempt: 1,
|
|
7121
|
+
status: "failed",
|
|
7122
|
+
startedAt,
|
|
7123
|
+
completedAt: now2(),
|
|
7124
|
+
error: error instanceof Error ? error.message : String(error)
|
|
7125
|
+
};
|
|
7126
|
+
} finally {
|
|
7127
|
+
clearTimeout(timeout);
|
|
7128
|
+
}
|
|
7129
|
+
}
|
|
7130
|
+
async function dispatchCommand(event, channel) {
|
|
7131
|
+
if (!channel.command)
|
|
7132
|
+
throw new Error(`Channel ${channel.id} has no command config`);
|
|
7133
|
+
const startedAt = now2();
|
|
7134
|
+
const eventJson = JSON.stringify(event);
|
|
7135
|
+
const env = {
|
|
7136
|
+
...process.env,
|
|
7137
|
+
...channel.command.env,
|
|
7138
|
+
HASNA_CHANNEL_ID: channel.id,
|
|
7139
|
+
HASNA_EVENT_ID: event.id,
|
|
7140
|
+
HASNA_EVENT_TYPE: event.type,
|
|
7141
|
+
HASNA_EVENT_SOURCE: event.source,
|
|
7142
|
+
HASNA_EVENT_SUBJECT: event.subject ?? "",
|
|
7143
|
+
HASNA_EVENT_SEVERITY: event.severity,
|
|
7144
|
+
HASNA_EVENT_TIME: event.time,
|
|
7145
|
+
HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
|
|
7146
|
+
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
7147
|
+
HASNA_EVENT_JSON: eventJson
|
|
7148
|
+
};
|
|
7149
|
+
return new Promise((resolve6) => {
|
|
7150
|
+
const child = spawn(channel.command.command, channel.command.args ?? [], {
|
|
7151
|
+
cwd: channel.command.cwd,
|
|
7152
|
+
env,
|
|
7153
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
7154
|
+
});
|
|
7155
|
+
let stdout = "";
|
|
7156
|
+
let stderr = "";
|
|
7157
|
+
const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
|
|
7158
|
+
child.stdin.end(eventJson);
|
|
7159
|
+
child.stdout.on("data", (chunk) => {
|
|
7160
|
+
stdout += chunk.toString();
|
|
7161
|
+
});
|
|
7162
|
+
child.stderr.on("data", (chunk) => {
|
|
7163
|
+
stderr += chunk.toString();
|
|
7164
|
+
});
|
|
7165
|
+
child.on("error", (error) => {
|
|
7166
|
+
clearTimeout(timeout);
|
|
7167
|
+
resolve6({
|
|
7168
|
+
attempt: 1,
|
|
7169
|
+
status: "failed",
|
|
7170
|
+
startedAt,
|
|
7171
|
+
completedAt: now2(),
|
|
7172
|
+
stdout: truncate(stdout),
|
|
7173
|
+
stderr: truncate(stderr),
|
|
7174
|
+
error: error.message
|
|
7175
|
+
});
|
|
7176
|
+
});
|
|
7177
|
+
child.on("close", (code, signal) => {
|
|
7178
|
+
clearTimeout(timeout);
|
|
7179
|
+
const success = code === 0;
|
|
7180
|
+
resolve6({
|
|
7181
|
+
attempt: 1,
|
|
7182
|
+
status: success ? "success" : "failed",
|
|
7183
|
+
startedAt,
|
|
7184
|
+
completedAt: now2(),
|
|
7185
|
+
stdout: truncate(stdout),
|
|
7186
|
+
stderr: truncate(stderr),
|
|
7187
|
+
error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
7188
|
+
});
|
|
7189
|
+
});
|
|
7190
|
+
});
|
|
7191
|
+
}
|
|
7192
|
+
async function dispatchChannel(event, channel, options = {}) {
|
|
7193
|
+
if (channel.transport === "webhook")
|
|
7194
|
+
return dispatchWebhook(event, channel, options);
|
|
7195
|
+
if (channel.transport === "command")
|
|
7196
|
+
return dispatchCommand(event, channel);
|
|
7197
|
+
return {
|
|
7198
|
+
attempt: 1,
|
|
7199
|
+
status: "skipped",
|
|
7200
|
+
startedAt: now2(),
|
|
7201
|
+
completedAt: now2(),
|
|
7202
|
+
error: `Unsupported transport: ${channel.transport}`
|
|
7203
|
+
};
|
|
7204
|
+
}
|
|
7205
|
+
function createDeliveryResult(event, channel, attempts) {
|
|
7206
|
+
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
7207
|
+
return {
|
|
7208
|
+
id: randomUUID2(),
|
|
7209
|
+
eventId: event.id,
|
|
7210
|
+
channelId: channel.id,
|
|
7211
|
+
transport: channel.transport,
|
|
7212
|
+
status,
|
|
7213
|
+
attempts,
|
|
7214
|
+
createdAt: attempts[0]?.startedAt ?? now2(),
|
|
7215
|
+
completedAt: attempts.at(-1)?.completedAt ?? now2()
|
|
7216
|
+
};
|
|
7217
|
+
}
|
|
7218
|
+
function createEvent(input) {
|
|
7219
|
+
return {
|
|
7220
|
+
id: input.id ?? randomUUID22(),
|
|
7221
|
+
source: input.source,
|
|
7222
|
+
type: input.type,
|
|
7223
|
+
time: normalizeTime(input.time),
|
|
7224
|
+
subject: input.subject,
|
|
7225
|
+
severity: input.severity ?? "info",
|
|
7226
|
+
data: input.data ?? {},
|
|
7227
|
+
message: input.message,
|
|
7228
|
+
dedupeKey: input.dedupeKey,
|
|
7229
|
+
schemaVersion: input.schemaVersion ?? "1.0",
|
|
7230
|
+
metadata: input.metadata ?? {}
|
|
7231
|
+
};
|
|
7232
|
+
}
|
|
7233
|
+
|
|
7234
|
+
class EventsClient {
|
|
7235
|
+
store;
|
|
7236
|
+
redactors;
|
|
7237
|
+
transportOptions;
|
|
7238
|
+
constructor(options = {}) {
|
|
7239
|
+
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
7240
|
+
this.redactors = options.redactors ?? [];
|
|
7241
|
+
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
7242
|
+
}
|
|
7243
|
+
async addChannel(input) {
|
|
7244
|
+
const timestamp = new Date().toISOString();
|
|
7245
|
+
return this.store.addChannel({
|
|
7246
|
+
...input,
|
|
7247
|
+
createdAt: input.createdAt ?? timestamp,
|
|
7248
|
+
updatedAt: input.updatedAt ?? timestamp
|
|
7249
|
+
});
|
|
7250
|
+
}
|
|
7251
|
+
async listChannels() {
|
|
7252
|
+
return this.store.listChannels();
|
|
7253
|
+
}
|
|
7254
|
+
async removeChannel(id) {
|
|
7255
|
+
return this.store.removeChannel(id);
|
|
7256
|
+
}
|
|
7257
|
+
async emit(input, options = {}) {
|
|
7258
|
+
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
7259
|
+
if (options.dedupe !== false) {
|
|
7260
|
+
const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
|
|
7261
|
+
if (existing) {
|
|
7262
|
+
return { event: existing, deliveries: [], deduped: true };
|
|
7263
|
+
}
|
|
7264
|
+
}
|
|
7265
|
+
await this.store.appendEvent(event);
|
|
7266
|
+
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
7267
|
+
return { event, deliveries, deduped: false };
|
|
7268
|
+
}
|
|
7269
|
+
async listEvents() {
|
|
7270
|
+
return this.store.listEvents();
|
|
7271
|
+
}
|
|
7272
|
+
async listDeliveries() {
|
|
7273
|
+
return this.store.listDeliveries();
|
|
7274
|
+
}
|
|
7275
|
+
async deliver(event) {
|
|
7276
|
+
const channels = await this.store.listChannels();
|
|
7277
|
+
const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
|
|
7278
|
+
const deliveries = [];
|
|
7279
|
+
for (const channel of selected) {
|
|
7280
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
7281
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
7282
|
+
await this.store.appendDelivery(result);
|
|
7283
|
+
deliveries.push(result);
|
|
7284
|
+
}
|
|
7285
|
+
return deliveries;
|
|
7286
|
+
}
|
|
7287
|
+
async matchChannel(id, input = {}) {
|
|
7288
|
+
const channel = await this.store.getChannel(id);
|
|
7289
|
+
if (!channel)
|
|
7290
|
+
throw new Error(`Channel not found: ${id}`);
|
|
7291
|
+
const event = createEvent({
|
|
7292
|
+
source: input.source ?? "hasna.events",
|
|
7293
|
+
type: input.type ?? "events.test",
|
|
7294
|
+
subject: input.subject ?? id,
|
|
7295
|
+
severity: input.severity ?? "info",
|
|
7296
|
+
data: input.data ?? { test: true },
|
|
7297
|
+
message: input.message ?? "Hasna events test delivery",
|
|
7298
|
+
dedupeKey: input.dedupeKey,
|
|
7299
|
+
schemaVersion: input.schemaVersion,
|
|
7300
|
+
metadata: input.metadata,
|
|
7301
|
+
time: input.time,
|
|
7302
|
+
id: input.id
|
|
7303
|
+
});
|
|
7304
|
+
const matched = channelMatchesEvent(channel, event);
|
|
7305
|
+
return {
|
|
7306
|
+
channelId: channel.id,
|
|
7307
|
+
matched,
|
|
7308
|
+
event,
|
|
7309
|
+
filters: channel.filters,
|
|
7310
|
+
reason: matched ? undefined : channel.enabled ? "event did not match channel filters" : "channel is disabled"
|
|
7311
|
+
};
|
|
7312
|
+
}
|
|
7313
|
+
async testChannel(id, input = {}, options = {}) {
|
|
7314
|
+
const channel = await this.store.getChannel(id);
|
|
7315
|
+
if (!channel)
|
|
7316
|
+
throw new Error(`Channel not found: ${id}`);
|
|
7317
|
+
const match = await this.matchChannel(id, input);
|
|
7318
|
+
const event = match.event;
|
|
7319
|
+
if (options.honorFilters && !match.matched) {
|
|
7320
|
+
const timestamp = new Date().toISOString();
|
|
7321
|
+
const result2 = createDeliveryResult(event, channel, [{
|
|
7322
|
+
attempt: 1,
|
|
7323
|
+
status: "skipped",
|
|
7324
|
+
startedAt: timestamp,
|
|
7325
|
+
completedAt: timestamp,
|
|
7326
|
+
error: match.reason
|
|
7327
|
+
}]);
|
|
7328
|
+
result2.metadata = { reason: "filter_mismatch" };
|
|
7329
|
+
await this.store.appendDelivery(result2);
|
|
7330
|
+
return result2;
|
|
7331
|
+
}
|
|
7332
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
7333
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
7334
|
+
await this.store.appendDelivery(result);
|
|
7335
|
+
return result;
|
|
7336
|
+
}
|
|
7337
|
+
async replay(options = {}) {
|
|
7338
|
+
const events = (await this.store.listEvents()).filter((event) => {
|
|
7339
|
+
if (options.eventId && event.id !== options.eventId)
|
|
7340
|
+
return false;
|
|
7341
|
+
if (options.source && event.source !== options.source)
|
|
7342
|
+
return false;
|
|
7343
|
+
if (options.type && event.type !== options.type)
|
|
7344
|
+
return false;
|
|
7345
|
+
return true;
|
|
7346
|
+
});
|
|
7347
|
+
if (options.dryRun)
|
|
7348
|
+
return { events, deliveries: [] };
|
|
7349
|
+
const deliveries = [];
|
|
7350
|
+
for (const event of events) {
|
|
7351
|
+
deliveries.push(...await this.deliver(event));
|
|
7352
|
+
}
|
|
7353
|
+
return { events, deliveries };
|
|
7354
|
+
}
|
|
7355
|
+
async applyRedaction(event, channel) {
|
|
7356
|
+
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
7357
|
+
for (const redactor of this.redactors) {
|
|
7358
|
+
next = await redactor(next, channel);
|
|
7359
|
+
}
|
|
7360
|
+
return next;
|
|
7361
|
+
}
|
|
7362
|
+
async deliverWithRetry(event, channel) {
|
|
7363
|
+
const policy = normalizeRetryPolicy(channel.retry);
|
|
7364
|
+
const attempts = [];
|
|
7365
|
+
for (let index = 0;index < policy.maxAttempts; index += 1) {
|
|
7366
|
+
const attempt = await dispatchChannel(event, channel, this.transportOptions);
|
|
7367
|
+
attempt.attempt = index + 1;
|
|
7368
|
+
if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
|
|
7369
|
+
attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
|
|
7370
|
+
}
|
|
7371
|
+
attempts.push(attempt);
|
|
7372
|
+
if (attempt.status !== "failed")
|
|
7373
|
+
break;
|
|
7374
|
+
if (attempt.nextBackoffMs)
|
|
7375
|
+
await Bun.sleep(attempt.nextBackoffMs);
|
|
7376
|
+
}
|
|
7377
|
+
return createDeliveryResult(event, channel, attempts);
|
|
7378
|
+
}
|
|
7379
|
+
}
|
|
7380
|
+
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
7381
|
+
if (paths.length === 0)
|
|
7382
|
+
return event;
|
|
7383
|
+
const copy = structuredClone(event);
|
|
7384
|
+
for (const path of paths) {
|
|
7385
|
+
setPath(copy, path, replacement);
|
|
7386
|
+
}
|
|
7387
|
+
return copy;
|
|
7388
|
+
}
|
|
7389
|
+
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
7390
|
+
return redactValue2(event, replacement);
|
|
7391
|
+
}
|
|
7392
|
+
function shouldRedactKey(key) {
|
|
7393
|
+
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
7394
|
+
}
|
|
7395
|
+
function redactValue2(value, replacement) {
|
|
7396
|
+
if (Array.isArray(value))
|
|
7397
|
+
return value.map((item) => redactValue2(item, replacement));
|
|
7398
|
+
if (!value || typeof value !== "object")
|
|
7399
|
+
return value;
|
|
7400
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
7401
|
+
key,
|
|
7402
|
+
shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
|
|
7403
|
+
]));
|
|
7404
|
+
}
|
|
7405
|
+
function setPath(input, path, replacement) {
|
|
7406
|
+
const parts = path.split(".");
|
|
7407
|
+
let cursor = input;
|
|
7408
|
+
for (const part of parts.slice(0, -1)) {
|
|
7409
|
+
const next = cursor[part];
|
|
7410
|
+
if (!next || typeof next !== "object")
|
|
7411
|
+
return;
|
|
7412
|
+
cursor = next;
|
|
7413
|
+
}
|
|
7414
|
+
const last = parts.at(-1);
|
|
7415
|
+
if (last && last in cursor)
|
|
7416
|
+
cursor[last] = replacement;
|
|
7417
|
+
}
|
|
7418
|
+
function normalizeTime(value) {
|
|
7419
|
+
if (!value)
|
|
7420
|
+
return new Date().toISOString();
|
|
7421
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
7422
|
+
}
|
|
7423
|
+
function normalizeRetryPolicy(policy) {
|
|
7424
|
+
return {
|
|
7425
|
+
maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
|
|
7426
|
+
backoffMs: Math.max(0, policy?.backoffMs ?? 250),
|
|
7427
|
+
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
7428
|
+
};
|
|
7429
|
+
}
|
|
7430
|
+
|
|
7431
|
+
// src/lib/shared-events.ts
|
|
7432
|
+
init_database();
|
|
7433
|
+
|
|
7434
|
+
// src/db/task-lists.ts
|
|
7435
|
+
init_types();
|
|
7436
|
+
init_database();
|
|
7437
|
+
function rowToTaskList(row) {
|
|
7438
|
+
return {
|
|
7439
|
+
...row,
|
|
7440
|
+
metadata: JSON.parse(row.metadata || "{}")
|
|
7441
|
+
};
|
|
7442
|
+
}
|
|
7443
|
+
function createTaskList(input, db) {
|
|
7444
|
+
const d = db || getDatabase();
|
|
7445
|
+
const id = uuid();
|
|
7446
|
+
const timestamp = now();
|
|
7447
|
+
const slug = input.slug || slugify(input.name);
|
|
7448
|
+
if (!input.project_id) {
|
|
7449
|
+
const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
|
|
7450
|
+
if (existing) {
|
|
7451
|
+
throw new Error(`Standalone task list with slug "${slug}" already exists`);
|
|
7452
|
+
}
|
|
7453
|
+
}
|
|
7454
|
+
d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
|
|
7455
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
|
|
7456
|
+
return getTaskList(id, d);
|
|
7457
|
+
}
|
|
7458
|
+
function getTaskList(id, db) {
|
|
7459
|
+
const d = db || getDatabase();
|
|
7460
|
+
const row = d.query("SELECT * FROM task_lists WHERE id = ?").get(id);
|
|
7461
|
+
return row ? rowToTaskList(row) : null;
|
|
7462
|
+
}
|
|
7463
|
+
function getTaskListBySlug(slug, projectId, db) {
|
|
7464
|
+
const d = db || getDatabase();
|
|
7465
|
+
let row;
|
|
7466
|
+
if (projectId) {
|
|
7467
|
+
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id = ?").get(slug, projectId);
|
|
7468
|
+
} else {
|
|
7469
|
+
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id IS NULL").get(slug);
|
|
7470
|
+
}
|
|
7471
|
+
return row ? rowToTaskList(row) : null;
|
|
7472
|
+
}
|
|
7473
|
+
function listTaskLists(projectId, db) {
|
|
7474
|
+
const d = db || getDatabase();
|
|
7475
|
+
if (projectId) {
|
|
7476
|
+
return d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(projectId).map(rowToTaskList);
|
|
7477
|
+
}
|
|
7478
|
+
return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
|
|
7479
|
+
}
|
|
7480
|
+
function updateTaskList(id, input, db) {
|
|
7481
|
+
const d = db || getDatabase();
|
|
7482
|
+
const existing = getTaskList(id, d);
|
|
7483
|
+
if (!existing)
|
|
7484
|
+
throw new TaskListNotFoundError(id);
|
|
7485
|
+
const sets = ["updated_at = ?"];
|
|
7486
|
+
const params = [now()];
|
|
7487
|
+
if (input.name !== undefined) {
|
|
7488
|
+
sets.push("name = ?");
|
|
7489
|
+
params.push(input.name);
|
|
7490
|
+
}
|
|
7491
|
+
if (input.description !== undefined) {
|
|
7492
|
+
sets.push("description = ?");
|
|
7493
|
+
params.push(input.description);
|
|
7494
|
+
}
|
|
7495
|
+
if (input.metadata !== undefined) {
|
|
7496
|
+
sets.push("metadata = ?");
|
|
7497
|
+
params.push(JSON.stringify(input.metadata));
|
|
7498
|
+
}
|
|
7499
|
+
params.push(id);
|
|
7500
|
+
d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
7501
|
+
return getTaskList(id, d);
|
|
7502
|
+
}
|
|
7503
|
+
function deleteTaskList(id, db) {
|
|
7504
|
+
const d = db || getDatabase();
|
|
7505
|
+
return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
7506
|
+
}
|
|
7507
|
+
function ensureTaskList(name, slug, projectId, db) {
|
|
7508
|
+
const d = db || getDatabase();
|
|
7509
|
+
const existing = getTaskListBySlug(slug, projectId, d);
|
|
7510
|
+
if (existing)
|
|
7511
|
+
return existing;
|
|
7512
|
+
return createTaskList({ name, slug, project_id: projectId }, d);
|
|
7513
|
+
}
|
|
7514
|
+
|
|
7515
|
+
// src/lib/shared-events.ts
|
|
7516
|
+
var SOURCE = "todos";
|
|
7517
|
+
function taskEventData(task, extra = {}) {
|
|
7518
|
+
return {
|
|
7519
|
+
id: task.id,
|
|
7520
|
+
task_id: task.id,
|
|
7521
|
+
short_id: task.short_id,
|
|
7522
|
+
title: task.title,
|
|
7523
|
+
description: task.description,
|
|
7524
|
+
status: task.status,
|
|
7525
|
+
priority: task.priority,
|
|
7526
|
+
project_id: task.project_id,
|
|
7527
|
+
parent_id: task.parent_id,
|
|
7528
|
+
plan_id: task.plan_id,
|
|
7529
|
+
task_list_id: task.task_list_id,
|
|
7530
|
+
agent_id: task.agent_id,
|
|
7531
|
+
assigned_to: task.assigned_to,
|
|
7532
|
+
session_id: task.session_id,
|
|
7533
|
+
working_dir: task.working_dir,
|
|
7534
|
+
tags: task.tags,
|
|
7535
|
+
metadata: task.metadata,
|
|
7536
|
+
version: task.version,
|
|
7537
|
+
created_at: task.created_at,
|
|
7538
|
+
updated_at: task.updated_at,
|
|
7539
|
+
started_at: task.started_at,
|
|
7540
|
+
completed_at: task.completed_at,
|
|
7541
|
+
due_at: task.due_at,
|
|
7542
|
+
...extra
|
|
7543
|
+
};
|
|
7544
|
+
}
|
|
7545
|
+
function taskEventMetadata(task) {
|
|
7546
|
+
const metadata = {
|
|
7547
|
+
package: "@hasna/todos",
|
|
7548
|
+
todos_event_schema_version: 1,
|
|
7549
|
+
task_id: task.id,
|
|
7550
|
+
task_short_id: task.short_id,
|
|
7551
|
+
project_id: task.project_id,
|
|
7552
|
+
task_list_id: task.task_list_id,
|
|
7553
|
+
working_dir: task.working_dir
|
|
7554
|
+
};
|
|
7555
|
+
try {
|
|
7556
|
+
const project = task.project_id ? getProject(task.project_id) : null;
|
|
7557
|
+
const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
|
|
7558
|
+
if (project) {
|
|
7559
|
+
metadata.project_id = project.id;
|
|
7560
|
+
metadata.project_name = project.name;
|
|
7561
|
+
metadata.project_path = projectPath;
|
|
7562
|
+
metadata.project_canonical_path = project.path;
|
|
7563
|
+
metadata.project_default_task_list_slug = project.task_list_id;
|
|
7564
|
+
metadata.root_project_id = inferRootProjectId(project);
|
|
7565
|
+
} else if (projectPath) {
|
|
7566
|
+
metadata.project_path = projectPath;
|
|
7567
|
+
metadata.project_canonical_path = projectPath;
|
|
7568
|
+
}
|
|
7569
|
+
if (projectPath) {
|
|
7570
|
+
metadata.project_kind = classifyProjectKind(projectPath);
|
|
7571
|
+
metadata.project_is_worktree = isWorktreePath(projectPath);
|
|
7572
|
+
if (typeof task.metadata.route_enabled === "boolean") {
|
|
7573
|
+
metadata.route_enabled = task.metadata.route_enabled;
|
|
7574
|
+
}
|
|
7575
|
+
metadata.working_dir = task.working_dir ?? projectPath;
|
|
7576
|
+
}
|
|
7577
|
+
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;
|
|
7578
|
+
if (taskList) {
|
|
7579
|
+
metadata.task_list_id = taskList.id;
|
|
7580
|
+
metadata.task_list_slug = taskList.slug;
|
|
7581
|
+
metadata.task_list_name = taskList.name;
|
|
7582
|
+
metadata.task_list_project_id = taskList.project_id;
|
|
7583
|
+
metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
|
|
7584
|
+
}
|
|
7585
|
+
} catch {}
|
|
7586
|
+
return metadata;
|
|
7587
|
+
}
|
|
7588
|
+
function classifyProjectKind(path) {
|
|
7589
|
+
return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
|
|
7590
|
+
}
|
|
7591
|
+
function isWorktreePath(path) {
|
|
7592
|
+
return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
|
|
7593
|
+
}
|
|
7594
|
+
function inferRootProjectId(project) {
|
|
7595
|
+
return isWorktreePath(project.path) ? null : project.id;
|
|
7596
|
+
}
|
|
7597
|
+
function readMachineLocalPath(project) {
|
|
7598
|
+
const machineId = process.env["TODOS_MACHINE_ID"];
|
|
7599
|
+
if (!machineId)
|
|
7600
|
+
return null;
|
|
7601
|
+
try {
|
|
7602
|
+
const row = getDatabase().query("SELECT path FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(project.id, machineId);
|
|
7603
|
+
return row?.path ?? null;
|
|
7604
|
+
} catch {
|
|
7605
|
+
return null;
|
|
7606
|
+
}
|
|
7607
|
+
}
|
|
7608
|
+
async function emitSharedTaskEvent(input) {
|
|
7609
|
+
const data = taskEventData(input.task, input.data);
|
|
7610
|
+
await new EventsClient().emit({
|
|
7611
|
+
source: SOURCE,
|
|
7612
|
+
type: input.type,
|
|
7613
|
+
subject: input.task.id,
|
|
7614
|
+
severity: input.severity ?? "info",
|
|
7615
|
+
message: input.message ?? `${input.type}: ${input.task.title}`,
|
|
7616
|
+
data,
|
|
7617
|
+
dedupeKey: input.dedupeKey ?? `${input.type}:${input.task.id}:${input.task.version}`,
|
|
7618
|
+
metadata: taskEventMetadata(input.task)
|
|
7619
|
+
}, { deliver: true, dedupe: true });
|
|
7620
|
+
}
|
|
7621
|
+
function emitSharedTaskEventQuiet(input) {
|
|
7622
|
+
emitSharedTaskEvent(input).catch(() => {
|
|
7623
|
+
return;
|
|
7624
|
+
});
|
|
7625
|
+
}
|
|
7626
|
+
|
|
6819
7627
|
// src/db/audit.ts
|
|
6820
7628
|
init_database();
|
|
6821
7629
|
function logTaskChange(taskId, action, field2, oldValue, newValue, agentId, db) {
|
|
@@ -7078,7 +7886,7 @@ async function deliverWebhook(wh, event, body, attempt, db) {
|
|
|
7078
7886
|
activeDeliveries--;
|
|
7079
7887
|
}
|
|
7080
7888
|
}
|
|
7081
|
-
async function
|
|
7889
|
+
async function dispatchWebhook2(event, payload, db) {
|
|
7082
7890
|
const d = db || getDatabase();
|
|
7083
7891
|
const webhooks = listWebhooks(d).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
|
|
7084
7892
|
const payloadObj = typeof payload === "object" && payload !== null ? payload : {};
|
|
@@ -7232,7 +8040,10 @@ function createTask(input, db) {
|
|
|
7232
8040
|
insertTaskTags(id, tags, d);
|
|
7233
8041
|
}
|
|
7234
8042
|
const task = getTask(id, d);
|
|
7235
|
-
|
|
8043
|
+
const payload = taskEventData(task);
|
|
8044
|
+
dispatchWebhook2("task.created", payload, d).catch(() => {});
|
|
8045
|
+
emitLocalEventHooksQuiet({ type: "task.created", payload });
|
|
8046
|
+
emitSharedTaskEventQuiet({ type: "task.created", task });
|
|
7236
8047
|
return task;
|
|
7237
8048
|
}
|
|
7238
8049
|
function getTask(id, db) {
|
|
@@ -7576,18 +8387,7 @@ function updateTask(id, input, db) {
|
|
|
7576
8387
|
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
7577
8388
|
if (input.approved_by !== undefined)
|
|
7578
8389
|
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
7579
|
-
|
|
7580
|
-
dispatchWebhook("task.assigned", { id, assigned_to: input.assigned_to, title: task.title }, d).catch(() => {});
|
|
7581
|
-
emitLocalEventHooksQuiet({ type: "task.assigned", payload: { id, assigned_to: input.assigned_to, title: task.title } });
|
|
7582
|
-
}
|
|
7583
|
-
if (input.status !== undefined && input.status !== task.status) {
|
|
7584
|
-
dispatchWebhook("task.status_changed", { id, old_status: task.status, new_status: input.status, title: task.title }, d).catch(() => {});
|
|
7585
|
-
emitLocalEventHooksQuiet({ type: "task.status_changed", payload: { id, old_status: task.status, new_status: input.status, title: task.title } });
|
|
7586
|
-
}
|
|
7587
|
-
if (input.approved_by !== undefined) {
|
|
7588
|
-
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
|
|
7589
|
-
}
|
|
7590
|
-
return {
|
|
8390
|
+
const updatedTask = {
|
|
7591
8391
|
...task,
|
|
7592
8392
|
...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
|
|
7593
8393
|
tags: input.tags ?? task.tags,
|
|
@@ -7605,6 +8405,22 @@ function updateTask(id, input, db) {
|
|
|
7605
8405
|
approved_by: input.approved_by ?? task.approved_by,
|
|
7606
8406
|
approved_at: input.approved_by ? timestamp : task.approved_at
|
|
7607
8407
|
};
|
|
8408
|
+
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
|
|
8409
|
+
const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
|
|
8410
|
+
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
8411
|
+
emitLocalEventHooksQuiet({ type: "task.assigned", payload });
|
|
8412
|
+
emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to } });
|
|
8413
|
+
}
|
|
8414
|
+
if (input.status !== undefined && input.status !== task.status) {
|
|
8415
|
+
const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
|
|
8416
|
+
dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
|
|
8417
|
+
emitLocalEventHooksQuiet({ type: "task.status_changed", payload });
|
|
8418
|
+
emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status } });
|
|
8419
|
+
}
|
|
8420
|
+
if (input.approved_by !== undefined) {
|
|
8421
|
+
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
|
|
8422
|
+
}
|
|
8423
|
+
return updatedTask;
|
|
7608
8424
|
}
|
|
7609
8425
|
function deleteTask(id, db) {
|
|
7610
8426
|
const d = db || getDatabase();
|
|
@@ -8337,9 +9153,12 @@ function startTask(id, agentId, db) {
|
|
|
8337
9153
|
throw new Error(`Task ${id} could not be started because it changed during claim`);
|
|
8338
9154
|
}
|
|
8339
9155
|
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
9156
|
+
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 };
|
|
9157
|
+
const payload = taskEventData(startedTask, { agent_id: agentId });
|
|
9158
|
+
dispatchWebhook2("task.started", payload, d).catch(() => {});
|
|
9159
|
+
emitLocalEventHooksQuiet({ type: "task.started", payload });
|
|
9160
|
+
emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId } });
|
|
9161
|
+
return startedTask;
|
|
8343
9162
|
}
|
|
8344
9163
|
function completeTask(id, agentId, db, options) {
|
|
8345
9164
|
const d = db || getDatabase();
|
|
@@ -8375,8 +9194,21 @@ function completeTask(id, agentId, db, options) {
|
|
|
8375
9194
|
});
|
|
8376
9195
|
tx();
|
|
8377
9196
|
logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
|
|
8378
|
-
|
|
8379
|
-
|
|
9197
|
+
const completedTaskForEvent = {
|
|
9198
|
+
...task,
|
|
9199
|
+
status: "completed",
|
|
9200
|
+
locked_by: null,
|
|
9201
|
+
locked_at: null,
|
|
9202
|
+
completed_at: timestamp,
|
|
9203
|
+
confidence,
|
|
9204
|
+
version: task.version + 1,
|
|
9205
|
+
updated_at: timestamp,
|
|
9206
|
+
metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
|
|
9207
|
+
};
|
|
9208
|
+
const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
|
|
9209
|
+
dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
|
|
9210
|
+
emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload });
|
|
9211
|
+
emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp } });
|
|
8380
9212
|
let spawnedTask = null;
|
|
8381
9213
|
if (task.recurrence_rule && !options?.skip_recurrence) {
|
|
8382
9214
|
spawnedTask = spawnNextRecurrence(task, d, timestamp);
|
|
@@ -8417,8 +9249,12 @@ function completeTask(id, agentId, db, options) {
|
|
|
8417
9249
|
if (unblockedDeps.length > 0) {
|
|
8418
9250
|
meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
|
|
8419
9251
|
for (const dep of unblockedDeps) {
|
|
8420
|
-
|
|
8421
|
-
|
|
9252
|
+
const depTask = getTask(dep.id, d);
|
|
9253
|
+
const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
|
|
9254
|
+
dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
|
|
9255
|
+
emitLocalEventHooksQuiet({ type: "task.unblocked", payload });
|
|
9256
|
+
if (depTask)
|
|
9257
|
+
emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id } });
|
|
8422
9258
|
}
|
|
8423
9259
|
}
|
|
8424
9260
|
return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
|
|
@@ -8604,9 +9440,6 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
8604
9440
|
const timestamp = now();
|
|
8605
9441
|
d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
|
|
8606
9442
|
WHERE id = ?`, [JSON.stringify(meta), timestamp, id]);
|
|
8607
|
-
logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
|
|
8608
|
-
dispatchWebhook("task.failed", { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title }, d).catch(() => {});
|
|
8609
|
-
emitLocalEventHooksQuiet({ type: "task.failed", payload: { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title } });
|
|
8610
9443
|
const failedTask = {
|
|
8611
9444
|
...task,
|
|
8612
9445
|
status: "failed",
|
|
@@ -8616,6 +9449,11 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
8616
9449
|
version: task.version + 1,
|
|
8617
9450
|
updated_at: timestamp
|
|
8618
9451
|
};
|
|
9452
|
+
logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
|
|
9453
|
+
const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
|
|
9454
|
+
dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
|
|
9455
|
+
emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload });
|
|
9456
|
+
emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning" });
|
|
8619
9457
|
let retryTask;
|
|
8620
9458
|
if (options?.retry) {
|
|
8621
9459
|
const retryCount = (task.retry_count || 0) + 1;
|
|
@@ -8690,9 +9528,12 @@ function stealTask(agentId, opts, db) {
|
|
|
8690
9528
|
return null;
|
|
8691
9529
|
logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
|
|
8692
9530
|
logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
9531
|
+
const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
|
|
9532
|
+
const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
|
|
9533
|
+
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
9534
|
+
emitLocalEventHooksQuiet({ type: "task.assigned", payload });
|
|
9535
|
+
emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to } });
|
|
9536
|
+
return stolenTask;
|
|
8696
9537
|
}
|
|
8697
9538
|
function claimOrSteal(agentId, filters, db) {
|
|
8698
9539
|
const d = db || getDatabase();
|
|
@@ -9751,8 +10592,8 @@ init_database();
|
|
|
9751
10592
|
init_database();
|
|
9752
10593
|
init_redaction();
|
|
9753
10594
|
import { createHash as createHash2 } from "crypto";
|
|
9754
|
-
import { existsSync as
|
|
9755
|
-
import { basename, dirname as dirname5, join as
|
|
10595
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
10596
|
+
import { basename, dirname as dirname5, join as join6, resolve as resolve6 } from "path";
|
|
9756
10597
|
import { tmpdir } from "os";
|
|
9757
10598
|
function isInMemoryDb2(path) {
|
|
9758
10599
|
return path === ":memory:" || path.startsWith("file::memory:");
|
|
@@ -9764,15 +10605,15 @@ function artifactStoreRoot() {
|
|
|
9764
10605
|
return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
|
|
9765
10606
|
const dbPath = getDatabasePath();
|
|
9766
10607
|
if (isInMemoryDb2(dbPath))
|
|
9767
|
-
return
|
|
9768
|
-
return
|
|
10608
|
+
return join6(tmpdir(), "hasna-todos-artifacts");
|
|
10609
|
+
return join6(dirname5(resolve6(dbPath)), "artifacts");
|
|
9769
10610
|
}
|
|
9770
10611
|
function artifactStorePath(relativePath) {
|
|
9771
10612
|
const normalized = relativePath.replace(/\\/g, "/");
|
|
9772
10613
|
if (normalized.includes("..") || normalized.startsWith("/") || normalized.length === 0) {
|
|
9773
10614
|
throw new Error("Invalid artifact store path");
|
|
9774
10615
|
}
|
|
9775
|
-
return
|
|
10616
|
+
return join6(artifactStoreRoot(), normalized);
|
|
9776
10617
|
}
|
|
9777
10618
|
function sha256(buffer) {
|
|
9778
10619
|
return createHash2("sha256").update(buffer).digest("hex");
|
|
@@ -9813,7 +10654,7 @@ function mediaTypeFor(path, textLike) {
|
|
|
9813
10654
|
}
|
|
9814
10655
|
function storeArtifactContent(input) {
|
|
9815
10656
|
const sourcePath = resolve6(input.path);
|
|
9816
|
-
if (!
|
|
10657
|
+
if (!existsSync8(sourcePath))
|
|
9817
10658
|
return null;
|
|
9818
10659
|
const sourceStat = statSync2(sourcePath);
|
|
9819
10660
|
if (!sourceStat.isFile())
|
|
@@ -9830,9 +10671,9 @@ function storeArtifactContent(input) {
|
|
|
9830
10671
|
redactionStatus = "redacted";
|
|
9831
10672
|
}
|
|
9832
10673
|
const storedSha = sha256(storedBuffer);
|
|
9833
|
-
const relativePath =
|
|
10674
|
+
const relativePath = join6("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
|
|
9834
10675
|
const destination = artifactStorePath(relativePath);
|
|
9835
|
-
if (!
|
|
10676
|
+
if (!existsSync8(destination)) {
|
|
9836
10677
|
mkdirSync4(dirname5(destination), { recursive: true });
|
|
9837
10678
|
writeFileSync2(destination, storedBuffer);
|
|
9838
10679
|
}
|
|
@@ -9892,7 +10733,7 @@ function verifyStoredArtifact(input) {
|
|
|
9892
10733
|
};
|
|
9893
10734
|
}
|
|
9894
10735
|
const storedPath = artifactStorePath(store.relative_path);
|
|
9895
|
-
if (!
|
|
10736
|
+
if (!existsSync8(storedPath)) {
|
|
9896
10737
|
return {
|
|
9897
10738
|
id: input.id,
|
|
9898
10739
|
path: input.path,
|
|
@@ -9973,15 +10814,15 @@ function getArtifactStoreRoot(dbPath) {
|
|
|
9973
10814
|
return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
|
|
9974
10815
|
const path = dbPath ?? getDatabasePath();
|
|
9975
10816
|
if (isInMemoryDb2(path))
|
|
9976
|
-
return
|
|
9977
|
-
return
|
|
10817
|
+
return join6(tmpdir(), "hasna-todos-artifacts");
|
|
10818
|
+
return join6(dirname5(resolve6(path)), "artifacts");
|
|
9978
10819
|
}
|
|
9979
10820
|
function computeContentHash(path) {
|
|
9980
10821
|
return sha256(readFileSync4(resolve6(path)));
|
|
9981
10822
|
}
|
|
9982
10823
|
function storeArtifactFile(input) {
|
|
9983
10824
|
const sourcePath = resolve6(input.sourcePath);
|
|
9984
|
-
if (!
|
|
10825
|
+
if (!existsSync8(sourcePath)) {
|
|
9985
10826
|
throw new Error(`Source file not found: ${input.sourcePath}`);
|
|
9986
10827
|
}
|
|
9987
10828
|
if (!statSync2(sourcePath).isFile()) {
|
|
@@ -9994,7 +10835,7 @@ function storeArtifactFile(input) {
|
|
|
9994
10835
|
let localPath = sourcePath;
|
|
9995
10836
|
if (storageMode === "copy") {
|
|
9996
10837
|
const fileName = input.name && input.name.trim().length > 0 ? basename(input.name) : basename(sourcePath);
|
|
9997
|
-
const destination =
|
|
10838
|
+
const destination = join6(getArtifactStoreRoot(input.dbPath), input.artifactId, fileName);
|
|
9998
10839
|
mkdirSync4(dirname5(destination), { recursive: true });
|
|
9999
10840
|
writeFileSync2(destination, buffer);
|
|
10000
10841
|
localPath = destination;
|
|
@@ -10004,7 +10845,7 @@ function storeArtifactFile(input) {
|
|
|
10004
10845
|
function deleteStoredArtifactFile(localPath, storageMode, _dbPath2) {
|
|
10005
10846
|
if (storageMode === "reference")
|
|
10006
10847
|
return false;
|
|
10007
|
-
if (!localPath || !
|
|
10848
|
+
if (!localPath || !existsSync8(localPath))
|
|
10008
10849
|
return false;
|
|
10009
10850
|
rmSync(localPath, { force: true });
|
|
10010
10851
|
try {
|
|
@@ -10016,8 +10857,8 @@ function isArtifactExpired(deletedAt, policy = {}) {
|
|
|
10016
10857
|
if (!deletedAt)
|
|
10017
10858
|
return false;
|
|
10018
10859
|
const retentionDays = policy.deleted_retention_days ?? DEFAULT_DELETED_RETENTION_DAYS;
|
|
10019
|
-
const
|
|
10020
|
-
const ageMs =
|
|
10860
|
+
const now3 = policy.now ?? new Date;
|
|
10861
|
+
const ageMs = now3.getTime() - new Date(deletedAt).getTime();
|
|
10021
10862
|
return ageMs > retentionDays * 24 * 60 * 60 * 1000;
|
|
10022
10863
|
}
|
|
10023
10864
|
function buildArtifactExportManifest(artifacts, dbPath) {
|
|
@@ -11092,7 +11933,7 @@ function rowToTask2(row) {
|
|
|
11092
11933
|
requires_approval: Boolean(row.requires_approval)
|
|
11093
11934
|
};
|
|
11094
11935
|
}
|
|
11095
|
-
function
|
|
11936
|
+
function rowToTaskList2(row) {
|
|
11096
11937
|
return { ...row, metadata: parseJsonObject3(row.metadata) };
|
|
11097
11938
|
}
|
|
11098
11939
|
function rowWithMetadata(row) {
|
|
@@ -11128,7 +11969,7 @@ function createLocalBridgeBundle(options = {}, db) {
|
|
|
11128
11969
|
const project = options.project_id ? d.query("SELECT * FROM projects WHERE id = ?").get(options.project_id) : null;
|
|
11129
11970
|
const data = redactValue({
|
|
11130
11971
|
projects: options.project_id ? project ? [project] : [] : d.query("SELECT * FROM projects ORDER BY name").all(),
|
|
11131
|
-
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(
|
|
11972
|
+
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),
|
|
11132
11973
|
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(),
|
|
11133
11974
|
tasks: queryByTaskIds(d, "SELECT * FROM tasks WHERE id IN (__TASK_IDS__) ORDER BY created_at", taskIds).map(rowToTask2),
|
|
11134
11975
|
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),
|
|
@@ -11783,7 +12624,7 @@ function writeOnboardingFixtureFiles(directory) {
|
|
|
11783
12624
|
mkdirSync5(directory, { recursive: true });
|
|
11784
12625
|
const files = [];
|
|
11785
12626
|
for (const fixture of allFixtures()) {
|
|
11786
|
-
const path =
|
|
12627
|
+
const path = join7(directory, `${fixture.summary.name}.bridge.json`);
|
|
11787
12628
|
writeFileSync3(path, `${JSON.stringify(fixture.bundle, null, 2)}
|
|
11788
12629
|
`, "utf-8");
|
|
11789
12630
|
files.push(path);
|
|
@@ -12609,7 +13450,7 @@ function renderLocalSnapshotMarkdown(snapshot) {
|
|
|
12609
13450
|
}
|
|
12610
13451
|
// src/lib/sdk-integration-fixtures.ts
|
|
12611
13452
|
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
12612
|
-
import { join as
|
|
13453
|
+
import { join as join8 } from "path";
|
|
12613
13454
|
|
|
12614
13455
|
// src/cli-mcp-parity.ts
|
|
12615
13456
|
function source4(version) {
|
|
@@ -13919,7 +14760,7 @@ function limits(input) {
|
|
|
13919
14760
|
stale_after_hours: clamp(input.stale_after_hours, DEFAULT_LIMITS.stale_after_hours, 24 * 365)
|
|
13920
14761
|
};
|
|
13921
14762
|
}
|
|
13922
|
-
function
|
|
14763
|
+
function truncate2(value, max) {
|
|
13923
14764
|
if (!value)
|
|
13924
14765
|
return value ?? null;
|
|
13925
14766
|
const redacted = redactEvidenceText(value);
|
|
@@ -13940,9 +14781,9 @@ function acceptanceCriteria(task2, maxText) {
|
|
|
13940
14781
|
const metadata = task2.metadata || {};
|
|
13941
14782
|
const raw = metadata["acceptance_criteria"] ?? metadata["acceptanceCriteria"] ?? metadata["criteria"];
|
|
13942
14783
|
if (Array.isArray(raw))
|
|
13943
|
-
return raw.map((item) =>
|
|
14784
|
+
return raw.map((item) => truncate2(String(item), maxText)).filter((item) => Boolean(item));
|
|
13944
14785
|
if (typeof raw === "string") {
|
|
13945
|
-
return raw.split(/\r?\n/).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).map((line) =>
|
|
14786
|
+
return raw.split(/\r?\n/).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).map((line) => truncate2(line, maxText)).filter((item) => Boolean(item));
|
|
13946
14787
|
}
|
|
13947
14788
|
return [];
|
|
13948
14789
|
}
|
|
@@ -13965,7 +14806,7 @@ function addFile(files, path, source5, base) {
|
|
|
13965
14806
|
path,
|
|
13966
14807
|
status: base?.status || "active",
|
|
13967
14808
|
agent_id: base?.agent_id ?? null,
|
|
13968
|
-
note:
|
|
14809
|
+
note: truncate2(base?.note, 240),
|
|
13969
14810
|
updated_at: base?.updated_at || "",
|
|
13970
14811
|
sources: [source5]
|
|
13971
14812
|
});
|
|
@@ -14023,7 +14864,7 @@ function estimateTokens(value) {
|
|
|
14023
14864
|
return Math.max(1, Math.ceil((text || "").length / 4));
|
|
14024
14865
|
}
|
|
14025
14866
|
function summarizeStrings(values, maxChars) {
|
|
14026
|
-
return
|
|
14867
|
+
return truncate2(values.filter(Boolean).join("; "), maxChars) || "No local details were available before this section was omitted.";
|
|
14027
14868
|
}
|
|
14028
14869
|
function summarizeSection(pack, section, maxChars) {
|
|
14029
14870
|
if (section === "project")
|
|
@@ -14214,8 +15055,8 @@ function createAgentContextPack(input, db) {
|
|
|
14214
15055
|
...taskFiles.map((file) => file.updated_at)
|
|
14215
15056
|
], task2.updated_at);
|
|
14216
15057
|
const warnings = [];
|
|
14217
|
-
const
|
|
14218
|
-
if (Date.parse(task2.updated_at) <
|
|
15058
|
+
const now3 = input.now ? new Date(input.now) : new Date;
|
|
15059
|
+
if (Date.parse(task2.updated_at) < now3.getTime() - limit.stale_after_hours * 60 * 60 * 1000) {
|
|
14219
15060
|
warnings.push(`task state is older than ${limit.stale_after_hours} hours`);
|
|
14220
15061
|
}
|
|
14221
15062
|
if (comments.length > recentComments.length)
|
|
@@ -14230,7 +15071,7 @@ function createAgentContextPack(input, db) {
|
|
|
14230
15071
|
id: task2.id,
|
|
14231
15072
|
short_id: task2.short_id,
|
|
14232
15073
|
title: redactEvidenceText(task2.title),
|
|
14233
|
-
description:
|
|
15074
|
+
description: truncate2(task2.description, limit.max_text_chars),
|
|
14234
15075
|
status: task2.status,
|
|
14235
15076
|
priority: task2.priority,
|
|
14236
15077
|
assigned_to: task2.assigned_to,
|
|
@@ -14254,7 +15095,7 @@ function createAgentContextPack(input, db) {
|
|
|
14254
15095
|
plan: plan ? {
|
|
14255
15096
|
id: plan.id,
|
|
14256
15097
|
name: plan.name,
|
|
14257
|
-
description:
|
|
15098
|
+
description: truncate2(plan.description, limit.max_text_chars),
|
|
14258
15099
|
status: plan.status,
|
|
14259
15100
|
agent_id: plan.agent_id,
|
|
14260
15101
|
tasks: planTasks.slice(0, limit.plan_task_limit).map(taskSummary).filter((item) => Boolean(item)),
|
|
@@ -14273,7 +15114,7 @@ function createAgentContextPack(input, db) {
|
|
|
14273
15114
|
type: comment.type,
|
|
14274
15115
|
progress_pct: comment.progress_pct,
|
|
14275
15116
|
created_at: comment.created_at,
|
|
14276
|
-
content:
|
|
15117
|
+
content: truncate2(comment.content, limit.max_text_chars) || ""
|
|
14277
15118
|
})),
|
|
14278
15119
|
omitted: Math.max(0, comments.length - recentComments.length)
|
|
14279
15120
|
},
|
|
@@ -14281,7 +15122,7 @@ function createAgentContextPack(input, db) {
|
|
|
14281
15122
|
traceability: {
|
|
14282
15123
|
commits: traceability.commits.map((commit) => ({
|
|
14283
15124
|
sha: commit.sha,
|
|
14284
|
-
message:
|
|
15125
|
+
message: truncate2(commit.message, 240),
|
|
14285
15126
|
files_changed: commit.files_changed,
|
|
14286
15127
|
committed_at: commit.committed_at
|
|
14287
15128
|
})),
|
|
@@ -14289,7 +15130,7 @@ function createAgentContextPack(input, db) {
|
|
|
14289
15130
|
verifications: verifications.map((verification) => ({
|
|
14290
15131
|
command: verification.command,
|
|
14291
15132
|
status: verification.status,
|
|
14292
|
-
output_summary:
|
|
15133
|
+
output_summary: truncate2(verification.output_summary, limit.max_text_chars),
|
|
14293
15134
|
artifact_path: verification.artifact_path,
|
|
14294
15135
|
run_at: verification.run_at
|
|
14295
15136
|
})),
|
|
@@ -14300,14 +15141,14 @@ function createAgentContextPack(input, db) {
|
|
|
14300
15141
|
id: ledger.run.id,
|
|
14301
15142
|
title: ledger.run.title,
|
|
14302
15143
|
status: ledger.run.status,
|
|
14303
|
-
summary:
|
|
15144
|
+
summary: truncate2(ledger.run.summary, limit.max_text_chars),
|
|
14304
15145
|
agent_id: ledger.run.agent_id,
|
|
14305
15146
|
started_at: ledger.run.started_at,
|
|
14306
15147
|
completed_at: ledger.run.completed_at,
|
|
14307
|
-
events: ledger.events.map((event) => ({ event_type: event.event_type, message:
|
|
14308
|
-
commands: ledger.commands.map((command) => ({ command: command.command, status: command.status, output_summary:
|
|
14309
|
-
files: ledger.files.map((file) => ({ path: file.path, status: file.status, note:
|
|
14310
|
-
artifacts: ledger.artifacts.map((artifact) => ({ path: artifact.path, artifact_type: artifact.artifact_type, description:
|
|
15148
|
+
events: ledger.events.map((event) => ({ event_type: event.event_type, message: truncate2(event.message, 500), created_at: event.created_at })),
|
|
15149
|
+
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 })),
|
|
15150
|
+
files: ledger.files.map((file) => ({ path: file.path, status: file.status, note: truncate2(file.note, 240) })),
|
|
15151
|
+
artifacts: ledger.artifacts.map((artifact) => ({ path: artifact.path, artifact_type: artifact.artifact_type, description: truncate2(artifact.description, 240), sha256: artifact.sha256 }))
|
|
14311
15152
|
})),
|
|
14312
15153
|
omitted: Math.max(0, runs.length - selectedRuns.length)
|
|
14313
15154
|
},
|
|
@@ -14412,7 +15253,7 @@ function renderAgentContextPackCompactMarkdown(pack) {
|
|
|
14412
15253
|
const lines = [
|
|
14413
15254
|
`# Context: ${pack.task.title}`,
|
|
14414
15255
|
`${pack.task.status} | ${pack.task.priority} | ${pack.task.short_id || pack.task.id.slice(0, 8)}`,
|
|
14415
|
-
pack.task.description ?
|
|
15256
|
+
pack.task.description ? truncate2(pack.task.description, Math.min(pack.limits.summary_char_limit, 700)) : null,
|
|
14416
15257
|
"",
|
|
14417
15258
|
"## Must Know",
|
|
14418
15259
|
bullet([
|
|
@@ -14558,7 +15399,7 @@ function writeSdkIntegrationFixtures(directory, options = {}) {
|
|
|
14558
15399
|
];
|
|
14559
15400
|
const written = [];
|
|
14560
15401
|
for (const [name, payload] of files) {
|
|
14561
|
-
const file =
|
|
15402
|
+
const file = join8(directory, name);
|
|
14562
15403
|
writeFileSync5(file, `${JSON.stringify(payload, null, 2)}
|
|
14563
15404
|
`, "utf-8");
|
|
14564
15405
|
written.push(file);
|
|
@@ -15275,7 +16116,7 @@ function createRoadmap(input) {
|
|
|
15275
16116
|
if (!name)
|
|
15276
16117
|
throw new Error("Roadmap name is required");
|
|
15277
16118
|
const store = readStore();
|
|
15278
|
-
const
|
|
16119
|
+
const now3 = timestamp();
|
|
15279
16120
|
const roadmap = {
|
|
15280
16121
|
id: newId("roadmap"),
|
|
15281
16122
|
name,
|
|
@@ -15286,8 +16127,8 @@ function createRoadmap(input) {
|
|
|
15286
16127
|
agent_id: cleanString(input.agent_id),
|
|
15287
16128
|
release: cleanString(input.release),
|
|
15288
16129
|
milestone_ids: [],
|
|
15289
|
-
created_at:
|
|
15290
|
-
updated_at:
|
|
16130
|
+
created_at: now3,
|
|
16131
|
+
updated_at: now3
|
|
15291
16132
|
};
|
|
15292
16133
|
store.roadmaps[roadmap.id] = roadmap;
|
|
15293
16134
|
writeStore(store);
|
|
@@ -15347,7 +16188,7 @@ function createMilestone(input) {
|
|
|
15347
16188
|
const title = input.title.trim();
|
|
15348
16189
|
if (!title)
|
|
15349
16190
|
throw new Error("Milestone title is required");
|
|
15350
|
-
const
|
|
16191
|
+
const now3 = timestamp();
|
|
15351
16192
|
const milestone = {
|
|
15352
16193
|
id: newId("milestone"),
|
|
15353
16194
|
roadmap_id: roadmapId,
|
|
@@ -15362,11 +16203,11 @@ function createMilestone(input) {
|
|
|
15362
16203
|
run_ids: cleanList3(input.run_ids),
|
|
15363
16204
|
release: cleanString(input.release ?? roadmap.release ?? undefined),
|
|
15364
16205
|
tags: cleanList3(input.tags),
|
|
15365
|
-
created_at:
|
|
15366
|
-
updated_at:
|
|
16206
|
+
created_at: now3,
|
|
16207
|
+
updated_at: now3
|
|
15367
16208
|
};
|
|
15368
16209
|
store.milestones[milestone.id] = milestone;
|
|
15369
|
-
store.roadmaps[roadmapId] = { ...roadmap, milestone_ids: cleanList3([...roadmap.milestone_ids, milestone.id]), updated_at:
|
|
16210
|
+
store.roadmaps[roadmapId] = { ...roadmap, milestone_ids: cleanList3([...roadmap.milestone_ids, milestone.id]), updated_at: now3 };
|
|
15370
16211
|
writeStore(store);
|
|
15371
16212
|
return milestone;
|
|
15372
16213
|
}
|
|
@@ -15426,7 +16267,7 @@ function upsertReleaseGroup(input) {
|
|
|
15426
16267
|
throw new Error("Release group name is required");
|
|
15427
16268
|
const key = releaseKey(roadmapId, name);
|
|
15428
16269
|
const existing = store.releases[key];
|
|
15429
|
-
const
|
|
16270
|
+
const now3 = timestamp();
|
|
15430
16271
|
const release = {
|
|
15431
16272
|
name,
|
|
15432
16273
|
version: input.version === undefined ? existing?.version ?? null : cleanString(input.version),
|
|
@@ -15437,8 +16278,8 @@ function upsertReleaseGroup(input) {
|
|
|
15437
16278
|
plan_ids: input.plan_ids === undefined ? existing?.plan_ids ?? [] : cleanList3(input.plan_ids),
|
|
15438
16279
|
run_ids: input.run_ids === undefined ? existing?.run_ids ?? [] : cleanList3(input.run_ids),
|
|
15439
16280
|
notes: input.notes === undefined ? existing?.notes ?? null : cleanString(input.notes),
|
|
15440
|
-
created_at: existing?.created_at ??
|
|
15441
|
-
updated_at:
|
|
16281
|
+
created_at: existing?.created_at ?? now3,
|
|
16282
|
+
updated_at: now3
|
|
15442
16283
|
};
|
|
15443
16284
|
store.releases[key] = release;
|
|
15444
16285
|
writeStore(store);
|
|
@@ -15829,7 +16670,7 @@ function renderLocalAuditLedgerMarkdown(ledger) {
|
|
|
15829
16670
|
init_migrations();
|
|
15830
16671
|
init_schema();
|
|
15831
16672
|
import { readFileSync as readFileSync6 } from "fs";
|
|
15832
|
-
import { join as
|
|
16673
|
+
import { join as join9, resolve as resolve8 } from "path";
|
|
15833
16674
|
import { Database as Database2 } from "bun:sqlite";
|
|
15834
16675
|
var LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION = 1;
|
|
15835
16676
|
var EXPECTED_PACKAGE_NAME = "@hasna/todos";
|
|
@@ -15875,7 +16716,7 @@ function warn(id, message, details) {
|
|
|
15875
16716
|
return { id, status: "warning", message, details };
|
|
15876
16717
|
}
|
|
15877
16718
|
function readPackageJson(root) {
|
|
15878
|
-
return JSON.parse(readFileSync6(
|
|
16719
|
+
return JSON.parse(readFileSync6(join9(root, "package.json"), "utf8"));
|
|
15879
16720
|
}
|
|
15880
16721
|
function sortedKeys(value) {
|
|
15881
16722
|
return Object.keys(value ?? {}).sort((left, right) => left.localeCompare(right));
|
|
@@ -17287,6 +18128,470 @@ function importExternalIssues(input, db) {
|
|
|
17287
18128
|
]
|
|
17288
18129
|
};
|
|
17289
18130
|
}
|
|
18131
|
+
// src/lib/tester-issue-reports.ts
|
|
18132
|
+
init_database();
|
|
18133
|
+
import { createHash as createHash7 } from "crypto";
|
|
18134
|
+
init_redaction();
|
|
18135
|
+
var TESTERS_ISSUE_REPORT_SCHEMA_VERSION = "testers.issue_report.v1";
|
|
18136
|
+
var TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION = "todos.tester_issue_report_result.v1";
|
|
18137
|
+
var TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION = "todos.tester_issue_report_batch_result.v1";
|
|
18138
|
+
var PRIORITIES3 = ["low", "medium", "high", "critical"];
|
|
18139
|
+
var SEVERITIES = new Set(PRIORITIES3);
|
|
18140
|
+
var KINDS = new Set([
|
|
18141
|
+
"assertion_failure",
|
|
18142
|
+
"runtime_error",
|
|
18143
|
+
"console_error",
|
|
18144
|
+
"network_error",
|
|
18145
|
+
"visual_regression",
|
|
18146
|
+
"accessibility",
|
|
18147
|
+
"performance",
|
|
18148
|
+
"broken_link",
|
|
18149
|
+
"security",
|
|
18150
|
+
"unknown"
|
|
18151
|
+
]);
|
|
18152
|
+
function asObject3(value) {
|
|
18153
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
18154
|
+
}
|
|
18155
|
+
function asString3(value) {
|
|
18156
|
+
if (typeof value === "string" && value.trim())
|
|
18157
|
+
return value.trim();
|
|
18158
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
18159
|
+
return String(value);
|
|
18160
|
+
return null;
|
|
18161
|
+
}
|
|
18162
|
+
function stringArray(value, limit = 20) {
|
|
18163
|
+
if (!Array.isArray(value))
|
|
18164
|
+
return [];
|
|
18165
|
+
return [...new Set(value.map((item) => asString3(item)).filter((item) => Boolean(item)))].slice(0, limit);
|
|
18166
|
+
}
|
|
18167
|
+
function objectArray(value, limit = 20) {
|
|
18168
|
+
if (!Array.isArray(value))
|
|
18169
|
+
return [];
|
|
18170
|
+
return value.map(asObject3).filter((item) => Object.keys(item).length > 0).slice(0, limit);
|
|
18171
|
+
}
|
|
18172
|
+
function cleanKey(value) {
|
|
18173
|
+
return value.toLowerCase().trim().replace(/[^a-z0-9._:-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
18174
|
+
}
|
|
18175
|
+
function truncate3(value, max) {
|
|
18176
|
+
if (!value)
|
|
18177
|
+
return;
|
|
18178
|
+
const redacted = redactEvidenceText(value).trim();
|
|
18179
|
+
if (!redacted)
|
|
18180
|
+
return;
|
|
18181
|
+
return redacted.length > max ? `${redacted.slice(0, max - 3)}...` : redacted;
|
|
18182
|
+
}
|
|
18183
|
+
function normalizeText2(value) {
|
|
18184
|
+
return (value || "").toLowerCase().replace(/https?:\/\/\S+/g, " ").replace(/[`"'()[\]{}.,:;!?/#\\_-]+/g, " ").replace(/\s+/g, " ").trim();
|
|
18185
|
+
}
|
|
18186
|
+
function normalizeUrlPattern(value) {
|
|
18187
|
+
if (!value)
|
|
18188
|
+
return "";
|
|
18189
|
+
try {
|
|
18190
|
+
const url = new URL(value);
|
|
18191
|
+
return `${url.origin.toLowerCase()}${url.pathname.replace(/\/+$/, "") || "/"}`;
|
|
18192
|
+
} catch {
|
|
18193
|
+
return value.split(/[?#]/, 1)[0].replace(/\/+$/, "").toLowerCase();
|
|
18194
|
+
}
|
|
18195
|
+
}
|
|
18196
|
+
function normalizeKind(value) {
|
|
18197
|
+
const raw = cleanKey(asString3(value) || "unknown").replace(/-/g, "_");
|
|
18198
|
+
return KINDS.has(raw) ? raw : raw || "unknown";
|
|
18199
|
+
}
|
|
18200
|
+
function normalizeSeverity(value, fallback) {
|
|
18201
|
+
const raw = cleanKey(asString3(value) || fallback);
|
|
18202
|
+
if (SEVERITIES.has(raw))
|
|
18203
|
+
return raw;
|
|
18204
|
+
if (/^(p0|blocker|urgent|highest)$/.test(raw))
|
|
18205
|
+
return "critical";
|
|
18206
|
+
if (/^(p1|major)$/.test(raw))
|
|
18207
|
+
return "high";
|
|
18208
|
+
if (/^(p3|minor|info)$/.test(raw))
|
|
18209
|
+
return "low";
|
|
18210
|
+
return fallback;
|
|
18211
|
+
}
|
|
18212
|
+
function reportSource(input) {
|
|
18213
|
+
const source6 = asObject3(input["source"]);
|
|
18214
|
+
const normalized = {
|
|
18215
|
+
tool: asString3(source6["tool"]) || asString3(input["tool"]) || "testers",
|
|
18216
|
+
run_id: asString3(source6["run_id"]) || asString3(input["run_id"]) || undefined,
|
|
18217
|
+
result_id: asString3(source6["result_id"]) || asString3(input["result_id"]) || undefined,
|
|
18218
|
+
scenario_id: asString3(source6["scenario_id"]) || asString3(input["scenario_id"]) || undefined,
|
|
18219
|
+
scenario_name: asString3(source6["scenario_name"]) || asString3(input["scenario_name"]) || undefined,
|
|
18220
|
+
project_id: asString3(source6["project_id"]) || asString3(input["project_id"]) || undefined,
|
|
18221
|
+
url: asString3(source6["url"]) || asString3(input["url"]) || undefined,
|
|
18222
|
+
page_url: asString3(source6["page_url"]) || asString3(input["page_url"]) || undefined,
|
|
18223
|
+
artifact_url: asString3(source6["artifact_url"]) || undefined,
|
|
18224
|
+
screenshot_url: asString3(source6["screenshot_url"]) || undefined,
|
|
18225
|
+
commit: asString3(source6["commit"]) || undefined,
|
|
18226
|
+
branch: asString3(source6["branch"]) || undefined
|
|
18227
|
+
};
|
|
18228
|
+
return Object.values(normalized).some(Boolean) ? normalized : undefined;
|
|
18229
|
+
}
|
|
18230
|
+
function reportTarget(input) {
|
|
18231
|
+
const target = asObject3(input["target"]);
|
|
18232
|
+
const normalized = {
|
|
18233
|
+
url: asString3(target["url"]) || asString3(input["target_url"]) || undefined,
|
|
18234
|
+
route: asString3(target["route"]) || undefined,
|
|
18235
|
+
selector: asString3(target["selector"]) || undefined,
|
|
18236
|
+
component: asString3(target["component"]) || undefined,
|
|
18237
|
+
browser: asString3(target["browser"]) || undefined,
|
|
18238
|
+
viewport: asString3(target["viewport"]) || undefined
|
|
18239
|
+
};
|
|
18240
|
+
return Object.values(normalized).some(Boolean) ? normalized : undefined;
|
|
18241
|
+
}
|
|
18242
|
+
function reportFailure(input) {
|
|
18243
|
+
const failure = asObject3(input["failure"]);
|
|
18244
|
+
const steps = stringArray(failure["steps"] ?? input["steps"], 50);
|
|
18245
|
+
const normalized = {
|
|
18246
|
+
message: truncate3(asString3(failure["message"]) || asString3(input["error"]) || asString3(input["message"]), 1000),
|
|
18247
|
+
expected: truncate3(asString3(failure["expected"]), 1000),
|
|
18248
|
+
actual: truncate3(asString3(failure["actual"]), 1000),
|
|
18249
|
+
stack: truncate3(asString3(failure["stack"]) || asString3(input["stack"]), 3000),
|
|
18250
|
+
reasoning: truncate3(asString3(failure["reasoning"]) || asString3(input["reasoning"]), 1500),
|
|
18251
|
+
steps: steps.length > 0 ? steps.map((step) => truncate3(step, 400)).filter(Boolean) : undefined
|
|
18252
|
+
};
|
|
18253
|
+
return Object.values(normalized).some(Boolean) ? normalized : undefined;
|
|
18254
|
+
}
|
|
18255
|
+
function artifactArray(value) {
|
|
18256
|
+
return objectArray(value, 12).map((item) => ({
|
|
18257
|
+
kind: asString3(item["kind"]) || undefined,
|
|
18258
|
+
label: asString3(item["label"]) || asString3(item["name"]) || undefined,
|
|
18259
|
+
path: asString3(item["path"]) || asString3(item["file_path"]) || undefined,
|
|
18260
|
+
url: asString3(item["url"]) || undefined
|
|
18261
|
+
})).filter((item) => item.kind || item.label || item.path || item.url);
|
|
18262
|
+
}
|
|
18263
|
+
function reportEvidence(input) {
|
|
18264
|
+
const evidence = asObject3(input["evidence"]);
|
|
18265
|
+
const logs = stringArray(evidence["logs"], 8).map((log) => truncate3(log, 1000)).filter(Boolean);
|
|
18266
|
+
const screenshots = artifactArray(evidence["screenshots"] ?? input["screenshots"]);
|
|
18267
|
+
const artifacts = artifactArray(evidence["artifacts"] ?? input["artifacts"]);
|
|
18268
|
+
const normalized = {
|
|
18269
|
+
logs: logs.length > 0 ? logs : undefined,
|
|
18270
|
+
screenshots: screenshots.length > 0 ? screenshots : undefined,
|
|
18271
|
+
artifacts: artifacts.length > 0 ? artifacts : undefined
|
|
18272
|
+
};
|
|
18273
|
+
return Object.values(normalized).some(Boolean) ? normalized : undefined;
|
|
18274
|
+
}
|
|
18275
|
+
function normalizeTesterIssueReport(value, fallbackPriority = "medium") {
|
|
18276
|
+
const input = asObject3(value);
|
|
18277
|
+
if (input["schema_version"] !== TESTERS_ISSUE_REPORT_SCHEMA_VERSION) {
|
|
18278
|
+
throw new Error(`Expected schema_version ${TESTERS_ISSUE_REPORT_SCHEMA_VERSION}`);
|
|
18279
|
+
}
|
|
18280
|
+
const failure = reportFailure(input);
|
|
18281
|
+
const source6 = reportSource(input);
|
|
18282
|
+
const target = reportTarget(input);
|
|
18283
|
+
const title = truncate3(asString3(input["title"]) || asString3(input["summary"]) || failure?.message || source6?.scenario_name || "Tester issue report", 220);
|
|
18284
|
+
if (!title)
|
|
18285
|
+
throw new Error("Tester issue report requires a title");
|
|
18286
|
+
const labels = [
|
|
18287
|
+
...stringArray(input["labels"], 20),
|
|
18288
|
+
...stringArray(input["tags"], 20)
|
|
18289
|
+
].map(cleanKey).filter(Boolean);
|
|
18290
|
+
const report = {
|
|
18291
|
+
schema_version: TESTERS_ISSUE_REPORT_SCHEMA_VERSION,
|
|
18292
|
+
id: asString3(input["id"]) || undefined,
|
|
18293
|
+
fingerprint: asString3(input["fingerprint"]) || undefined,
|
|
18294
|
+
title,
|
|
18295
|
+
summary: truncate3(asString3(input["summary"]), 1000) ?? null,
|
|
18296
|
+
kind: normalizeKind(input["kind"] ?? input["type"]),
|
|
18297
|
+
severity: normalizeSeverity(input["severity"] ?? input["priority"], fallbackPriority),
|
|
18298
|
+
source: source6,
|
|
18299
|
+
target,
|
|
18300
|
+
failure,
|
|
18301
|
+
evidence: reportEvidence(input),
|
|
18302
|
+
labels: labels.length > 0 ? [...new Set(labels)].slice(0, 20) : undefined,
|
|
18303
|
+
metadata: redactValue(asObject3(input["metadata"])),
|
|
18304
|
+
occurred_at: asString3(input["occurred_at"]) || asString3(input["timestamp"]) || undefined
|
|
18305
|
+
};
|
|
18306
|
+
return redactValue(report);
|
|
18307
|
+
}
|
|
18308
|
+
function fingerprintTesterIssueReport(report) {
|
|
18309
|
+
if (report.fingerprint)
|
|
18310
|
+
return `testers:${cleanKey(report.fingerprint)}`;
|
|
18311
|
+
const stackTop = report.failure?.stack?.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || "";
|
|
18312
|
+
const url = normalizeUrlPattern(report.target?.url || report.source?.page_url || report.source?.url || "");
|
|
18313
|
+
const raw = [
|
|
18314
|
+
report.kind || "unknown",
|
|
18315
|
+
report.source?.project_id || "",
|
|
18316
|
+
report.source?.scenario_id || report.source?.scenario_name || "",
|
|
18317
|
+
url,
|
|
18318
|
+
report.target?.route || "",
|
|
18319
|
+
report.target?.selector || report.target?.component || "",
|
|
18320
|
+
normalizeText2(report.failure?.message || report.summary || report.title).slice(0, 240),
|
|
18321
|
+
normalizeText2(stackTop).slice(0, 160)
|
|
18322
|
+
].join("::");
|
|
18323
|
+
return `testers:${createHash7("sha256").update(raw).digest("hex").slice(0, 16)}`;
|
|
18324
|
+
}
|
|
18325
|
+
function priorityForSeverity(severity, fallback) {
|
|
18326
|
+
return PRIORITIES3.includes(severity) ? severity : fallback;
|
|
18327
|
+
}
|
|
18328
|
+
function maxPriority(left, right) {
|
|
18329
|
+
return PRIORITIES3.indexOf(right) > PRIORITIES3.indexOf(left) ? right : left;
|
|
18330
|
+
}
|
|
18331
|
+
function taskTitle2(report) {
|
|
18332
|
+
const title = report.title.replace(/^BUG:\s*/i, "").replace(/^\[testers\]\s*/i, "");
|
|
18333
|
+
return `BUG: [testers] ${title}`.slice(0, 240);
|
|
18334
|
+
}
|
|
18335
|
+
function evidenceLines(report) {
|
|
18336
|
+
const lines = [];
|
|
18337
|
+
for (const item of report.evidence?.screenshots || []) {
|
|
18338
|
+
lines.push(`Screenshot: ${item.label || item.kind || item.path || item.url}${item.path ? ` (${item.path})` : item.url ? ` (${item.url})` : ""}`);
|
|
18339
|
+
}
|
|
18340
|
+
for (const item of report.evidence?.artifacts || []) {
|
|
18341
|
+
lines.push(`Artifact: ${item.label || item.kind || item.path || item.url}${item.path ? ` (${item.path})` : item.url ? ` (${item.url})` : ""}`);
|
|
18342
|
+
}
|
|
18343
|
+
for (const log of report.evidence?.logs || [])
|
|
18344
|
+
lines.push(`Log: ${log}`);
|
|
18345
|
+
return lines.slice(0, 12);
|
|
18346
|
+
}
|
|
18347
|
+
function taskDescription2(report, fingerprint2) {
|
|
18348
|
+
const failure = report.failure;
|
|
18349
|
+
const lines = [
|
|
18350
|
+
"Tester issue report.",
|
|
18351
|
+
"",
|
|
18352
|
+
`Schema: ${report.schema_version}`,
|
|
18353
|
+
`Fingerprint: ${fingerprint2}`,
|
|
18354
|
+
`Kind: ${report.kind || "unknown"}`,
|
|
18355
|
+
`Severity: ${report.severity || "medium"}`,
|
|
18356
|
+
report.source?.run_id ? `Run: ${report.source.run_id}` : null,
|
|
18357
|
+
report.source?.result_id ? `Result: ${report.source.result_id}` : null,
|
|
18358
|
+
report.source?.scenario_name || report.source?.scenario_id ? `Scenario: ${report.source.scenario_name || report.source.scenario_id}` : null,
|
|
18359
|
+
report.target?.url || report.source?.page_url || report.source?.url ? `URL: ${report.target?.url || report.source?.page_url || report.source?.url}` : null,
|
|
18360
|
+
report.target?.route ? `Route: ${report.target.route}` : null,
|
|
18361
|
+
report.target?.selector ? `Selector: ${report.target.selector}` : null,
|
|
18362
|
+
report.occurred_at ? `Occurred at: ${report.occurred_at}` : null,
|
|
18363
|
+
"",
|
|
18364
|
+
report.summary ? `Summary:
|
|
18365
|
+
${report.summary}` : null,
|
|
18366
|
+
failure?.message ? `Failure:
|
|
18367
|
+
${failure.message}` : null,
|
|
18368
|
+
failure?.expected ? `Expected:
|
|
18369
|
+
${failure.expected}` : null,
|
|
18370
|
+
failure?.actual ? `Actual:
|
|
18371
|
+
${failure.actual}` : null,
|
|
18372
|
+
failure?.reasoning ? `Reasoning:
|
|
18373
|
+
${failure.reasoning}` : null,
|
|
18374
|
+
failure?.steps?.length ? `Steps:
|
|
18375
|
+
${failure.steps.map((step, index) => `${index + 1}. ${step}`).join(`
|
|
18376
|
+
`)}` : null,
|
|
18377
|
+
evidenceLines(report).length ? `Evidence:
|
|
18378
|
+
${evidenceLines(report).map((line) => `- ${line}`).join(`
|
|
18379
|
+
`)}` : null,
|
|
18380
|
+
failure?.stack ? `Stack:
|
|
18381
|
+
${failure.stack}` : null
|
|
18382
|
+
].filter((line) => line !== null);
|
|
18383
|
+
return lines.join(`
|
|
18384
|
+
`).replace(/\n{3,}/g, `
|
|
18385
|
+
|
|
18386
|
+
`).slice(0, 6000);
|
|
18387
|
+
}
|
|
18388
|
+
function taskTags(report) {
|
|
18389
|
+
return [...new Set([
|
|
18390
|
+
"bug",
|
|
18391
|
+
"testers",
|
|
18392
|
+
"tester-report",
|
|
18393
|
+
report.kind ? cleanKey(String(report.kind)).replace(/_/g, "-") : "unknown",
|
|
18394
|
+
...report.labels || []
|
|
18395
|
+
].filter(Boolean))].slice(0, 16);
|
|
18396
|
+
}
|
|
18397
|
+
function storedReportSummary(report) {
|
|
18398
|
+
return {
|
|
18399
|
+
id: report.id ?? null,
|
|
18400
|
+
title: report.title,
|
|
18401
|
+
kind: report.kind ?? "unknown",
|
|
18402
|
+
severity: report.severity ?? "medium",
|
|
18403
|
+
run_id: report.source?.run_id ?? null,
|
|
18404
|
+
result_id: report.source?.result_id ?? null,
|
|
18405
|
+
scenario_id: report.source?.scenario_id ?? null,
|
|
18406
|
+
scenario_name: report.source?.scenario_name ?? null,
|
|
18407
|
+
url: report.target?.url ?? report.source?.page_url ?? report.source?.url ?? null,
|
|
18408
|
+
occurred_at: report.occurred_at ?? null
|
|
18409
|
+
};
|
|
18410
|
+
}
|
|
18411
|
+
function testerMetadata(report, fingerprint2, previous, timestamp2) {
|
|
18412
|
+
const occurrenceCount = typeof previous?.["occurrence_count"] === "number" ? previous["occurrence_count"] + 1 : 1;
|
|
18413
|
+
const previousRecent = previous?.["recent_reports"];
|
|
18414
|
+
const recent = Array.isArray(previousRecent) ? previousRecent : [];
|
|
18415
|
+
return {
|
|
18416
|
+
schema_version: TESTERS_ISSUE_REPORT_SCHEMA_VERSION,
|
|
18417
|
+
fingerprint: fingerprint2,
|
|
18418
|
+
first_seen_at: asString3(previous?.["first_seen_at"]) || timestamp2,
|
|
18419
|
+
last_seen_at: timestamp2,
|
|
18420
|
+
occurrence_count: occurrenceCount,
|
|
18421
|
+
latest_report: storedReportSummary(report),
|
|
18422
|
+
recent_reports: [...recent.slice(-4), storedReportSummary(report)]
|
|
18423
|
+
};
|
|
18424
|
+
}
|
|
18425
|
+
function sourceMetadata(report) {
|
|
18426
|
+
const url = report.target?.url || report.source?.page_url || report.source?.url || null;
|
|
18427
|
+
return {
|
|
18428
|
+
...url ? { source_url: url, external_url: url, issue_url: url } : {},
|
|
18429
|
+
...report.source?.run_id ? { tester_run_id: report.source.run_id } : {},
|
|
18430
|
+
...report.source?.result_id ? { tester_result_id: report.source.result_id } : {},
|
|
18431
|
+
...report.source?.scenario_id ? { tester_scenario_id: report.source.scenario_id } : {},
|
|
18432
|
+
...report.source?.project_id ? { tester_project_id: report.source.project_id } : {}
|
|
18433
|
+
};
|
|
18434
|
+
}
|
|
18435
|
+
function findExistingTask2(fingerprint2, input, db) {
|
|
18436
|
+
for (const task2 of listTasks({
|
|
18437
|
+
include_archived: true,
|
|
18438
|
+
project_id: input.project_id,
|
|
18439
|
+
task_list_id: input.task_list_id
|
|
18440
|
+
}, db)) {
|
|
18441
|
+
const metadata = task2.metadata || {};
|
|
18442
|
+
const tester = asObject3(metadata["tester_issue_report"]);
|
|
18443
|
+
if (tester["fingerprint"] === fingerprint2)
|
|
18444
|
+
return task2;
|
|
18445
|
+
if (metadata["tester_issue_fingerprint"] === fingerprint2)
|
|
18446
|
+
return task2;
|
|
18447
|
+
if (metadata["external_ref"] === fingerprint2)
|
|
18448
|
+
return task2;
|
|
18449
|
+
}
|
|
18450
|
+
return null;
|
|
18451
|
+
}
|
|
18452
|
+
function commandsFor(task2) {
|
|
18453
|
+
return [
|
|
18454
|
+
"todos issues report --file tester-report.json --apply --json",
|
|
18455
|
+
task2 ? `todos show ${task2.id.slice(0, 8)}` : `todos list --tags tester-report --json`,
|
|
18456
|
+
`todos dedupe scan --threshold 0.8 --json`
|
|
18457
|
+
];
|
|
18458
|
+
}
|
|
18459
|
+
function updateExistingTask(task2, report, fingerprint2, input, timestamp2, db) {
|
|
18460
|
+
if (input.update_existing === false)
|
|
18461
|
+
return { action: "matched", task: task2 };
|
|
18462
|
+
const previous = asObject3(task2.metadata["tester_issue_report"]);
|
|
18463
|
+
const severityPriority = priorityForSeverity(report.severity, input.default_priority || "medium");
|
|
18464
|
+
const nextStatus = task2.status === "completed" || task2.status === "cancelled" ? "pending" : task2.status;
|
|
18465
|
+
const action = nextStatus !== task2.status ? "regressed" : "updated";
|
|
18466
|
+
const updated = updateTask(task2.id, {
|
|
18467
|
+
version: task2.version,
|
|
18468
|
+
title: taskTitle2(report),
|
|
18469
|
+
description: taskDescription2(report, fingerprint2),
|
|
18470
|
+
priority: maxPriority(task2.priority, severityPriority),
|
|
18471
|
+
status: nextStatus,
|
|
18472
|
+
completed_at: nextStatus !== task2.status ? null : undefined,
|
|
18473
|
+
tags: [...new Set([...task2.tags, ...taskTags(report)])],
|
|
18474
|
+
metadata: {
|
|
18475
|
+
...task2.metadata,
|
|
18476
|
+
...sourceMetadata(report),
|
|
18477
|
+
external_ref: fingerprint2,
|
|
18478
|
+
tester_issue_fingerprint: fingerprint2,
|
|
18479
|
+
tester_issue_report: testerMetadata(report, fingerprint2, previous, timestamp2)
|
|
18480
|
+
},
|
|
18481
|
+
...input.assigned_to !== undefined ? { assigned_to: input.assigned_to } : {},
|
|
18482
|
+
task_type: task2.task_type || "bug"
|
|
18483
|
+
}, db);
|
|
18484
|
+
return { action, task: updated };
|
|
18485
|
+
}
|
|
18486
|
+
function upsertTesterIssueReport(input, db) {
|
|
18487
|
+
const d = db || getDatabase();
|
|
18488
|
+
const timestamp2 = now();
|
|
18489
|
+
const warnings = [];
|
|
18490
|
+
const report = normalizeTesterIssueReport(input.report, input.default_priority || "medium");
|
|
18491
|
+
const fingerprint2 = fingerprintTesterIssueReport(report);
|
|
18492
|
+
const existing = findExistingTask2(fingerprint2, input, d);
|
|
18493
|
+
if (!input.apply) {
|
|
18494
|
+
const action = existing ? "matched" : "preview";
|
|
18495
|
+
return {
|
|
18496
|
+
schema_version: TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
|
|
18497
|
+
local_only: true,
|
|
18498
|
+
dry_run: true,
|
|
18499
|
+
processed_at: timestamp2,
|
|
18500
|
+
action,
|
|
18501
|
+
fingerprint: fingerprint2,
|
|
18502
|
+
report,
|
|
18503
|
+
task: existing,
|
|
18504
|
+
warnings,
|
|
18505
|
+
commands: commandsFor(existing)
|
|
18506
|
+
};
|
|
18507
|
+
}
|
|
18508
|
+
if (existing) {
|
|
18509
|
+
const updated = updateExistingTask(existing, report, fingerprint2, input, timestamp2, d);
|
|
18510
|
+
return {
|
|
18511
|
+
schema_version: TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
|
|
18512
|
+
local_only: true,
|
|
18513
|
+
dry_run: false,
|
|
18514
|
+
processed_at: timestamp2,
|
|
18515
|
+
action: updated.action,
|
|
18516
|
+
fingerprint: fingerprint2,
|
|
18517
|
+
report,
|
|
18518
|
+
task: updated.task,
|
|
18519
|
+
warnings,
|
|
18520
|
+
commands: commandsFor(updated.task)
|
|
18521
|
+
};
|
|
18522
|
+
}
|
|
18523
|
+
const priority = priorityForSeverity(report.severity, input.default_priority || "medium");
|
|
18524
|
+
const task2 = createTask({
|
|
18525
|
+
title: taskTitle2(report),
|
|
18526
|
+
description: taskDescription2(report, fingerprint2),
|
|
18527
|
+
priority,
|
|
18528
|
+
status: "pending",
|
|
18529
|
+
tags: taskTags(report),
|
|
18530
|
+
metadata: {
|
|
18531
|
+
...sourceMetadata(report),
|
|
18532
|
+
external_ref: fingerprint2,
|
|
18533
|
+
tester_issue_fingerprint: fingerprint2,
|
|
18534
|
+
tester_issue_report: testerMetadata(report, fingerprint2, null, timestamp2),
|
|
18535
|
+
tester_issue_report_raw: redactValue({
|
|
18536
|
+
...report,
|
|
18537
|
+
evidence: report.evidence ? {
|
|
18538
|
+
screenshots: report.evidence.screenshots,
|
|
18539
|
+
artifacts: report.evidence.artifacts
|
|
18540
|
+
} : undefined
|
|
18541
|
+
})
|
|
18542
|
+
},
|
|
18543
|
+
project_id: input.project_id,
|
|
18544
|
+
task_list_id: input.task_list_id,
|
|
18545
|
+
agent_id: input.agent_id,
|
|
18546
|
+
assigned_to: input.assigned_to,
|
|
18547
|
+
task_type: "bug"
|
|
18548
|
+
}, d);
|
|
18549
|
+
return {
|
|
18550
|
+
schema_version: TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
|
|
18551
|
+
local_only: true,
|
|
18552
|
+
dry_run: false,
|
|
18553
|
+
processed_at: timestamp2,
|
|
18554
|
+
action: "created",
|
|
18555
|
+
fingerprint: fingerprint2,
|
|
18556
|
+
report,
|
|
18557
|
+
task: task2,
|
|
18558
|
+
warnings,
|
|
18559
|
+
commands: commandsFor(task2)
|
|
18560
|
+
};
|
|
18561
|
+
}
|
|
18562
|
+
function upsertTesterIssueReports(input, db) {
|
|
18563
|
+
const d = db || getDatabase();
|
|
18564
|
+
const run = () => input.reports.map((report) => upsertTesterIssueReport({ ...input, report }, d));
|
|
18565
|
+
const results = input.apply ? d.transaction(run)() : run();
|
|
18566
|
+
const summary = {
|
|
18567
|
+
total: results.length,
|
|
18568
|
+
preview: 0,
|
|
18569
|
+
matched: 0,
|
|
18570
|
+
created: 0,
|
|
18571
|
+
updated: 0,
|
|
18572
|
+
regressed: 0
|
|
18573
|
+
};
|
|
18574
|
+
for (const result of results)
|
|
18575
|
+
summary[result.action]++;
|
|
18576
|
+
return {
|
|
18577
|
+
schema_version: TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION,
|
|
18578
|
+
local_only: true,
|
|
18579
|
+
dry_run: !input.apply,
|
|
18580
|
+
processed_at: now(),
|
|
18581
|
+
results,
|
|
18582
|
+
summary
|
|
18583
|
+
};
|
|
18584
|
+
}
|
|
18585
|
+
function readTesterIssueReportsPayload(value) {
|
|
18586
|
+
if (Array.isArray(value))
|
|
18587
|
+
return value;
|
|
18588
|
+
const record = asObject3(value);
|
|
18589
|
+
if (Array.isArray(record["reports"]))
|
|
18590
|
+
return record["reports"];
|
|
18591
|
+
if (Array.isArray(record["issues"]))
|
|
18592
|
+
return record["issues"];
|
|
18593
|
+
return [value];
|
|
18594
|
+
}
|
|
17290
18595
|
// src/lib/local-notifications.ts
|
|
17291
18596
|
init_database();
|
|
17292
18597
|
|
|
@@ -17389,13 +18694,13 @@ function eventSeverity(eventType) {
|
|
|
17389
18694
|
function payloadText(payload) {
|
|
17390
18695
|
return JSON.stringify(payload).toLowerCase();
|
|
17391
18696
|
}
|
|
17392
|
-
function
|
|
18697
|
+
function asString4(value) {
|
|
17393
18698
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
17394
18699
|
}
|
|
17395
18700
|
function fieldMatches(allowed, value) {
|
|
17396
18701
|
if (!allowed || allowed.length === 0)
|
|
17397
18702
|
return true;
|
|
17398
|
-
const stringValue =
|
|
18703
|
+
const stringValue = asString4(value);
|
|
17399
18704
|
return Boolean(stringValue && allowed.includes(stringValue));
|
|
17400
18705
|
}
|
|
17401
18706
|
function containsMatches(needles, payload) {
|
|
@@ -17461,8 +18766,8 @@ function evaluateTerminalWatchRules(input, rules = listTerminalNotificationRules
|
|
|
17461
18766
|
if (isQuietTime(rule.quiet_hours, timestamp2))
|
|
17462
18767
|
skipped.push("quiet hours active");
|
|
17463
18768
|
const matched = skipped.length === 0;
|
|
17464
|
-
const title =
|
|
17465
|
-
const taskId =
|
|
18769
|
+
const title = asString4(payload["title"]) || asString4(payload["name"]) || input.type;
|
|
18770
|
+
const taskId = asString4(payload["id"]) || asString4(payload["task_id"]);
|
|
17466
18771
|
const notification = {
|
|
17467
18772
|
rule: rule.name,
|
|
17468
18773
|
event_type: input.type,
|
|
@@ -17471,8 +18776,8 @@ function evaluateTerminalWatchRules(input, rules = listTerminalNotificationRules
|
|
|
17471
18776
|
message: `${input.type}: ${title}`,
|
|
17472
18777
|
timestamp: timestamp2,
|
|
17473
18778
|
task_id: taskId,
|
|
17474
|
-
project_id:
|
|
17475
|
-
agent_id:
|
|
18779
|
+
project_id: asString4(payload["project_id"]),
|
|
18780
|
+
agent_id: asString4(payload["agent_id"]) || asString4(payload["assigned_to"]),
|
|
17476
18781
|
bell: rule.bell && severity === "critical",
|
|
17477
18782
|
payload
|
|
17478
18783
|
};
|
|
@@ -18122,12 +19427,12 @@ function summarizeTask(task2) {
|
|
|
18122
19427
|
};
|
|
18123
19428
|
}
|
|
18124
19429
|
function overdueTasks(tasks, nowIso) {
|
|
18125
|
-
const
|
|
19430
|
+
const now3 = Date.parse(nowIso);
|
|
18126
19431
|
return tasks.filter((task2) => {
|
|
18127
19432
|
if (isTerminal(task2) || !task2.due_at)
|
|
18128
19433
|
return false;
|
|
18129
19434
|
const due = Date.parse(task2.due_at);
|
|
18130
|
-
return Number.isFinite(due) && due <
|
|
19435
|
+
return Number.isFinite(due) && due < now3;
|
|
18131
19436
|
});
|
|
18132
19437
|
}
|
|
18133
19438
|
function isReady(task2, db) {
|
|
@@ -18460,7 +19765,7 @@ function renderLocalReportMarkdown(report) {
|
|
|
18460
19765
|
// src/lib/local-encryption.ts
|
|
18461
19766
|
init_config();
|
|
18462
19767
|
init_redaction();
|
|
18463
|
-
import { createCipheriv, createDecipheriv, createHash as
|
|
19768
|
+
import { createCipheriv, createDecipheriv, createHash as createHash8, randomBytes, scryptSync, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
18464
19769
|
var TODOS_ENCRYPTED_VALUE_KIND = "hasna.todos.encrypted-value";
|
|
18465
19770
|
var TODOS_ENCRYPTED_BRIDGE_KIND = "hasna.todos.encrypted-bridge";
|
|
18466
19771
|
var TODOS_ENCRYPTION_SCHEMA_VERSION = 1;
|
|
@@ -18482,11 +19787,11 @@ class EncryptedPayloadError extends Error {
|
|
|
18482
19787
|
super(message);
|
|
18483
19788
|
}
|
|
18484
19789
|
}
|
|
18485
|
-
function
|
|
19790
|
+
function now3() {
|
|
18486
19791
|
return new Date().toISOString();
|
|
18487
19792
|
}
|
|
18488
19793
|
function sha2564(value) {
|
|
18489
|
-
return
|
|
19794
|
+
return createHash8("sha256").update(value).digest("hex");
|
|
18490
19795
|
}
|
|
18491
19796
|
function normalizeProfileName(value) {
|
|
18492
19797
|
const name = (value || DEFAULT_ENCRYPTION_PROFILE).trim();
|
|
@@ -18519,7 +19824,7 @@ function upsertEncryptionProfile(input) {
|
|
|
18519
19824
|
const name = normalizeProfileName(input.name);
|
|
18520
19825
|
const config = loadConfig();
|
|
18521
19826
|
const existing = config.encryption_profiles?.[name];
|
|
18522
|
-
const timestamp2 =
|
|
19827
|
+
const timestamp2 = now3();
|
|
18523
19828
|
const profile = {
|
|
18524
19829
|
name,
|
|
18525
19830
|
algorithm: "aes-256-gcm",
|
|
@@ -18576,7 +19881,7 @@ function encryptString(plaintext, options = {}) {
|
|
|
18576
19881
|
return {
|
|
18577
19882
|
schemaVersion: TODOS_ENCRYPTION_SCHEMA_VERSION,
|
|
18578
19883
|
kind: TODOS_ENCRYPTED_VALUE_KIND,
|
|
18579
|
-
encryptedAt: options.encryptedAt ??
|
|
19884
|
+
encryptedAt: options.encryptedAt ?? now3(),
|
|
18580
19885
|
profile: profile.name,
|
|
18581
19886
|
key_env: profile.key_env,
|
|
18582
19887
|
algorithm: "aes-256-gcm",
|
|
@@ -18611,7 +19916,7 @@ function decryptString(envelope, env = process.env) {
|
|
|
18611
19916
|
]).toString("utf8");
|
|
18612
19917
|
const expected = Buffer.from(envelope.plaintext_sha256, "hex");
|
|
18613
19918
|
const actual = Buffer.from(sha2564(plaintext), "hex");
|
|
18614
|
-
if (expected.length !== actual.length || !
|
|
19919
|
+
if (expected.length !== actual.length || !timingSafeEqual2(expected, actual)) {
|
|
18615
19920
|
throw new EncryptedPayloadError("decrypted payload checksum mismatch");
|
|
18616
19921
|
}
|
|
18617
19922
|
return plaintext;
|
|
@@ -20053,87 +21358,6 @@ function getCapableAgents(capabilities, opts, db) {
|
|
|
20053
21358
|
return opts?.limit ? scored.slice(0, opts.limit) : scored;
|
|
20054
21359
|
}
|
|
20055
21360
|
|
|
20056
|
-
// src/db/task-lists.ts
|
|
20057
|
-
init_types();
|
|
20058
|
-
init_database();
|
|
20059
|
-
function rowToTaskList2(row) {
|
|
20060
|
-
return {
|
|
20061
|
-
...row,
|
|
20062
|
-
metadata: JSON.parse(row.metadata || "{}")
|
|
20063
|
-
};
|
|
20064
|
-
}
|
|
20065
|
-
function createTaskList(input, db) {
|
|
20066
|
-
const d = db || getDatabase();
|
|
20067
|
-
const id = uuid();
|
|
20068
|
-
const timestamp2 = now();
|
|
20069
|
-
const slug = input.slug || slugify(input.name);
|
|
20070
|
-
if (!input.project_id) {
|
|
20071
|
-
const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
|
|
20072
|
-
if (existing) {
|
|
20073
|
-
throw new Error(`Standalone task list with slug "${slug}" already exists`);
|
|
20074
|
-
}
|
|
20075
|
-
}
|
|
20076
|
-
d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
|
|
20077
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp2, timestamp2]);
|
|
20078
|
-
return getTaskList(id, d);
|
|
20079
|
-
}
|
|
20080
|
-
function getTaskList(id, db) {
|
|
20081
|
-
const d = db || getDatabase();
|
|
20082
|
-
const row = d.query("SELECT * FROM task_lists WHERE id = ?").get(id);
|
|
20083
|
-
return row ? rowToTaskList2(row) : null;
|
|
20084
|
-
}
|
|
20085
|
-
function getTaskListBySlug(slug, projectId, db) {
|
|
20086
|
-
const d = db || getDatabase();
|
|
20087
|
-
let row;
|
|
20088
|
-
if (projectId) {
|
|
20089
|
-
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id = ?").get(slug, projectId);
|
|
20090
|
-
} else {
|
|
20091
|
-
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id IS NULL").get(slug);
|
|
20092
|
-
}
|
|
20093
|
-
return row ? rowToTaskList2(row) : null;
|
|
20094
|
-
}
|
|
20095
|
-
function listTaskLists(projectId, db) {
|
|
20096
|
-
const d = db || getDatabase();
|
|
20097
|
-
if (projectId) {
|
|
20098
|
-
return d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(projectId).map(rowToTaskList2);
|
|
20099
|
-
}
|
|
20100
|
-
return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList2);
|
|
20101
|
-
}
|
|
20102
|
-
function updateTaskList(id, input, db) {
|
|
20103
|
-
const d = db || getDatabase();
|
|
20104
|
-
const existing = getTaskList(id, d);
|
|
20105
|
-
if (!existing)
|
|
20106
|
-
throw new TaskListNotFoundError(id);
|
|
20107
|
-
const sets = ["updated_at = ?"];
|
|
20108
|
-
const params = [now()];
|
|
20109
|
-
if (input.name !== undefined) {
|
|
20110
|
-
sets.push("name = ?");
|
|
20111
|
-
params.push(input.name);
|
|
20112
|
-
}
|
|
20113
|
-
if (input.description !== undefined) {
|
|
20114
|
-
sets.push("description = ?");
|
|
20115
|
-
params.push(input.description);
|
|
20116
|
-
}
|
|
20117
|
-
if (input.metadata !== undefined) {
|
|
20118
|
-
sets.push("metadata = ?");
|
|
20119
|
-
params.push(JSON.stringify(input.metadata));
|
|
20120
|
-
}
|
|
20121
|
-
params.push(id);
|
|
20122
|
-
d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
20123
|
-
return getTaskList(id, d);
|
|
20124
|
-
}
|
|
20125
|
-
function deleteTaskList(id, db) {
|
|
20126
|
-
const d = db || getDatabase();
|
|
20127
|
-
return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
20128
|
-
}
|
|
20129
|
-
function ensureTaskList(name, slug, projectId, db) {
|
|
20130
|
-
const d = db || getDatabase();
|
|
20131
|
-
const existing = getTaskListBySlug(slug, projectId, d);
|
|
20132
|
-
if (existing)
|
|
20133
|
-
return existing;
|
|
20134
|
-
return createTaskList({ name, slug, project_id: projectId }, d);
|
|
20135
|
-
}
|
|
20136
|
-
|
|
20137
21361
|
// src/storage/local-sqlite.ts
|
|
20138
21362
|
init_database();
|
|
20139
21363
|
|
|
@@ -20702,7 +21926,7 @@ function createHybridTodosStorageAdapter(options) {
|
|
|
20702
21926
|
}
|
|
20703
21927
|
|
|
20704
21928
|
// src/storage/postgres-adapter.ts
|
|
20705
|
-
import { randomUUID as
|
|
21929
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
20706
21930
|
function createPostgresTodosStorageAdapter(options) {
|
|
20707
21931
|
const store = new PostgresJsonRecordStore(options);
|
|
20708
21932
|
const adapter = {
|
|
@@ -20885,7 +22109,7 @@ async function createTask2(input, store, context) {
|
|
|
20885
22109
|
const timestamp2 = new Date().toISOString();
|
|
20886
22110
|
const shortId = input.project_id ? await nextTaskShortId2(input.project_id, store, context) : null;
|
|
20887
22111
|
const task2 = {
|
|
20888
|
-
id:
|
|
22112
|
+
id: randomUUID3(),
|
|
20889
22113
|
short_id: shortId,
|
|
20890
22114
|
project_id: input.project_id ?? context?.projectId ?? null,
|
|
20891
22115
|
parent_id: input.parent_id ?? null,
|
|
@@ -21052,7 +22276,7 @@ async function getChangedSince(since, filters, store) {
|
|
|
21052
22276
|
async function createProject2(input, store, context) {
|
|
21053
22277
|
const timestamp2 = new Date().toISOString();
|
|
21054
22278
|
const project = {
|
|
21055
|
-
id:
|
|
22279
|
+
id: randomUUID3(),
|
|
21056
22280
|
name: input.name,
|
|
21057
22281
|
path: input.path,
|
|
21058
22282
|
description: input.description ?? null,
|
|
@@ -21072,7 +22296,7 @@ async function updateProject2(id, input, store) {
|
|
|
21072
22296
|
async function createPlan2(input, store, context) {
|
|
21073
22297
|
const timestamp2 = new Date().toISOString();
|
|
21074
22298
|
return store.upsert("plans", {
|
|
21075
|
-
id:
|
|
22299
|
+
id: randomUUID3(),
|
|
21076
22300
|
project_id: input.project_id ?? context?.projectId ?? null,
|
|
21077
22301
|
task_list_id: input.task_list_id ?? context?.taskListId ?? null,
|
|
21078
22302
|
agent_id: input.agent_id ?? context?.agentId ?? null,
|
|
@@ -21094,7 +22318,7 @@ async function registerAgent2(input, store, context) {
|
|
|
21094
22318
|
}
|
|
21095
22319
|
const timestamp2 = new Date().toISOString();
|
|
21096
22320
|
const agent = {
|
|
21097
|
-
id: existing?.id ??
|
|
22321
|
+
id: existing?.id ?? randomUUID3().slice(0, 8),
|
|
21098
22322
|
name: input.name,
|
|
21099
22323
|
description: input.description ?? existing?.description ?? null,
|
|
21100
22324
|
role: input.role ?? existing?.role ?? null,
|
|
@@ -21130,7 +22354,7 @@ async function updateAgent2(id, input, store) {
|
|
|
21130
22354
|
async function createTaskList2(input, store, context) {
|
|
21131
22355
|
const timestamp2 = new Date().toISOString();
|
|
21132
22356
|
return store.upsert("task_lists", {
|
|
21133
|
-
id:
|
|
22357
|
+
id: randomUUID3(),
|
|
21134
22358
|
project_id: input.project_id ?? context?.projectId ?? null,
|
|
21135
22359
|
slug: input.slug ?? slugify2(input.name),
|
|
21136
22360
|
name: input.name,
|
|
@@ -21152,7 +22376,7 @@ async function updateTaskList2(id, input, store) {
|
|
|
21152
22376
|
async function createTemplate2(input, store, context) {
|
|
21153
22377
|
const timestamp2 = new Date().toISOString();
|
|
21154
22378
|
return store.upsert("templates", {
|
|
21155
|
-
id:
|
|
22379
|
+
id: randomUUID3(),
|
|
21156
22380
|
name: input.name,
|
|
21157
22381
|
title_pattern: input.title_pattern,
|
|
21158
22382
|
description: input.description ?? null,
|
|
@@ -21181,7 +22405,7 @@ async function updateTemplate2(id, input, store) {
|
|
|
21181
22405
|
}
|
|
21182
22406
|
async function logTaskChange2(taskId, action, field2, oldValue, newValue, agentId, store, context) {
|
|
21183
22407
|
const entry2 = {
|
|
21184
|
-
id:
|
|
22408
|
+
id: randomUUID3(),
|
|
21185
22409
|
task_id: taskId,
|
|
21186
22410
|
action,
|
|
21187
22411
|
field: field2 ?? null,
|
|
@@ -21194,7 +22418,7 @@ async function logTaskChange2(taskId, action, field2, oldValue, newValue, agentI
|
|
|
21194
22418
|
}
|
|
21195
22419
|
async function addComment2(input, store, context) {
|
|
21196
22420
|
const comment = {
|
|
21197
|
-
id:
|
|
22421
|
+
id: randomUUID3(),
|
|
21198
22422
|
task_id: input.task_id,
|
|
21199
22423
|
agent_id: input.agent_id ?? context?.agentId ?? null,
|
|
21200
22424
|
session_id: input.session_id ?? context?.sessionId ?? null,
|
|
@@ -21369,10 +22593,10 @@ function assertRemoteAdapterCapabilities(adapter, mode) {
|
|
|
21369
22593
|
}
|
|
21370
22594
|
}
|
|
21371
22595
|
// src/storage/s3-artifacts.ts
|
|
21372
|
-
import { createHash as
|
|
22596
|
+
import { createHash as createHash9, createHmac as createHmac2 } from "crypto";
|
|
21373
22597
|
function createTodosS3ArtifactStore(options) {
|
|
21374
22598
|
const requestFetch = options.fetch ?? fetch;
|
|
21375
|
-
const
|
|
22599
|
+
const now4 = options.now ?? (() => new Date);
|
|
21376
22600
|
return {
|
|
21377
22601
|
objectKey: (relativePath) => buildS3ObjectKey(options.config, relativePath),
|
|
21378
22602
|
objectUrl: (relativePath) => buildS3ObjectUrl(options.config, buildS3ObjectKey(options.config, relativePath)),
|
|
@@ -21394,7 +22618,7 @@ function createTodosS3ArtifactStore(options) {
|
|
|
21394
22618
|
headers,
|
|
21395
22619
|
body,
|
|
21396
22620
|
credentials: options.credentials,
|
|
21397
|
-
now:
|
|
22621
|
+
now: now4()
|
|
21398
22622
|
});
|
|
21399
22623
|
const response = await requestFetch(url, { method: "PUT", headers: signed.headers, body });
|
|
21400
22624
|
if (!response.ok)
|
|
@@ -21415,7 +22639,7 @@ function createTodosS3ArtifactStore(options) {
|
|
|
21415
22639
|
service: "s3",
|
|
21416
22640
|
headers: {},
|
|
21417
22641
|
credentials: options.credentials,
|
|
21418
|
-
now:
|
|
22642
|
+
now: now4()
|
|
21419
22643
|
});
|
|
21420
22644
|
const response = await requestFetch(url, { method: "GET", headers: signed.headers });
|
|
21421
22645
|
if (!response.ok)
|
|
@@ -21431,7 +22655,7 @@ function createTodosS3ArtifactStore(options) {
|
|
|
21431
22655
|
service: "s3",
|
|
21432
22656
|
headers: {},
|
|
21433
22657
|
credentials: options.credentials,
|
|
21434
|
-
now:
|
|
22658
|
+
now: now4()
|
|
21435
22659
|
});
|
|
21436
22660
|
const response = await requestFetch(url, { method: "DELETE", headers: signed.headers });
|
|
21437
22661
|
if (!response.ok && response.status !== 404)
|
|
@@ -21541,13 +22765,13 @@ function toAmzDate(date) {
|
|
|
21541
22765
|
return date.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
21542
22766
|
}
|
|
21543
22767
|
function sha256Hex(value) {
|
|
21544
|
-
return
|
|
22768
|
+
return createHash9("sha256").update(value).digest("hex");
|
|
21545
22769
|
}
|
|
21546
22770
|
function hmac(key, value) {
|
|
21547
|
-
return
|
|
22771
|
+
return createHmac2("sha256", key).update(value).digest();
|
|
21548
22772
|
}
|
|
21549
22773
|
function hmacHex(key, value) {
|
|
21550
|
-
return
|
|
22774
|
+
return createHmac2("sha256", key).update(value).digest("hex");
|
|
21551
22775
|
}
|
|
21552
22776
|
function getSigningKey(secretAccessKey, dateStamp, region, service) {
|
|
21553
22777
|
const dateKey = hmac(`AWS4${secretAccessKey}`, dateStamp);
|
|
@@ -21559,7 +22783,7 @@ function getSigningKey(secretAccessKey, dateStamp, region, service) {
|
|
|
21559
22783
|
init_database();
|
|
21560
22784
|
async function uploadRunArtifactsToS3(options) {
|
|
21561
22785
|
const db = options.db ?? getDatabase();
|
|
21562
|
-
const
|
|
22786
|
+
const now4 = options.now ?? (() => new Date);
|
|
21563
22787
|
const result = emptyResult();
|
|
21564
22788
|
for (const artifact of listRunArtifacts(db, options.filter)) {
|
|
21565
22789
|
try {
|
|
@@ -21598,7 +22822,7 @@ async function uploadRunArtifactsToS3(options) {
|
|
|
21598
22822
|
url: ref.url,
|
|
21599
22823
|
sha256: content.sha256,
|
|
21600
22824
|
size_bytes: content.size_bytes,
|
|
21601
|
-
uploaded_at:
|
|
22825
|
+
uploaded_at: now4().toISOString()
|
|
21602
22826
|
};
|
|
21603
22827
|
updateArtifactMetadata(db, artifact.id, {
|
|
21604
22828
|
...metadata,
|
|
@@ -21673,7 +22897,7 @@ function planRunArtifactsS3Sync(options) {
|
|
|
21673
22897
|
}
|
|
21674
22898
|
async function downloadRunArtifactsFromS3(options) {
|
|
21675
22899
|
const db = options.db ?? getDatabase();
|
|
21676
|
-
const
|
|
22900
|
+
const now4 = options.now ?? (() => new Date);
|
|
21677
22901
|
const result = emptyResult();
|
|
21678
22902
|
for (const artifact of listRunArtifacts(db, options.filter)) {
|
|
21679
22903
|
try {
|
|
@@ -21709,7 +22933,7 @@ async function downloadRunArtifactsFromS3(options) {
|
|
|
21709
22933
|
...metadata,
|
|
21710
22934
|
remote_artifact_store: {
|
|
21711
22935
|
...remote,
|
|
21712
|
-
downloaded_at:
|
|
22936
|
+
downloaded_at: now4().toISOString()
|
|
21713
22937
|
}
|
|
21714
22938
|
});
|
|
21715
22939
|
result.downloaded += 1;
|
|
@@ -22728,7 +23952,7 @@ function parseObject2(value) {
|
|
|
22728
23952
|
function normalizeTags(tags) {
|
|
22729
23953
|
return [...new Set((tags || []).map((tag) => tag.trim()).filter(Boolean))].sort((left, right) => left.localeCompare(right));
|
|
22730
23954
|
}
|
|
22731
|
-
function
|
|
23955
|
+
function normalizeText3(value) {
|
|
22732
23956
|
if (value === undefined)
|
|
22733
23957
|
return null;
|
|
22734
23958
|
const trimmed = value.trim();
|
|
@@ -22783,9 +24007,9 @@ function createKnowledgeRecord(input, db) {
|
|
|
22783
24007
|
id,
|
|
22784
24008
|
input.record_type,
|
|
22785
24009
|
title,
|
|
22786
|
-
|
|
22787
|
-
|
|
22788
|
-
|
|
24010
|
+
normalizeText3(input.content),
|
|
24011
|
+
normalizeText3(input.decision),
|
|
24012
|
+
normalizeText3(input.rationale),
|
|
22789
24013
|
JSON.stringify(input.alternatives || []),
|
|
22790
24014
|
resolveKnownId("tasks", input.task_id, d),
|
|
22791
24015
|
resolveKnownId("projects", input.project_id, d),
|
|
@@ -22980,7 +24204,7 @@ function parseObject3(value) {
|
|
|
22980
24204
|
function normalizeTags2(tags) {
|
|
22981
24205
|
return [...new Set((tags || []).flatMap((tag) => tag.split(",")).map((tag) => tag.trim()).filter(Boolean))].sort((left, right) => left.localeCompare(right));
|
|
22982
24206
|
}
|
|
22983
|
-
function
|
|
24207
|
+
function normalizeText4(value) {
|
|
22984
24208
|
if (value === null || value === undefined)
|
|
22985
24209
|
return null;
|
|
22986
24210
|
const trimmed = value.trim();
|
|
@@ -23049,13 +24273,13 @@ function createRisk(input, db) {
|
|
|
23049
24273
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
23050
24274
|
id,
|
|
23051
24275
|
title,
|
|
23052
|
-
|
|
24276
|
+
normalizeText4(input.description),
|
|
23053
24277
|
status,
|
|
23054
24278
|
severity,
|
|
23055
24279
|
probability,
|
|
23056
|
-
|
|
23057
|
-
|
|
23058
|
-
|
|
24280
|
+
normalizeText4(input.owner),
|
|
24281
|
+
normalizeText4(input.mitigation),
|
|
24282
|
+
normalizeText4(input.due_at),
|
|
23059
24283
|
resolveKnownId2("projects", input.project_id, d),
|
|
23060
24284
|
resolveKnownId2("plans", input.plan_id, d),
|
|
23061
24285
|
resolveKnownId2("tasks", input.task_id, d),
|
|
@@ -23090,7 +24314,7 @@ function updateRisk(id, input, db) {
|
|
|
23090
24314
|
}
|
|
23091
24315
|
if (input.description !== undefined) {
|
|
23092
24316
|
sets.push("description = ?");
|
|
23093
|
-
params.push(
|
|
24317
|
+
params.push(normalizeText4(input.description));
|
|
23094
24318
|
}
|
|
23095
24319
|
if (input.status !== undefined) {
|
|
23096
24320
|
assertStatus(input.status);
|
|
@@ -23111,15 +24335,15 @@ function updateRisk(id, input, db) {
|
|
|
23111
24335
|
}
|
|
23112
24336
|
if (input.owner !== undefined) {
|
|
23113
24337
|
sets.push("owner = ?");
|
|
23114
|
-
params.push(
|
|
24338
|
+
params.push(normalizeText4(input.owner));
|
|
23115
24339
|
}
|
|
23116
24340
|
if (input.mitigation !== undefined) {
|
|
23117
24341
|
sets.push("mitigation = ?");
|
|
23118
|
-
params.push(
|
|
24342
|
+
params.push(normalizeText4(input.mitigation));
|
|
23119
24343
|
}
|
|
23120
24344
|
if (input.due_at !== undefined) {
|
|
23121
24345
|
sets.push("due_at = ?");
|
|
23122
|
-
params.push(
|
|
24346
|
+
params.push(normalizeText4(input.due_at));
|
|
23123
24347
|
}
|
|
23124
24348
|
if (input.project_id !== undefined) {
|
|
23125
24349
|
sets.push("project_id = ?");
|
|
@@ -23640,7 +24864,7 @@ function renderRetrospectiveMarkdown(record) {
|
|
|
23640
24864
|
}
|
|
23641
24865
|
// src/lib/project-bootstrap.ts
|
|
23642
24866
|
init_database();
|
|
23643
|
-
import { existsSync as
|
|
24867
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7, statSync as statSync3 } from "fs";
|
|
23644
24868
|
import { basename as basename2, dirname as dirname7, resolve as resolve9 } from "path";
|
|
23645
24869
|
function safeStat(path) {
|
|
23646
24870
|
try {
|
|
@@ -23659,7 +24883,7 @@ function canonicalPath(input) {
|
|
|
23659
24883
|
function findUp(start, marker) {
|
|
23660
24884
|
let current = canonicalPath(start);
|
|
23661
24885
|
while (true) {
|
|
23662
|
-
if (
|
|
24886
|
+
if (existsSync9(resolve9(current, marker)))
|
|
23663
24887
|
return current;
|
|
23664
24888
|
const parent = dirname7(current);
|
|
23665
24889
|
if (parent === current)
|
|
@@ -23671,7 +24895,7 @@ function readPackageJson2(path) {
|
|
|
23671
24895
|
if (!path)
|
|
23672
24896
|
return null;
|
|
23673
24897
|
const file = resolve9(path, "package.json");
|
|
23674
|
-
if (!
|
|
24898
|
+
if (!existsSync9(file))
|
|
23675
24899
|
return null;
|
|
23676
24900
|
try {
|
|
23677
24901
|
const parsed = JSON.parse(readFileSync7(file, "utf-8"));
|
|
@@ -23693,7 +24917,7 @@ function workspaceMarker(root, rootPackage) {
|
|
|
23693
24917
|
if (rootPackage?.workspaces)
|
|
23694
24918
|
markers.push("package.json#workspaces");
|
|
23695
24919
|
for (const marker of ["pnpm-workspace.yaml", "turbo.json", "nx.json", "lerna.json", "rush.json", "bun.lock", "bun.lockb"]) {
|
|
23696
|
-
if (
|
|
24920
|
+
if (existsSync9(resolve9(root, marker)))
|
|
23697
24921
|
markers.push(marker);
|
|
23698
24922
|
}
|
|
23699
24923
|
const kind = markers.find((marker) => marker !== "bun.lock" && marker !== "bun.lockb") ?? null;
|
|
@@ -23801,7 +25025,7 @@ function getProjectByPathForBootstrap(path, db) {
|
|
|
23801
25025
|
}
|
|
23802
25026
|
// src/db/api-keys.ts
|
|
23803
25027
|
init_database();
|
|
23804
|
-
import { createHash as
|
|
25028
|
+
import { createHash as createHash10, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
23805
25029
|
function rowToRecord(row) {
|
|
23806
25030
|
return {
|
|
23807
25031
|
id: row.id,
|
|
@@ -23815,12 +25039,12 @@ function rowToRecord(row) {
|
|
|
23815
25039
|
};
|
|
23816
25040
|
}
|
|
23817
25041
|
function hashApiKey(key) {
|
|
23818
|
-
return
|
|
25042
|
+
return createHash10("sha256").update(key).digest("hex");
|
|
23819
25043
|
}
|
|
23820
25044
|
function safeEqualHex(a, b) {
|
|
23821
25045
|
if (a.length !== b.length)
|
|
23822
25046
|
return false;
|
|
23823
|
-
return
|
|
25047
|
+
return timingSafeEqual3(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
|
|
23824
25048
|
}
|
|
23825
25049
|
function generatePlaintextKey() {
|
|
23826
25050
|
return `tdos_${randomBytes2(32).toString("base64url")}`;
|
|
@@ -24006,18 +25230,18 @@ var gatherTrainingData = async (options = {}) => {
|
|
|
24006
25230
|
};
|
|
24007
25231
|
// src/lib/model-config.ts
|
|
24008
25232
|
init_sync_utils();
|
|
24009
|
-
import { existsSync as
|
|
24010
|
-
import { join as
|
|
25233
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
|
|
25234
|
+
import { join as join10 } from "path";
|
|
24011
25235
|
var DEFAULT_MODEL = "gpt-4o-mini";
|
|
24012
25236
|
function getConfigDir() {
|
|
24013
25237
|
return getTodosGlobalDir();
|
|
24014
25238
|
}
|
|
24015
25239
|
function getConfigPath2() {
|
|
24016
|
-
return
|
|
25240
|
+
return join10(getConfigDir(), "config.json");
|
|
24017
25241
|
}
|
|
24018
25242
|
function readConfig() {
|
|
24019
25243
|
const configPath = getConfigPath2();
|
|
24020
|
-
if (!
|
|
25244
|
+
if (!existsSync10(configPath))
|
|
24021
25245
|
return {};
|
|
24022
25246
|
try {
|
|
24023
25247
|
const raw = readFileSync8(configPath, "utf-8");
|
|
@@ -24028,7 +25252,7 @@ function readConfig() {
|
|
|
24028
25252
|
}
|
|
24029
25253
|
function writeConfig(config) {
|
|
24030
25254
|
const configDir = getConfigDir();
|
|
24031
|
-
if (!
|
|
25255
|
+
if (!existsSync10(configDir)) {
|
|
24032
25256
|
mkdirSync8(configDir, { recursive: true });
|
|
24033
25257
|
}
|
|
24034
25258
|
writeFileSync6(getConfigPath2(), JSON.stringify(config, null, 2) + `
|
|
@@ -24753,7 +25977,7 @@ CLI equivalent: \`${r.equivalent_cli}\`
|
|
|
24753
25977
|
`);
|
|
24754
25978
|
}
|
|
24755
25979
|
// src/lib/verification-providers.ts
|
|
24756
|
-
import { existsSync as
|
|
25980
|
+
import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
|
|
24757
25981
|
init_database();
|
|
24758
25982
|
init_config();
|
|
24759
25983
|
init_redaction();
|
|
@@ -24919,7 +26143,7 @@ Timed out after ${provider.timeout_ms}ms`);
|
|
|
24919
26143
|
};
|
|
24920
26144
|
}
|
|
24921
26145
|
function runCiLogProvider(input) {
|
|
24922
|
-
const text = input.log_text ?? (input.log_path &&
|
|
26146
|
+
const text = input.log_text ?? (input.log_path && existsSync11(input.log_path) ? readFileSync9(input.log_path, "utf-8") : "");
|
|
24923
26147
|
return {
|
|
24924
26148
|
status: classifyLog(text),
|
|
24925
26149
|
attempts: 1,
|
|
@@ -24931,7 +26155,7 @@ function runBrowserProvider(input) {
|
|
|
24931
26155
|
if (!input.artifact_path) {
|
|
24932
26156
|
return { status: "unknown", attempts: 1, exit_code: null, output_summary: "browser provider needs a screenshot or artifact path" };
|
|
24933
26157
|
}
|
|
24934
|
-
if (!
|
|
26158
|
+
if (!existsSync11(input.artifact_path)) {
|
|
24935
26159
|
return { status: "failed", attempts: 1, exit_code: null, output_summary: `artifact not found: ${input.artifact_path}` };
|
|
24936
26160
|
}
|
|
24937
26161
|
return {
|
|
@@ -25529,11 +26753,11 @@ function buildResourceSnapshot(uri, staleMs = DEFAULT_STALE_MS) {
|
|
|
25529
26753
|
content_hash: hashContent(content)
|
|
25530
26754
|
};
|
|
25531
26755
|
}
|
|
25532
|
-
function isSnapshotStale(snapshot,
|
|
26756
|
+
function isSnapshotStale(snapshot, now4 = new Date) {
|
|
25533
26757
|
const staleAt = Date.parse(snapshot.stale_after);
|
|
25534
26758
|
if (!Number.isFinite(staleAt))
|
|
25535
26759
|
return true;
|
|
25536
|
-
return
|
|
26760
|
+
return now4.getTime() >= staleAt;
|
|
25537
26761
|
}
|
|
25538
26762
|
function subscribeResource(uri, agentId) {
|
|
25539
26763
|
const key = `${uri}:${agentId || "*"}`;
|
|
@@ -25570,21 +26794,21 @@ function resourceDiagnostics() {
|
|
|
25570
26794
|
};
|
|
25571
26795
|
}
|
|
25572
26796
|
// src/lib/sandbox-profiles.ts
|
|
25573
|
-
import { existsSync as
|
|
25574
|
-
import { join as
|
|
26797
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10, writeFileSync as writeFileSync8, mkdirSync as mkdirSync10 } from "fs";
|
|
26798
|
+
import { join as join11, dirname as dirname9 } from "path";
|
|
25575
26799
|
var SANDBOX_PROFILE_VERSION = "todos.sandbox-profile.v1";
|
|
25576
26800
|
function getProfilesPath() {
|
|
25577
26801
|
if (process.env["TODOS_SANDBOX_PROFILES_PATH"]) {
|
|
25578
26802
|
return process.env["TODOS_SANDBOX_PROFILES_PATH"];
|
|
25579
26803
|
}
|
|
25580
|
-
const localDir =
|
|
25581
|
-
const local =
|
|
25582
|
-
if (
|
|
26804
|
+
const localDir = join11(process.cwd(), ".todos");
|
|
26805
|
+
const local = join11(localDir, "sandbox-profiles.json");
|
|
26806
|
+
if (existsSync12(localDir))
|
|
25583
26807
|
return local;
|
|
25584
|
-
if (
|
|
26808
|
+
if (existsSync12(local))
|
|
25585
26809
|
return local;
|
|
25586
26810
|
const home = process.env["HOME"] || "~";
|
|
25587
|
-
return
|
|
26811
|
+
return join11(home, ".hasna", "todos", "sandbox-profiles.json");
|
|
25588
26812
|
}
|
|
25589
26813
|
var cached2 = null;
|
|
25590
26814
|
function resetSandboxProfileCache() {
|
|
@@ -25616,7 +26840,7 @@ function loadSandboxProfiles() {
|
|
|
25616
26840
|
if (cached2)
|
|
25617
26841
|
return cached2;
|
|
25618
26842
|
const path = getProfilesPath();
|
|
25619
|
-
if (!
|
|
26843
|
+
if (!existsSync12(path)) {
|
|
25620
26844
|
cached2 = getDefaultSandboxProfiles();
|
|
25621
26845
|
return cached2;
|
|
25622
26846
|
}
|
|
@@ -25989,7 +27213,7 @@ function getDefaultAgentAdapters() {
|
|
|
25989
27213
|
}
|
|
25990
27214
|
function resetAgentAdapterCache() {}
|
|
25991
27215
|
// src/lib/git-traceability.ts
|
|
25992
|
-
import { existsSync as
|
|
27216
|
+
import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
|
|
25993
27217
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
25994
27218
|
import { resolve as resolve11 } from "path";
|
|
25995
27219
|
var GIT_TRACEABILITY_SCHEMA_VERSION = "todos.git_traceability.v1";
|
|
@@ -26035,7 +27259,7 @@ function inspectGitCommit(sha, cwd) {
|
|
|
26035
27259
|
}
|
|
26036
27260
|
function loadCiSnapshot(path) {
|
|
26037
27261
|
const target = path ? resolve11(path) : resolve11(process.cwd(), ".todos", "ci-snapshot.json");
|
|
26038
|
-
if (!
|
|
27262
|
+
if (!existsSync13(target))
|
|
26039
27263
|
return null;
|
|
26040
27264
|
try {
|
|
26041
27265
|
const parsed = JSON.parse(readFileSync11(target, "utf8"));
|
|
@@ -26131,8 +27355,8 @@ function formatTraceabilityReport(report) {
|
|
|
26131
27355
|
`);
|
|
26132
27356
|
}
|
|
26133
27357
|
// src/lib/mention-resolver.ts
|
|
26134
|
-
import { existsSync as
|
|
26135
|
-
import { basename as basename3, isAbsolute, join as
|
|
27358
|
+
import { existsSync as existsSync14, readdirSync as readdirSync2, readFileSync as readFileSync12, statSync as statSync4 } from "fs";
|
|
27359
|
+
import { basename as basename3, isAbsolute, join as join12, relative as relative4, resolve as resolve12, sep } from "path";
|
|
26136
27360
|
init_database();
|
|
26137
27361
|
var PREFIXES = {
|
|
26138
27362
|
file: "file",
|
|
@@ -26283,7 +27507,7 @@ function resolveFile(parsed, workspace) {
|
|
|
26283
27507
|
return resolution;
|
|
26284
27508
|
}
|
|
26285
27509
|
resolution.path = relPath;
|
|
26286
|
-
if (!
|
|
27510
|
+
if (!existsSync14(absolutePath)) {
|
|
26287
27511
|
resolution.warnings.push("file does not exist in the local workspace");
|
|
26288
27512
|
return resolution;
|
|
26289
27513
|
}
|
|
@@ -26316,7 +27540,7 @@ function walkSourceFiles(root, current = root, files = []) {
|
|
|
26316
27540
|
if (SKIP_DIRS.has(entry2.name))
|
|
26317
27541
|
continue;
|
|
26318
27542
|
}
|
|
26319
|
-
const absolutePath =
|
|
27543
|
+
const absolutePath = join12(current, entry2.name);
|
|
26320
27544
|
if (entry2.isDirectory()) {
|
|
26321
27545
|
if (!SKIP_DIRS.has(entry2.name))
|
|
26322
27546
|
walkSourceFiles(root, absolutePath, files);
|
|
@@ -28189,9 +29413,9 @@ function getAdapterDocsFingerprint() {
|
|
|
28189
29413
|
}
|
|
28190
29414
|
// src/lib/inbox-intake.ts
|
|
28191
29415
|
init_database();
|
|
28192
|
-
import { existsSync as
|
|
29416
|
+
import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
|
|
28193
29417
|
import { basename as basename4 } from "path";
|
|
28194
|
-
import { createHash as
|
|
29418
|
+
import { createHash as createHash11 } from "crypto";
|
|
28195
29419
|
init_secret_redaction();
|
|
28196
29420
|
var INBOX_INTAKE_SCHEMA = "todos.inbox_intake.v1";
|
|
28197
29421
|
var INTAKE_SOURCE_TYPES = [
|
|
@@ -28204,7 +29428,7 @@ var INTAKE_SOURCE_TYPES = [
|
|
|
28204
29428
|
];
|
|
28205
29429
|
var INTAKE_TRIAGE_STATUSES = ["preview", "triaged", "duplicate", "created"];
|
|
28206
29430
|
function fingerprint2(text) {
|
|
28207
|
-
return
|
|
29431
|
+
return createHash11("sha256").update(text).digest("hex").slice(0, 16);
|
|
28208
29432
|
}
|
|
28209
29433
|
function loadRawContent(input) {
|
|
28210
29434
|
if (input.github_url) {
|
|
@@ -28235,7 +29459,7 @@ function loadRawContent(input) {
|
|
|
28235
29459
|
}
|
|
28236
29460
|
}
|
|
28237
29461
|
if (input.file_path) {
|
|
28238
|
-
if (!
|
|
29462
|
+
if (!existsSync15(input.file_path))
|
|
28239
29463
|
throw new Error(`File not found: ${input.file_path}`);
|
|
28240
29464
|
const raw = readFileSync13(input.file_path, "utf8");
|
|
28241
29465
|
const name = basename4(input.file_path).toLowerCase();
|
|
@@ -28853,7 +30077,7 @@ function formatNlIntakePreviewText(preview) {
|
|
|
28853
30077
|
}
|
|
28854
30078
|
// src/lib/issue-importers.ts
|
|
28855
30079
|
init_database();
|
|
28856
|
-
import { existsSync as
|
|
30080
|
+
import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
|
|
28857
30081
|
var ISSUE_IMPORT_SCHEMA = "todos.issue_import.v1";
|
|
28858
30082
|
var ISSUE_SOURCES = ["github", "linear", "jira", "auto"];
|
|
28859
30083
|
var GITHUB_LABEL_PRIORITY = {
|
|
@@ -29072,7 +30296,7 @@ function parseIssueExport(data, source9 = "auto") {
|
|
|
29072
30296
|
return normalized;
|
|
29073
30297
|
}
|
|
29074
30298
|
function loadIssueExportFromFile(path) {
|
|
29075
|
-
if (!
|
|
30299
|
+
if (!existsSync16(path))
|
|
29076
30300
|
throw new Error(`File not found: ${path}`);
|
|
29077
30301
|
return JSON.parse(readFileSync14(path, "utf8"));
|
|
29078
30302
|
}
|
|
@@ -29232,8 +30456,8 @@ todos import issues ./linear.json --source linear --dry-run
|
|
|
29232
30456
|
// src/lib/run-records.ts
|
|
29233
30457
|
init_database();
|
|
29234
30458
|
init_secret_redaction();
|
|
29235
|
-
import { existsSync as
|
|
29236
|
-
import { join as
|
|
30459
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync11, writeFileSync as writeFileSync9 } from "fs";
|
|
30460
|
+
import { join as join13, dirname as dirname10 } from "path";
|
|
29237
30461
|
var RUN_RECORD_SCHEMA = "todos.run_record.v1";
|
|
29238
30462
|
var RUN_RECORD_STATUSES = ["active", "completed", "failed", "archived"];
|
|
29239
30463
|
function parseJsonArray3(raw, fallback = []) {
|
|
@@ -29426,7 +30650,7 @@ function buildRunReplayBundle(id, db) {
|
|
|
29426
30650
|
}
|
|
29427
30651
|
function exportRunReplay(id, outputPath, db) {
|
|
29428
30652
|
const bundle = buildRunReplayBundle(id, db);
|
|
29429
|
-
const path = outputPath ??
|
|
30653
|
+
const path = outputPath ?? join13(process.cwd(), ".todos", "replays", `${id.slice(0, 8)}.json`);
|
|
29430
30654
|
mkdirSync11(dirname10(path), { recursive: true });
|
|
29431
30655
|
writeFileSync9(path, JSON.stringify(bundle, null, 2));
|
|
29432
30656
|
const d = db || getDatabase();
|
|
@@ -29466,16 +30690,16 @@ function formatRunRecordMarkdown(record) {
|
|
|
29466
30690
|
`;
|
|
29467
30691
|
}
|
|
29468
30692
|
function getDefaultReplayDir() {
|
|
29469
|
-
const local =
|
|
29470
|
-
if (
|
|
30693
|
+
const local = join13(process.cwd(), ".todos", "replays");
|
|
30694
|
+
if (existsSync17(join13(process.cwd(), ".todos")))
|
|
29471
30695
|
return local;
|
|
29472
30696
|
const home = process.env["HOME"] || "~";
|
|
29473
|
-
return
|
|
30697
|
+
return join13(home, ".hasna", "todos", "replays");
|
|
29474
30698
|
}
|
|
29475
30699
|
// src/lib/release-checks.ts
|
|
29476
30700
|
init_secret_redaction();
|
|
29477
|
-
import { existsSync as
|
|
29478
|
-
import { join as
|
|
30701
|
+
import { existsSync as existsSync18, readFileSync as readFileSync15, readdirSync as readdirSync3, statSync as statSync5 } from "fs";
|
|
30702
|
+
import { join as join14, relative as relative5 } from "path";
|
|
29479
30703
|
var RELEASE_CHECK_SCHEMA = "todos.release_check.v1";
|
|
29480
30704
|
var FORBIDDEN_DIST_PATTERNS = [
|
|
29481
30705
|
{
|
|
@@ -29488,16 +30712,16 @@ var FORBIDDEN_DIST_PATTERNS = [
|
|
|
29488
30712
|
];
|
|
29489
30713
|
var REQUIRED_BINS = ["todos", "todos-mcp", "todos-serve"];
|
|
29490
30714
|
function readPackageJson3(root) {
|
|
29491
|
-
const path =
|
|
29492
|
-
if (!
|
|
30715
|
+
const path = join14(root, "package.json");
|
|
30716
|
+
if (!existsSync18(path))
|
|
29493
30717
|
throw new Error(`package.json not found in ${root}`);
|
|
29494
30718
|
return JSON.parse(readFileSync15(path, "utf8"));
|
|
29495
30719
|
}
|
|
29496
30720
|
function walkFiles(dir, acc = []) {
|
|
29497
|
-
if (!
|
|
30721
|
+
if (!existsSync18(dir))
|
|
29498
30722
|
return acc;
|
|
29499
30723
|
for (const entry2 of readdirSync3(dir)) {
|
|
29500
|
-
const full =
|
|
30724
|
+
const full = join14(dir, entry2);
|
|
29501
30725
|
const st = statSync5(full);
|
|
29502
30726
|
if (st.isDirectory())
|
|
29503
30727
|
walkFiles(full, acc);
|
|
@@ -29515,8 +30739,8 @@ function auditPackageContents(root) {
|
|
|
29515
30739
|
checks.push({ id: "files_dist", severity: "error", message: "package.json files must include dist" });
|
|
29516
30740
|
}
|
|
29517
30741
|
for (const pattern of files) {
|
|
29518
|
-
const target =
|
|
29519
|
-
if (!
|
|
30742
|
+
const target = join14(root, pattern);
|
|
30743
|
+
if (!existsSync18(target)) {
|
|
29520
30744
|
checks.push({ id: `files_missing_${pattern}`, severity: "error", message: `Published file path missing: ${pattern}` });
|
|
29521
30745
|
}
|
|
29522
30746
|
}
|
|
@@ -29530,8 +30754,8 @@ function auditPackageContents(root) {
|
|
|
29530
30754
|
checks.push({ id: `bin_${name}`, severity: "error", message: `Missing bin entry: ${name}` });
|
|
29531
30755
|
continue;
|
|
29532
30756
|
}
|
|
29533
|
-
const binPath =
|
|
29534
|
-
if (!
|
|
30757
|
+
const binPath = join14(root, rel);
|
|
30758
|
+
if (!existsSync18(binPath)) {
|
|
29535
30759
|
checks.push({ id: `bin_path_${name}`, severity: "error", message: `Bin file missing: ${rel}` });
|
|
29536
30760
|
} else {
|
|
29537
30761
|
checks.push({ id: `bin_ok_${name}`, severity: "info", message: `Bin present: ${name} \u2192 ${rel}` });
|
|
@@ -29548,8 +30772,8 @@ function auditPackageContents(root) {
|
|
|
29548
30772
|
}
|
|
29549
30773
|
function scanDistArtifacts(root) {
|
|
29550
30774
|
const checks = [];
|
|
29551
|
-
const distDir =
|
|
29552
|
-
if (!
|
|
30775
|
+
const distDir = join14(root, "dist");
|
|
30776
|
+
if (!existsSync18(distDir)) {
|
|
29553
30777
|
checks.push({ id: "dist_missing", severity: "error", message: "dist/ directory not found \u2014 run bun run build" });
|
|
29554
30778
|
return checks;
|
|
29555
30779
|
}
|
|
@@ -29855,8 +31079,8 @@ function renderReleaseNotesMarkdown(document) {
|
|
|
29855
31079
|
// src/lib/db-backup.ts
|
|
29856
31080
|
init_database();
|
|
29857
31081
|
init_migrations();
|
|
29858
|
-
import { existsSync as
|
|
29859
|
-
import { dirname as dirname11, join as
|
|
31082
|
+
import { existsSync as existsSync19, copyFileSync, mkdirSync as mkdirSync12, readFileSync as readFileSync16, statSync as statSync6, writeFileSync as writeFileSync10, unlinkSync } from "fs";
|
|
31083
|
+
import { dirname as dirname11, join as join15, resolve as resolve13 } from "path";
|
|
29860
31084
|
import { Database as Database3 } from "bun:sqlite";
|
|
29861
31085
|
var DB_BACKUP_SCHEMA = "todos.db_backup.v1";
|
|
29862
31086
|
function resolveDbPath(dbPath) {
|
|
@@ -29873,7 +31097,7 @@ function resolveDbPath(dbPath) {
|
|
|
29873
31097
|
}
|
|
29874
31098
|
function backupDatabase(outputPath, sourcePath) {
|
|
29875
31099
|
const source9 = resolveDbPath(sourcePath);
|
|
29876
|
-
if (!
|
|
31100
|
+
if (!existsSync19(source9))
|
|
29877
31101
|
throw new Error(`Database not found: ${source9}`);
|
|
29878
31102
|
mkdirSync12(dirname11(outputPath), { recursive: true });
|
|
29879
31103
|
closeDatabase();
|
|
@@ -29896,7 +31120,7 @@ function backupDatabase(outputPath, sourcePath) {
|
|
|
29896
31120
|
};
|
|
29897
31121
|
}
|
|
29898
31122
|
function restoreDatabase(backupPath, targetPath) {
|
|
29899
|
-
if (!
|
|
31123
|
+
if (!existsSync19(backupPath))
|
|
29900
31124
|
throw new Error(`Backup not found: ${backupPath}`);
|
|
29901
31125
|
const integrity = checkDatabaseIntegrity(backupPath);
|
|
29902
31126
|
if (!integrity.ok) {
|
|
@@ -29923,7 +31147,7 @@ function restoreDatabase(backupPath, targetPath) {
|
|
|
29923
31147
|
function checkDatabaseIntegrity(dbPath) {
|
|
29924
31148
|
const path = dbPath ? resolve13(dbPath) : resolveDbPath();
|
|
29925
31149
|
const errors = [];
|
|
29926
|
-
if (!
|
|
31150
|
+
if (!existsSync19(path)) {
|
|
29927
31151
|
return {
|
|
29928
31152
|
schema_version: DB_BACKUP_SCHEMA,
|
|
29929
31153
|
path,
|
|
@@ -30022,11 +31246,11 @@ function migrationDryRun(dbPath) {
|
|
|
30022
31246
|
function defaultBackupPath(dbPath) {
|
|
30023
31247
|
const base = dbPath ? dirname11(resolve13(dbPath)) : dirname11(resolveDbPath());
|
|
30024
31248
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
30025
|
-
return
|
|
31249
|
+
return join15(base, "backups", `todos-${stamp}.db`);
|
|
30026
31250
|
}
|
|
30027
31251
|
function readBackupManifest(backupPath) {
|
|
30028
31252
|
const manifestPath = `${backupPath}.json`;
|
|
30029
|
-
if (!
|
|
31253
|
+
if (!existsSync19(manifestPath))
|
|
30030
31254
|
return null;
|
|
30031
31255
|
try {
|
|
30032
31256
|
return JSON.parse(readFileSync16(manifestPath, "utf8"));
|
|
@@ -30054,7 +31278,10 @@ var SCHEMA_ENTITIES = [
|
|
|
30054
31278
|
"verification_evidence",
|
|
30055
31279
|
"handoff",
|
|
30056
31280
|
"import_export_bundle",
|
|
30057
|
-
"mcp_response"
|
|
31281
|
+
"mcp_response",
|
|
31282
|
+
"tester_issue_report",
|
|
31283
|
+
"tester_issue_report_result",
|
|
31284
|
+
"tester_issue_report_batch_result"
|
|
30058
31285
|
];
|
|
30059
31286
|
var ISO_DATE = { type: "string", description: "ISO-8601 timestamp" };
|
|
30060
31287
|
function def(entity, schemaVersion, title, required, properties) {
|
|
@@ -30184,6 +31411,81 @@ var JSON_SCHEMAS = {
|
|
|
30184
31411
|
}
|
|
30185
31412
|
},
|
|
30186
31413
|
is_error: { type: "boolean" }
|
|
31414
|
+
}),
|
|
31415
|
+
tester_issue_report: def("tester_issue_report", "testers.issue_report.v1", "TesterIssueReport", ["schema_version", "title"], {
|
|
31416
|
+
schema_version: { type: "string", enum: ["testers.issue_report.v1"] },
|
|
31417
|
+
id: { type: "string" },
|
|
31418
|
+
fingerprint: { type: "string" },
|
|
31419
|
+
title: { type: "string" },
|
|
31420
|
+
summary: { type: ["string", "null"] },
|
|
31421
|
+
kind: { type: "string" },
|
|
31422
|
+
severity: { type: "string", enum: ["low", "medium", "high", "critical"] },
|
|
31423
|
+
source: {
|
|
31424
|
+
type: "object",
|
|
31425
|
+
additionalProperties: true,
|
|
31426
|
+
properties: {
|
|
31427
|
+
tool: { type: "string" },
|
|
31428
|
+
run_id: { type: "string" },
|
|
31429
|
+
result_id: { type: "string" },
|
|
31430
|
+
scenario_id: { type: "string" },
|
|
31431
|
+
scenario_name: { type: "string" },
|
|
31432
|
+
project_id: { type: "string" },
|
|
31433
|
+
url: { type: "string" },
|
|
31434
|
+
page_url: { type: "string" },
|
|
31435
|
+
artifact_url: { type: "string" },
|
|
31436
|
+
screenshot_url: { type: "string" },
|
|
31437
|
+
commit: { type: "string" },
|
|
31438
|
+
branch: { type: "string" }
|
|
31439
|
+
}
|
|
31440
|
+
},
|
|
31441
|
+
target: {
|
|
31442
|
+
type: "object",
|
|
31443
|
+
additionalProperties: true,
|
|
31444
|
+
properties: {
|
|
31445
|
+
url: { type: "string" },
|
|
31446
|
+
route: { type: "string" },
|
|
31447
|
+
selector: { type: "string" },
|
|
31448
|
+
component: { type: "string" },
|
|
31449
|
+
browser: { type: "string" },
|
|
31450
|
+
viewport: { type: "string" }
|
|
31451
|
+
}
|
|
31452
|
+
},
|
|
31453
|
+
failure: {
|
|
31454
|
+
type: "object",
|
|
31455
|
+
additionalProperties: true,
|
|
31456
|
+
properties: {
|
|
31457
|
+
message: { type: "string" },
|
|
31458
|
+
expected: { type: "string" },
|
|
31459
|
+
actual: { type: "string" },
|
|
31460
|
+
stack: { type: "string" },
|
|
31461
|
+
reasoning: { type: "string" },
|
|
31462
|
+
steps: { type: "array", items: { type: "string" } }
|
|
31463
|
+
}
|
|
31464
|
+
},
|
|
31465
|
+
evidence: { type: "object", additionalProperties: true },
|
|
31466
|
+
labels: { type: "array", items: { type: "string" } },
|
|
31467
|
+
metadata: { type: "object", additionalProperties: true },
|
|
31468
|
+
occurred_at: ISO_DATE
|
|
31469
|
+
}),
|
|
31470
|
+
tester_issue_report_result: def("tester_issue_report_result", "todos.tester_issue_report_result.v1", "TesterIssueReportResult", ["schema_version", "local_only", "dry_run", "processed_at", "action", "fingerprint", "report", "task", "warnings", "commands"], {
|
|
31471
|
+
schema_version: { type: "string", enum: ["todos.tester_issue_report_result.v1"] },
|
|
31472
|
+
local_only: { type: "boolean" },
|
|
31473
|
+
dry_run: { type: "boolean" },
|
|
31474
|
+
processed_at: ISO_DATE,
|
|
31475
|
+
action: { type: "string", enum: ["preview", "matched", "created", "updated", "regressed"] },
|
|
31476
|
+
fingerprint: { type: "string" },
|
|
31477
|
+
report: { type: "object", additionalProperties: true },
|
|
31478
|
+
task: { type: ["object", "null"], additionalProperties: true },
|
|
31479
|
+
warnings: { type: "array", items: { type: "string" } },
|
|
31480
|
+
commands: { type: "array", items: { type: "string" } }
|
|
31481
|
+
}),
|
|
31482
|
+
tester_issue_report_batch_result: def("tester_issue_report_batch_result", "todos.tester_issue_report_batch_result.v1", "TesterIssueReportBatchResult", ["schema_version", "local_only", "dry_run", "processed_at", "results", "summary"], {
|
|
31483
|
+
schema_version: { type: "string", enum: ["todos.tester_issue_report_batch_result.v1"] },
|
|
31484
|
+
local_only: { type: "boolean" },
|
|
31485
|
+
dry_run: { type: "boolean" },
|
|
31486
|
+
processed_at: ISO_DATE,
|
|
31487
|
+
results: { type: "array", items: { type: "object", additionalProperties: true } },
|
|
31488
|
+
summary: { type: "object", additionalProperties: true }
|
|
30187
31489
|
})
|
|
30188
31490
|
};
|
|
30189
31491
|
var SCHEMA_CONTRACT_FIXTURES = {
|
|
@@ -30264,6 +31566,47 @@ var SCHEMA_CONTRACT_FIXTURES = {
|
|
|
30264
31566
|
mcp_response: {
|
|
30265
31567
|
schema_version: "todos.mcp_response.v1",
|
|
30266
31568
|
content: [{ type: "text", text: "ok" }]
|
|
31569
|
+
},
|
|
31570
|
+
tester_issue_report: {
|
|
31571
|
+
schema_version: "testers.issue_report.v1",
|
|
31572
|
+
title: "Checkout button fails",
|
|
31573
|
+
kind: "assertion_failure",
|
|
31574
|
+
severity: "high",
|
|
31575
|
+
fingerprint: "checkout-button-1234",
|
|
31576
|
+
source: {
|
|
31577
|
+
tool: "testers",
|
|
31578
|
+
run_id: "run-001",
|
|
31579
|
+
scenario_id: "scenario-001",
|
|
31580
|
+
url: "https://preview.example.com/checkout"
|
|
31581
|
+
},
|
|
31582
|
+
failure: {
|
|
31583
|
+
message: "Expected checkout button to become enabled",
|
|
31584
|
+
steps: ["Open checkout", "Fill required fields"]
|
|
31585
|
+
},
|
|
31586
|
+
labels: ["checkout", "regression"]
|
|
31587
|
+
},
|
|
31588
|
+
tester_issue_report_result: {
|
|
31589
|
+
schema_version: "todos.tester_issue_report_result.v1",
|
|
31590
|
+
local_only: true,
|
|
31591
|
+
dry_run: false,
|
|
31592
|
+
processed_at: "2026-01-01T00:00:00.000Z",
|
|
31593
|
+
action: "created",
|
|
31594
|
+
fingerprint: "testers:checkout-button-1234",
|
|
31595
|
+
report: {
|
|
31596
|
+
schema_version: "testers.issue_report.v1",
|
|
31597
|
+
title: "Checkout button fails"
|
|
31598
|
+
},
|
|
31599
|
+
task: null,
|
|
31600
|
+
warnings: [],
|
|
31601
|
+
commands: []
|
|
31602
|
+
},
|
|
31603
|
+
tester_issue_report_batch_result: {
|
|
31604
|
+
schema_version: "todos.tester_issue_report_batch_result.v1",
|
|
31605
|
+
local_only: true,
|
|
31606
|
+
dry_run: false,
|
|
31607
|
+
processed_at: "2026-01-01T00:00:00.000Z",
|
|
31608
|
+
results: [],
|
|
31609
|
+
summary: { total: 0, preview: 0, matched: 0, created: 0, updated: 0, regressed: 0 }
|
|
30267
31610
|
}
|
|
30268
31611
|
};
|
|
30269
31612
|
function typeMatches(value, expected) {
|
|
@@ -30427,15 +31770,15 @@ ${SCHEMA_ENTITIES.map((e) => `- **${e}**: \`${JSON_SCHEMAS[e].schema_version}\``
|
|
|
30427
31770
|
}
|
|
30428
31771
|
function exportSchemasToDirectory(dir) {
|
|
30429
31772
|
const { mkdirSync: mkdirSync13, writeFileSync: writeFileSync11 } = __require("fs");
|
|
30430
|
-
const { join:
|
|
31773
|
+
const { join: join16 } = __require("path");
|
|
30431
31774
|
mkdirSync13(dir, { recursive: true });
|
|
30432
31775
|
const written = [];
|
|
30433
31776
|
for (const entity of SCHEMA_ENTITIES) {
|
|
30434
|
-
const path =
|
|
31777
|
+
const path = join16(dir, `${entity}.${JSON_SCHEMAS[entity].schema_version.replace(/\./g, "-")}.json`);
|
|
30435
31778
|
writeFileSync11(path, JSON.stringify(JSON_SCHEMAS[entity], null, 2));
|
|
30436
31779
|
written.push(path);
|
|
30437
31780
|
}
|
|
30438
|
-
const catalogPath =
|
|
31781
|
+
const catalogPath = join16(dir, "catalog.json");
|
|
30439
31782
|
writeFileSync11(catalogPath, JSON.stringify({
|
|
30440
31783
|
catalog_version: JSON_SCHEMA_CATALOG_VERSION,
|
|
30441
31784
|
semver: SCHEMA_SEMVER,
|
|
@@ -30641,6 +31984,9 @@ function hasFts(db) {
|
|
|
30641
31984
|
function escapeFtsQuery(q) {
|
|
30642
31985
|
return q.replace(/["*^()]/g, " ").trim().split(/\s+/).filter(Boolean).map((token) => `"${token}"*`).join(" ");
|
|
30643
31986
|
}
|
|
31987
|
+
function shouldUseFts(q) {
|
|
31988
|
+
return /^[\p{L}\p{N}_\-\s]+$/u.test(q);
|
|
31989
|
+
}
|
|
30644
31990
|
function searchTasks(options, projectId, taskListId, db) {
|
|
30645
31991
|
const opts = typeof options === "string" ? { query: options || undefined, project_id: projectId, task_list_id: taskListId } : options;
|
|
30646
31992
|
const d = db || getDatabase();
|
|
@@ -30649,7 +31995,8 @@ function searchTasks(options, projectId, taskListId, db) {
|
|
|
30649
31995
|
let sql;
|
|
30650
31996
|
const raw = opts.query?.trim() ?? "";
|
|
30651
31997
|
const q = raw === "*" ? "" : raw;
|
|
30652
|
-
|
|
31998
|
+
const useFts = hasFts(d) && q && shouldUseFts(q);
|
|
31999
|
+
if (useFts) {
|
|
30653
32000
|
const ftsQuery = escapeFtsQuery(q);
|
|
30654
32001
|
sql = `SELECT t.* FROM tasks t
|
|
30655
32002
|
INNER JOIN tasks_fts fts ON fts.rowid = t.rowid
|
|
@@ -30657,8 +32004,16 @@ function searchTasks(options, projectId, taskListId, db) {
|
|
|
30657
32004
|
params.push(ftsQuery);
|
|
30658
32005
|
} else if (q) {
|
|
30659
32006
|
const pattern = `%${q}%`;
|
|
30660
|
-
sql = `SELECT * FROM tasks t WHERE (
|
|
30661
|
-
|
|
32007
|
+
sql = `SELECT * FROM tasks t WHERE (
|
|
32008
|
+
t.id LIKE ?
|
|
32009
|
+
OR t.short_id LIKE ?
|
|
32010
|
+
OR t.title LIKE ?
|
|
32011
|
+
OR t.description LIKE ?
|
|
32012
|
+
OR t.working_dir LIKE ?
|
|
32013
|
+
OR t.metadata LIKE ?
|
|
32014
|
+
OR EXISTS (SELECT 1 FROM task_tags WHERE task_tags.task_id = t.id AND tag LIKE ?)
|
|
32015
|
+
)`;
|
|
32016
|
+
params.push(pattern, pattern, pattern, pattern, pattern, pattern, pattern);
|
|
30662
32017
|
} else {
|
|
30663
32018
|
sql = `SELECT * FROM tasks t WHERE 1=1`;
|
|
30664
32019
|
}
|
|
@@ -30714,7 +32069,7 @@ function searchTasks(options, projectId, taskListId, db) {
|
|
|
30714
32069
|
} else if (opts.is_blocked === false) {
|
|
30715
32070
|
sql += " AND t.id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed')";
|
|
30716
32071
|
}
|
|
30717
|
-
if (
|
|
32072
|
+
if (useFts) {
|
|
30718
32073
|
sql += ` ORDER BY bm25(tasks_fts),
|
|
30719
32074
|
CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
|
|
30720
32075
|
t.created_at DESC`;
|
|
@@ -32992,7 +34347,7 @@ function generateCliReferenceMarkdown() {
|
|
|
32992
34347
|
// src/db/builtin-templates.ts
|
|
32993
34348
|
init_database();
|
|
32994
34349
|
import { mkdirSync as mkdirSync15, writeFileSync as writeFileSync13 } from "fs";
|
|
32995
|
-
import { join as
|
|
34350
|
+
import { join as join16 } from "path";
|
|
32996
34351
|
var BUILTIN_TEMPLATE_LIBRARY_VERSION = "2026-05-21";
|
|
32997
34352
|
var BUILTIN_TEMPLATE_LIBRARY_SOURCE = "bundled-local-template-library";
|
|
32998
34353
|
var TEMPLATE_LIBRARY_SCHEMA = "todos.template_library.v1";
|
|
@@ -33268,7 +34623,7 @@ function writeBuiltinTemplateFiles(directory) {
|
|
|
33268
34623
|
mkdirSync15(directory, { recursive: true });
|
|
33269
34624
|
const files = [];
|
|
33270
34625
|
for (const entry2 of exportBuiltinTemplateFiles()) {
|
|
33271
|
-
const path =
|
|
34626
|
+
const path = join16(directory, entry2.filename);
|
|
33272
34627
|
writeFileSync13(path, `${JSON.stringify(entry2.template, null, 2)}
|
|
33273
34628
|
`, "utf-8");
|
|
33274
34629
|
files.push(path);
|
|
@@ -33550,10 +34905,10 @@ todos machines topology # full diagnostic report
|
|
|
33550
34905
|
`;
|
|
33551
34906
|
}
|
|
33552
34907
|
// src/lib/environment-snapshots.ts
|
|
33553
|
-
import { createHash as
|
|
33554
|
-
import { existsSync as
|
|
34908
|
+
import { createHash as createHash12 } from "crypto";
|
|
34909
|
+
import { existsSync as existsSync20, readFileSync as readFileSync19, statSync as statSync7 } from "fs";
|
|
33555
34910
|
import { hostname as hostname3, platform, arch } from "os";
|
|
33556
|
-
import { dirname as dirname15, join as
|
|
34911
|
+
import { dirname as dirname15, join as join17, resolve as resolve14 } from "path";
|
|
33557
34912
|
import { tmpdir as tmpdir2 } from "os";
|
|
33558
34913
|
init_database();
|
|
33559
34914
|
init_redaction();
|
|
@@ -33575,11 +34930,11 @@ var CONFIG_FILES = [
|
|
|
33575
34930
|
"dashboard/vite.config.ts"
|
|
33576
34931
|
];
|
|
33577
34932
|
function sha2565(value) {
|
|
33578
|
-
return
|
|
34933
|
+
return createHash12("sha256").update(value).digest("hex");
|
|
33579
34934
|
}
|
|
33580
34935
|
function fileRecord(root, relativePath) {
|
|
33581
|
-
const path =
|
|
33582
|
-
if (!
|
|
34936
|
+
const path = join17(root, relativePath);
|
|
34937
|
+
if (!existsSync20(path))
|
|
33583
34938
|
return null;
|
|
33584
34939
|
const stat = statSync7(path);
|
|
33585
34940
|
if (!stat.isFile())
|
|
@@ -33591,7 +34946,7 @@ function manifestRecord(root, relativePath) {
|
|
|
33591
34946
|
const base = fileRecord(root, relativePath);
|
|
33592
34947
|
if (!base)
|
|
33593
34948
|
return null;
|
|
33594
|
-
const parsed = readJsonFile(
|
|
34949
|
+
const parsed = readJsonFile(join17(root, relativePath));
|
|
33595
34950
|
if (!parsed)
|
|
33596
34951
|
return { ...base, redacted: {} };
|
|
33597
34952
|
const redacted = redactValue({
|
|
@@ -33686,8 +35041,8 @@ function commandEnv(env, includeValues) {
|
|
|
33686
35041
|
function defaultSnapshotDir() {
|
|
33687
35042
|
const dbPath = getDatabasePath();
|
|
33688
35043
|
if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
|
|
33689
|
-
return
|
|
33690
|
-
return
|
|
35044
|
+
return join17(tmpdir2(), "hasna-todos", "environment-snapshots");
|
|
35045
|
+
return join17(dirname15(resolve14(dbPath)), "environment-snapshots");
|
|
33691
35046
|
}
|
|
33692
35047
|
function snapshotWithId(snapshot) {
|
|
33693
35048
|
const digest = sha2565(JSON.stringify(snapshot)).slice(0, 24);
|
|
@@ -33734,7 +35089,7 @@ function captureEnvironmentSnapshot(input = {}) {
|
|
|
33734
35089
|
});
|
|
33735
35090
|
}
|
|
33736
35091
|
function writeEnvironmentSnapshot(snapshot, outputPath) {
|
|
33737
|
-
const path = outputPath ? resolve14(outputPath) :
|
|
35092
|
+
const path = outputPath ? resolve14(outputPath) : join17(defaultSnapshotDir(), `${snapshot.id}.json`);
|
|
33738
35093
|
ensureDir2(dirname15(path));
|
|
33739
35094
|
writeJsonFile(path, snapshot);
|
|
33740
35095
|
return path;
|
|
@@ -33825,9 +35180,9 @@ function compareEnvironmentSnapshotFiles(leftPath, rightPath) {
|
|
|
33825
35180
|
}
|
|
33826
35181
|
// src/lib/decision-records.ts
|
|
33827
35182
|
init_database();
|
|
33828
|
-
import { createHash as
|
|
35183
|
+
import { createHash as createHash13 } from "crypto";
|
|
33829
35184
|
import { mkdirSync as mkdirSync17, writeFileSync as writeFileSync15 } from "fs";
|
|
33830
|
-
import { dirname as dirname16, join as
|
|
35185
|
+
import { dirname as dirname16, join as join18 } from "path";
|
|
33831
35186
|
var DECISION_RECORD_SCHEMA = "todos.decision_record.v1";
|
|
33832
35187
|
var KNOWLEDGE_SNAPSHOT_SCHEMA = "todos.knowledge_snapshot.v1";
|
|
33833
35188
|
var DECISION_STATUSES = ["proposed", "accepted", "deprecated", "superseded", "rejected"];
|
|
@@ -33881,7 +35236,7 @@ function rowToDecisionRecord(row) {
|
|
|
33881
35236
|
}
|
|
33882
35237
|
function stableSnapshotHash(payload) {
|
|
33883
35238
|
const { captured_at: _capturedAt, ...rest } = payload;
|
|
33884
|
-
return
|
|
35239
|
+
return createHash13("sha256").update(JSON.stringify(rest)).digest("hex");
|
|
33885
35240
|
}
|
|
33886
35241
|
function createDecisionRecord(input, db) {
|
|
33887
35242
|
const d = db || getDatabase();
|
|
@@ -34057,7 +35412,7 @@ function exportDecisionRecord(id, outputPath, format = "markdown", db) {
|
|
|
34057
35412
|
if (!record)
|
|
34058
35413
|
throw new Error(`Decision record not found: ${id}`);
|
|
34059
35414
|
const content = format === "markdown" ? formatDecisionRecordMarkdown(record) : JSON.stringify(record, null, 2);
|
|
34060
|
-
const path = outputPath ??
|
|
35415
|
+
const path = outputPath ?? join18(process.cwd(), ".todos", "decisions", `${record.short_ref}.${format === "markdown" ? "md" : "json"}`);
|
|
34061
35416
|
mkdirSync17(dirname16(path), { recursive: true });
|
|
34062
35417
|
writeFileSync15(path, content, "utf8");
|
|
34063
35418
|
return { path, content };
|
|
@@ -34205,7 +35560,7 @@ function exportKnowledgeSnapshot(id, outputPath, format = "markdown", db) {
|
|
|
34205
35560
|
throw new Error(`Knowledge snapshot not found: ${id}`);
|
|
34206
35561
|
const content = format === "markdown" ? formatKnowledgeSnapshotMarkdown(record) : JSON.stringify(record, null, 2);
|
|
34207
35562
|
const slug = record.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
34208
|
-
const path = outputPath ??
|
|
35563
|
+
const path = outputPath ?? join18(process.cwd(), ".todos", "knowledge", `${slug || record.id.slice(0, 8)}.${format === "markdown" ? "md" : "json"}`);
|
|
34209
35564
|
mkdirSync17(dirname16(path), { recursive: true });
|
|
34210
35565
|
writeFileSync15(path, content, "utf8");
|
|
34211
35566
|
return { path, content };
|
|
@@ -34500,8 +35855,8 @@ todos report export --kind retrospective --days 14 --format markdown --out retro
|
|
|
34500
35855
|
`;
|
|
34501
35856
|
}
|
|
34502
35857
|
// src/lib/command-aliases.ts
|
|
34503
|
-
import { existsSync as
|
|
34504
|
-
import { join as
|
|
35858
|
+
import { existsSync as existsSync21, readFileSync as readFileSync20, writeFileSync as writeFileSync17, mkdirSync as mkdirSync19 } from "fs";
|
|
35859
|
+
import { join as join19 } from "path";
|
|
34505
35860
|
var COMMAND_ALIASES_SCHEMA = "todos.command_aliases.v1";
|
|
34506
35861
|
var RESERVED = new Set([...listTopLevelCommands(), "help", "version", "alias", "shortcuts"]);
|
|
34507
35862
|
var BUILTIN_SHORTCUTS = [
|
|
@@ -34520,7 +35875,7 @@ var BUILTIN_SHORTCUTS = [
|
|
|
34520
35875
|
{ pattern: /^reports?$/, argv: ["report", "docs"], explain: "Report export documentation" }
|
|
34521
35876
|
];
|
|
34522
35877
|
function aliasesPath(cwd = process.cwd()) {
|
|
34523
|
-
return
|
|
35878
|
+
return join19(cwd, ".todos", "aliases.json");
|
|
34524
35879
|
}
|
|
34525
35880
|
function emptyStore() {
|
|
34526
35881
|
return { schema_version: COMMAND_ALIASES_SCHEMA, aliases: {}, updated_at: new Date(0).toISOString() };
|
|
@@ -34537,7 +35892,7 @@ function validateAliasName(name) {
|
|
|
34537
35892
|
}
|
|
34538
35893
|
function loadAliasStore(cwd) {
|
|
34539
35894
|
const path = aliasesPath(cwd);
|
|
34540
|
-
if (!
|
|
35895
|
+
if (!existsSync21(path))
|
|
34541
35896
|
return emptyStore();
|
|
34542
35897
|
const parsed = JSON.parse(readFileSync20(path, "utf8"));
|
|
34543
35898
|
if (parsed.schema_version !== COMMAND_ALIASES_SCHEMA) {
|
|
@@ -34547,7 +35902,7 @@ function loadAliasStore(cwd) {
|
|
|
34547
35902
|
}
|
|
34548
35903
|
function saveAliasStore(store, cwd) {
|
|
34549
35904
|
const path = aliasesPath(cwd);
|
|
34550
|
-
mkdirSync19(
|
|
35905
|
+
mkdirSync19(join19(path, ".."), { recursive: true });
|
|
34551
35906
|
store.updated_at = new Date().toISOString();
|
|
34552
35907
|
writeFileSync17(path, JSON.stringify(store, null, 2), "utf8");
|
|
34553
35908
|
}
|
|
@@ -35195,18 +36550,18 @@ function createBranchWorkPlan(input, db) {
|
|
|
35195
36550
|
}
|
|
35196
36551
|
// src/lib/user-scaffolds.ts
|
|
35197
36552
|
init_database();
|
|
35198
|
-
import { existsSync as
|
|
35199
|
-
import { join as
|
|
36553
|
+
import { existsSync as existsSync22, readFileSync as readFileSync21, writeFileSync as writeFileSync18, mkdirSync as mkdirSync20 } from "fs";
|
|
36554
|
+
import { join as join20 } from "path";
|
|
35200
36555
|
var USER_SCAFFOLD_SCHEMA = "todos.user_scaffold.v1";
|
|
35201
36556
|
var SCAFFOLD_KINDS = ["task", "project", "plan", "checklist", "contract", "verification_policy"];
|
|
35202
36557
|
function storeDir(cwd = process.cwd()) {
|
|
35203
|
-
return
|
|
36558
|
+
return join20(cwd, ".todos", "scaffolds");
|
|
35204
36559
|
}
|
|
35205
36560
|
function storePath(cwd) {
|
|
35206
|
-
return
|
|
36561
|
+
return join20(storeDir(cwd), "store.json");
|
|
35207
36562
|
}
|
|
35208
36563
|
function versionsDir(cwd) {
|
|
35209
|
-
return
|
|
36564
|
+
return join20(storeDir(cwd), "versions");
|
|
35210
36565
|
}
|
|
35211
36566
|
function slugify5(name) {
|
|
35212
36567
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -35216,7 +36571,7 @@ function emptyStore2() {
|
|
|
35216
36571
|
}
|
|
35217
36572
|
function loadUserScaffoldStore(cwd) {
|
|
35218
36573
|
const path = storePath(cwd);
|
|
35219
|
-
if (!
|
|
36574
|
+
if (!existsSync22(path))
|
|
35220
36575
|
return emptyStore2();
|
|
35221
36576
|
const parsed = JSON.parse(readFileSync21(path, "utf8"));
|
|
35222
36577
|
if (parsed.schema_version !== USER_SCAFFOLD_SCHEMA) {
|
|
@@ -35231,7 +36586,7 @@ function saveUserScaffoldStore(store, cwd) {
|
|
|
35231
36586
|
}
|
|
35232
36587
|
function snapshotVersion(scaffold, cwd) {
|
|
35233
36588
|
mkdirSync20(versionsDir(cwd), { recursive: true });
|
|
35234
|
-
const path =
|
|
36589
|
+
const path = join20(versionsDir(cwd), `${scaffold.id}-v${scaffold.version}.json`);
|
|
35235
36590
|
writeFileSync18(path, JSON.stringify(scaffold, null, 2), "utf8");
|
|
35236
36591
|
}
|
|
35237
36592
|
function listUserScaffolds(kind, cwd) {
|
|
@@ -35478,7 +36833,7 @@ function listLinkedTemplates(db, cwd) {
|
|
|
35478
36833
|
// src/lib/agent-workflow-demo.ts
|
|
35479
36834
|
init_database();
|
|
35480
36835
|
import { mkdtempSync } from "fs";
|
|
35481
|
-
import { join as
|
|
36836
|
+
import { join as join21 } from "path";
|
|
35482
36837
|
import { tmpdir as tmpdir3 } from "os";
|
|
35483
36838
|
var AGENT_WORKFLOW_DEMO_SCHEMA = "todos.agent_workflow_demo.v1";
|
|
35484
36839
|
var DEMO_DEFAULT_AGENT = "demoagent";
|
|
@@ -35494,7 +36849,7 @@ function setupEphemeralDemoDb(options = {}) {
|
|
|
35494
36849
|
if (options.db_path) {
|
|
35495
36850
|
db_path = options.db_path;
|
|
35496
36851
|
} else if (options.persist) {
|
|
35497
|
-
db_path =
|
|
36852
|
+
db_path = join21(mkdtempSync(join21(tmpdir3(), "todos-demo-")), "todos.db");
|
|
35498
36853
|
} else {
|
|
35499
36854
|
db_path = ":memory:";
|
|
35500
36855
|
}
|
|
@@ -37830,18 +39185,18 @@ function runSearchView(idOrName, db) {
|
|
|
37830
39185
|
return { ...runSavedSearch(view.filters, view.scope, d), view };
|
|
37831
39186
|
}
|
|
37832
39187
|
// src/lib/claude-tasks.ts
|
|
37833
|
-
import { existsSync as
|
|
37834
|
-
import { join as
|
|
39188
|
+
import { existsSync as existsSync23, readFileSync as readFileSync22, readdirSync as readdirSync4, writeFileSync as writeFileSync19 } from "fs";
|
|
39189
|
+
import { join as join22 } from "path";
|
|
37835
39190
|
init_config();
|
|
37836
39191
|
init_sync_utils();
|
|
37837
39192
|
function getTaskListDir(taskListId) {
|
|
37838
|
-
return
|
|
39193
|
+
return join22(HOME, ".claude", "tasks", taskListId);
|
|
37839
39194
|
}
|
|
37840
39195
|
function readClaudeTask(dir, filename) {
|
|
37841
|
-
return readJsonFile(
|
|
39196
|
+
return readJsonFile(join22(dir, filename));
|
|
37842
39197
|
}
|
|
37843
39198
|
function writeClaudeTask(dir, task2) {
|
|
37844
|
-
writeJsonFile(
|
|
39199
|
+
writeJsonFile(join22(dir, `${task2.id}.json`), task2);
|
|
37845
39200
|
}
|
|
37846
39201
|
function toClaudeStatus(status) {
|
|
37847
39202
|
if (status === "pending" || status === "in_progress" || status === "completed") {
|
|
@@ -37853,14 +39208,14 @@ function toSqliteStatus(status) {
|
|
|
37853
39208
|
return status;
|
|
37854
39209
|
}
|
|
37855
39210
|
function readPrefixCounter(dir) {
|
|
37856
|
-
const path =
|
|
37857
|
-
if (!
|
|
39211
|
+
const path = join22(dir, ".prefix-counter");
|
|
39212
|
+
if (!existsSync23(path))
|
|
37858
39213
|
return 0;
|
|
37859
39214
|
const val = parseInt(readFileSync22(path, "utf-8").trim(), 10);
|
|
37860
39215
|
return isNaN(val) ? 0 : val;
|
|
37861
39216
|
}
|
|
37862
39217
|
function writePrefixCounter(dir, value) {
|
|
37863
|
-
writeFileSync19(
|
|
39218
|
+
writeFileSync19(join22(dir, ".prefix-counter"), String(value));
|
|
37864
39219
|
}
|
|
37865
39220
|
function formatPrefixedSubject(title, prefix, counter) {
|
|
37866
39221
|
const padded = String(counter).padStart(5, "0");
|
|
@@ -37887,7 +39242,7 @@ function taskToClaudeTask(task2, claudeTaskId, existingMeta) {
|
|
|
37887
39242
|
}
|
|
37888
39243
|
function pushToClaudeTaskList(taskListId, projectId, options = {}) {
|
|
37889
39244
|
const dir = getTaskListDir(taskListId);
|
|
37890
|
-
if (!
|
|
39245
|
+
if (!existsSync23(dir))
|
|
37891
39246
|
ensureDir2(dir);
|
|
37892
39247
|
const filter = {};
|
|
37893
39248
|
if (projectId)
|
|
@@ -37896,7 +39251,7 @@ function pushToClaudeTaskList(taskListId, projectId, options = {}) {
|
|
|
37896
39251
|
const existingByTodosId = new Map;
|
|
37897
39252
|
const files = listJsonFiles(dir);
|
|
37898
39253
|
for (const f of files) {
|
|
37899
|
-
const path =
|
|
39254
|
+
const path = join22(dir, f);
|
|
37900
39255
|
const ct = readClaudeTask(dir, f);
|
|
37901
39256
|
if (ct?.metadata?.["todos_id"]) {
|
|
37902
39257
|
existingByTodosId.set(ct.metadata["todos_id"], { task: ct, mtimeMs: getFileMtimeMs(path) });
|
|
@@ -37983,7 +39338,7 @@ function pushToClaudeTaskList(taskListId, projectId, options = {}) {
|
|
|
37983
39338
|
}
|
|
37984
39339
|
function pullFromClaudeTaskList(taskListId, projectId, options = {}) {
|
|
37985
39340
|
const dir = getTaskListDir(taskListId);
|
|
37986
|
-
if (!
|
|
39341
|
+
if (!existsSync23(dir)) {
|
|
37987
39342
|
return { pushed: 0, pulled: 0, errors: [`Task list directory not found: ${dir}`] };
|
|
37988
39343
|
}
|
|
37989
39344
|
const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
|
|
@@ -38003,7 +39358,7 @@ function pullFromClaudeTaskList(taskListId, projectId, options = {}) {
|
|
|
38003
39358
|
}
|
|
38004
39359
|
for (const f of files) {
|
|
38005
39360
|
try {
|
|
38006
|
-
const filePath =
|
|
39361
|
+
const filePath = join22(dir, f);
|
|
38007
39362
|
const ct = readClaudeTask(dir, f);
|
|
38008
39363
|
if (!ct)
|
|
38009
39364
|
continue;
|
|
@@ -38071,22 +39426,22 @@ function syncClaudeTaskList(taskListId, projectId, options = {}) {
|
|
|
38071
39426
|
}
|
|
38072
39427
|
|
|
38073
39428
|
// src/lib/agent-tasks.ts
|
|
38074
|
-
import { existsSync as
|
|
38075
|
-
import { join as
|
|
39429
|
+
import { existsSync as existsSync24 } from "fs";
|
|
39430
|
+
import { join as join23 } from "path";
|
|
38076
39431
|
init_sync_utils();
|
|
38077
39432
|
init_config();
|
|
38078
39433
|
function agentBaseDir(agent) {
|
|
38079
39434
|
const key = `TODOS_${agent.toUpperCase()}_TASKS_DIR`;
|
|
38080
|
-
return process.env[key] || getAgentTasksDir(agent) || process.env["TODOS_AGENT_TASKS_DIR"] ||
|
|
39435
|
+
return process.env[key] || getAgentTasksDir(agent) || process.env["TODOS_AGENT_TASKS_DIR"] || join23(getTodosGlobalDir(), "agents");
|
|
38081
39436
|
}
|
|
38082
39437
|
function getTaskListDir2(agent, taskListId) {
|
|
38083
|
-
return
|
|
39438
|
+
return join23(agentBaseDir(agent), agent, taskListId);
|
|
38084
39439
|
}
|
|
38085
39440
|
function readAgentTask(dir, filename) {
|
|
38086
|
-
return readJsonFile(
|
|
39441
|
+
return readJsonFile(join23(dir, filename));
|
|
38087
39442
|
}
|
|
38088
39443
|
function writeAgentTask(dir, task2) {
|
|
38089
|
-
writeJsonFile(
|
|
39444
|
+
writeJsonFile(join23(dir, `${task2.id}.json`), task2);
|
|
38090
39445
|
}
|
|
38091
39446
|
function taskToAgentTask(task2, externalId, existingMeta) {
|
|
38092
39447
|
return {
|
|
@@ -38111,7 +39466,7 @@ function metadataKey(agent) {
|
|
|
38111
39466
|
}
|
|
38112
39467
|
function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
|
|
38113
39468
|
const dir = getTaskListDir2(agent, taskListId);
|
|
38114
|
-
if (!
|
|
39469
|
+
if (!existsSync24(dir))
|
|
38115
39470
|
ensureDir2(dir);
|
|
38116
39471
|
const filter = {};
|
|
38117
39472
|
if (projectId)
|
|
@@ -38120,7 +39475,7 @@ function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
|
|
|
38120
39475
|
const existingByTodosId = new Map;
|
|
38121
39476
|
const files = listJsonFiles(dir);
|
|
38122
39477
|
for (const f of files) {
|
|
38123
|
-
const path =
|
|
39478
|
+
const path = join23(dir, f);
|
|
38124
39479
|
const at = readAgentTask(dir, f);
|
|
38125
39480
|
if (at?.metadata?.["todos_id"]) {
|
|
38126
39481
|
existingByTodosId.set(at.metadata["todos_id"], { task: at, mtimeMs: getFileMtimeMs(path) });
|
|
@@ -38194,7 +39549,7 @@ function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
|
|
|
38194
39549
|
}
|
|
38195
39550
|
function pullFromAgentTaskList(agent, taskListId, projectId, options = {}) {
|
|
38196
39551
|
const dir = getTaskListDir2(agent, taskListId);
|
|
38197
|
-
if (!
|
|
39552
|
+
if (!existsSync24(dir)) {
|
|
38198
39553
|
return { pushed: 0, pulled: 0, errors: [`Task list directory not found: ${dir}`] };
|
|
38199
39554
|
}
|
|
38200
39555
|
const files = listJsonFiles(dir);
|
|
@@ -38213,7 +39568,7 @@ function pullFromAgentTaskList(agent, taskListId, projectId, options = {}) {
|
|
|
38213
39568
|
}
|
|
38214
39569
|
for (const f of files) {
|
|
38215
39570
|
try {
|
|
38216
|
-
const filePath =
|
|
39571
|
+
const filePath = join23(dir, f);
|
|
38217
39572
|
const at = readAgentTask(dir, f);
|
|
38218
39573
|
if (!at)
|
|
38219
39574
|
continue;
|
|
@@ -38351,9 +39706,9 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
|
|
|
38351
39706
|
return { pushed, pulled, errors };
|
|
38352
39707
|
}
|
|
38353
39708
|
// src/lib/extract.ts
|
|
38354
|
-
import { existsSync as
|
|
38355
|
-
import { createHash as
|
|
38356
|
-
import { relative as relative6, resolve as resolve15, join as
|
|
39709
|
+
import { existsSync as existsSync25, readFileSync as readFileSync23, statSync as statSync8 } from "fs";
|
|
39710
|
+
import { createHash as createHash14 } from "crypto";
|
|
39711
|
+
import { relative as relative6, resolve as resolve15, join as join24 } from "path";
|
|
38357
39712
|
var EXTRACT_TAGS = ["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"];
|
|
38358
39713
|
var DEFAULT_EXTENSIONS = new Set([
|
|
38359
39714
|
".ts",
|
|
@@ -38417,15 +39772,15 @@ var SKIP_DIRS2 = new Set([
|
|
|
38417
39772
|
".parcel-cache"
|
|
38418
39773
|
]);
|
|
38419
39774
|
function stableHash(value) {
|
|
38420
|
-
return
|
|
39775
|
+
return createHash14("sha256").update(value).digest("hex");
|
|
38421
39776
|
}
|
|
38422
39777
|
function normalizePathForMatch(value) {
|
|
38423
39778
|
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
38424
39779
|
}
|
|
38425
39780
|
function readGitignorePatterns(basePath) {
|
|
38426
39781
|
const root = statSync8(basePath).isFile() ? resolve15(basePath, "..") : basePath;
|
|
38427
|
-
const gitignorePath =
|
|
38428
|
-
if (!
|
|
39782
|
+
const gitignorePath = join24(root, ".gitignore");
|
|
39783
|
+
if (!existsSync25(gitignorePath))
|
|
38429
39784
|
return [];
|
|
38430
39785
|
try {
|
|
38431
39786
|
return readFileSync23(gitignorePath, "utf-8").split(`
|
|
@@ -38567,7 +39922,7 @@ function buildCodebaseIndex(options) {
|
|
|
38567
39922
|
const files = collectFiles(basePath, extensions, excludes, respectGitignore);
|
|
38568
39923
|
const indexed = [];
|
|
38569
39924
|
for (const file of files) {
|
|
38570
|
-
const fullPath = statSync8(basePath).isFile() ? basePath :
|
|
39925
|
+
const fullPath = statSync8(basePath).isFile() ? basePath : join24(basePath, file);
|
|
38571
39926
|
try {
|
|
38572
39927
|
const source9 = readFileSync23(fullPath, "utf-8");
|
|
38573
39928
|
const relPath = statSync8(basePath).isFile() ? relative6(resolve15(basePath, ".."), fullPath) : file;
|
|
@@ -38598,7 +39953,7 @@ function extractTodos(options, db) {
|
|
|
38598
39953
|
const files = collectFiles(basePath, extensions, excludes, respectGitignore);
|
|
38599
39954
|
const allComments = [];
|
|
38600
39955
|
for (const file of files) {
|
|
38601
|
-
const fullPath = statSync8(basePath).isFile() ? basePath :
|
|
39956
|
+
const fullPath = statSync8(basePath).isFile() ? basePath : join24(basePath, file);
|
|
38602
39957
|
try {
|
|
38603
39958
|
const source9 = readFileSync23(fullPath, "utf-8");
|
|
38604
39959
|
const relPath = statSync8(basePath).isFile() ? relative6(resolve15(basePath, ".."), fullPath) : file;
|
|
@@ -38633,7 +39988,7 @@ function extractTodos(options, db) {
|
|
|
38633
39988
|
skipped++;
|
|
38634
39989
|
continue;
|
|
38635
39990
|
}
|
|
38636
|
-
const
|
|
39991
|
+
const taskTags2 = ["extracted", comment.tag.toLowerCase(), ...options.tags || []];
|
|
38637
39992
|
const task2 = createTask({
|
|
38638
39993
|
title: `[${comment.tag}] ${comment.message}`,
|
|
38639
39994
|
description: `Extracted from code comment in \`${comment.file}\` at line ${comment.line}:
|
|
@@ -38645,7 +40000,7 @@ ${comment.raw.trim()}
|
|
|
38645
40000
|
task_list_id: options.task_list_id,
|
|
38646
40001
|
assigned_to: options.assigned_to,
|
|
38647
40002
|
agent_id: options.agent_id,
|
|
38648
|
-
tags:
|
|
40003
|
+
tags: taskTags2,
|
|
38649
40004
|
metadata: {
|
|
38650
40005
|
source: "code_comment",
|
|
38651
40006
|
comment_type: comment.tag,
|
|
@@ -38906,7 +40261,7 @@ init_config();
|
|
|
38906
40261
|
// src/lib/natural-language-intake.ts
|
|
38907
40262
|
init_database();
|
|
38908
40263
|
init_redaction();
|
|
38909
|
-
var
|
|
40264
|
+
var PRIORITIES4 = new Set(["low", "medium", "high", "critical"]);
|
|
38910
40265
|
function compact2(value) {
|
|
38911
40266
|
return value.replace(/\s+/g, " ").trim();
|
|
38912
40267
|
}
|
|
@@ -38918,7 +40273,7 @@ function splitItems(text) {
|
|
|
38918
40273
|
}
|
|
38919
40274
|
function parsePriority(line, fallback) {
|
|
38920
40275
|
const explicit = line.match(/\b(?:priority[:= ]+|p:)(critical|high|medium|low)\b/i)?.[1]?.toLowerCase();
|
|
38921
|
-
if (explicit &&
|
|
40276
|
+
if (explicit && PRIORITIES4.has(explicit))
|
|
38922
40277
|
return explicit;
|
|
38923
40278
|
if (/\b(p0|urgent|blocker|critical)\b/i.test(line))
|
|
38924
40279
|
return "critical";
|
|
@@ -39305,7 +40660,7 @@ function renderWorkflowStatesMarkdown(states = listWorkflowStates()) {
|
|
|
39305
40660
|
}
|
|
39306
40661
|
// src/lib/agent-replay-simulator.ts
|
|
39307
40662
|
init_redaction();
|
|
39308
|
-
import { createHash as
|
|
40663
|
+
import { createHash as createHash15 } from "crypto";
|
|
39309
40664
|
import { readFileSync as readFileSync24 } from "fs";
|
|
39310
40665
|
function isObject(value) {
|
|
39311
40666
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
@@ -39327,7 +40682,7 @@ function stable2(value) {
|
|
|
39327
40682
|
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable2(value[key])]));
|
|
39328
40683
|
}
|
|
39329
40684
|
function fingerprint3(value) {
|
|
39330
|
-
return
|
|
40685
|
+
return createHash15("sha256").update(JSON.stringify(stable2(value))).digest("hex");
|
|
39331
40686
|
}
|
|
39332
40687
|
function unpackFixture(input) {
|
|
39333
40688
|
if (!isObject(input))
|
|
@@ -39566,9 +40921,9 @@ function renderAgentReplaySimulationMarkdown(simulation) {
|
|
|
39566
40921
|
}
|
|
39567
40922
|
// src/lib/local-extensions.ts
|
|
39568
40923
|
init_config();
|
|
39569
|
-
import { createHash as
|
|
39570
|
-
import { existsSync as
|
|
39571
|
-
import { basename as basename5, join as
|
|
40924
|
+
import { createHash as createHash16, createVerify } from "crypto";
|
|
40925
|
+
import { existsSync as existsSync26, readdirSync as readdirSync5, readFileSync as readFileSync25, statSync as statSync9 } from "fs";
|
|
40926
|
+
import { basename as basename5, join as join25, resolve as resolve16 } from "path";
|
|
39572
40927
|
init_redaction();
|
|
39573
40928
|
function isObject2(value) {
|
|
39574
40929
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
@@ -39653,7 +41008,7 @@ function parseJson(path) {
|
|
|
39653
41008
|
return JSON.parse(readFileSync25(path, "utf8"));
|
|
39654
41009
|
}
|
|
39655
41010
|
function sha2566(bytes) {
|
|
39656
|
-
return `sha256:${
|
|
41011
|
+
return `sha256:${createHash16("sha256").update(bytes).digest("hex")}`;
|
|
39657
41012
|
}
|
|
39658
41013
|
function compareVersions(a, b) {
|
|
39659
41014
|
const left = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
@@ -39849,10 +41204,10 @@ function verifyExtensionSignature(input) {
|
|
|
39849
41204
|
}
|
|
39850
41205
|
function inspectExtensionSource(source9) {
|
|
39851
41206
|
const resolved = resolve16(source9);
|
|
39852
|
-
if (!
|
|
41207
|
+
if (!existsSync26(resolved))
|
|
39853
41208
|
throw new Error(`extension source not found: ${source9}`);
|
|
39854
41209
|
const stat = statSync9(resolved);
|
|
39855
|
-
const manifestPath = stat.isDirectory() ? [
|
|
41210
|
+
const manifestPath = stat.isDirectory() ? [join25(resolved, "todos.extension.json"), join25(resolved, "extension.json")].find(existsSync26) : resolved;
|
|
39856
41211
|
if (!manifestPath)
|
|
39857
41212
|
throw new Error(`extension directory ${source9} is missing todos.extension.json`);
|
|
39858
41213
|
const raw = readFileSync25(manifestPath);
|
|
@@ -39948,20 +41303,20 @@ function projectExtensionSources(projectPath) {
|
|
|
39948
41303
|
return [];
|
|
39949
41304
|
const root = resolve16(projectPath);
|
|
39950
41305
|
const candidates = [
|
|
39951
|
-
|
|
39952
|
-
|
|
41306
|
+
join25(root, "todos.extension.json"),
|
|
41307
|
+
join25(root, ".todos", "todos.extension.json")
|
|
39953
41308
|
];
|
|
39954
|
-
const extensionDir =
|
|
39955
|
-
if (
|
|
41309
|
+
const extensionDir = join25(root, ".todos", "extensions");
|
|
41310
|
+
if (existsSync26(extensionDir)) {
|
|
39956
41311
|
for (const entry2 of readdirSync5(extensionDir)) {
|
|
39957
41312
|
if (entry2.startsWith("."))
|
|
39958
41313
|
continue;
|
|
39959
|
-
const full =
|
|
41314
|
+
const full = join25(extensionDir, entry2);
|
|
39960
41315
|
if (statSync9(full).isDirectory() || entry2.endsWith(".json"))
|
|
39961
41316
|
candidates.push(full);
|
|
39962
41317
|
}
|
|
39963
41318
|
}
|
|
39964
|
-
return candidates.filter(
|
|
41319
|
+
return candidates.filter(existsSync26);
|
|
39965
41320
|
}
|
|
39966
41321
|
function discoverLocalExtensions(options = {}) {
|
|
39967
41322
|
const config = loadConfig();
|
|
@@ -40248,6 +41603,18 @@ function formatDispatchMessage(tasks, opts = {}) {
|
|
|
40248
41603
|
// src/lib/tmux.ts
|
|
40249
41604
|
var DELAY_MIN = 3000;
|
|
40250
41605
|
var DELAY_MAX = 5000;
|
|
41606
|
+
var IDLE_TMUX_COMMANDS = new Set([
|
|
41607
|
+
"bash",
|
|
41608
|
+
"dash",
|
|
41609
|
+
"elvish",
|
|
41610
|
+
"fish",
|
|
41611
|
+
"ksh",
|
|
41612
|
+
"nu",
|
|
41613
|
+
"pwsh",
|
|
41614
|
+
"sh",
|
|
41615
|
+
"tmux",
|
|
41616
|
+
"zsh"
|
|
41617
|
+
]);
|
|
40251
41618
|
function parseTmuxTarget(spec) {
|
|
40252
41619
|
if (!spec || spec.trim() === "") {
|
|
40253
41620
|
throw new Error("tmux target spec cannot be empty");
|
|
@@ -40304,17 +41671,73 @@ async function validateTmuxTarget(spec) {
|
|
|
40304
41671
|
throw new Error(`tmux target "${targetStr}" not found: ${stderr.trim() || "unknown error"}`);
|
|
40305
41672
|
}
|
|
40306
41673
|
}
|
|
41674
|
+
async function inspectTmuxPane(target) {
|
|
41675
|
+
const proc = Bun.spawn([
|
|
41676
|
+
"tmux",
|
|
41677
|
+
"display-message",
|
|
41678
|
+
"-p",
|
|
41679
|
+
"-t",
|
|
41680
|
+
target,
|
|
41681
|
+
"#{pane_id}\t#{pane_current_command}\t#{pane_dead}\t#{pane_input_off}\t#{pane_in_mode}"
|
|
41682
|
+
], {
|
|
41683
|
+
stdout: "pipe",
|
|
41684
|
+
stderr: "pipe"
|
|
41685
|
+
});
|
|
41686
|
+
const exitCode = await proc.exited;
|
|
41687
|
+
if (exitCode !== 0) {
|
|
41688
|
+
const stderr = await new Response(proc.stderr).text();
|
|
41689
|
+
throw new Error(`tmux target "${target}" not found: ${stderr.trim() || "unknown error"}`);
|
|
41690
|
+
}
|
|
41691
|
+
const stdout = (await new Response(proc.stdout).text()).trim();
|
|
41692
|
+
const [paneId, currentCommand = "", paneDead = "0", inputOff = "0", inMode = "0"] = stdout.split("\t");
|
|
41693
|
+
if (!paneId) {
|
|
41694
|
+
throw new Error(`tmux target "${target}" did not resolve to a pane`);
|
|
41695
|
+
}
|
|
41696
|
+
return {
|
|
41697
|
+
target,
|
|
41698
|
+
paneId,
|
|
41699
|
+
currentCommand,
|
|
41700
|
+
paneDead: paneDead === "1",
|
|
41701
|
+
inputOff: inputOff === "1",
|
|
41702
|
+
inMode: inMode === "1"
|
|
41703
|
+
};
|
|
41704
|
+
}
|
|
41705
|
+
function tmuxPaneBusyStatus(pane) {
|
|
41706
|
+
if (pane.paneDead) {
|
|
41707
|
+
return { busy: true, reason: "pane is dead" };
|
|
41708
|
+
}
|
|
41709
|
+
if (pane.inputOff) {
|
|
41710
|
+
return { busy: true, reason: "pane input is disabled" };
|
|
41711
|
+
}
|
|
41712
|
+
if (pane.inMode) {
|
|
41713
|
+
return { busy: true, reason: "pane is in copy or alternate mode" };
|
|
41714
|
+
}
|
|
41715
|
+
const currentCommand = pane.currentCommand.trim();
|
|
41716
|
+
if (currentCommand && !IDLE_TMUX_COMMANDS.has(currentCommand)) {
|
|
41717
|
+
return { busy: true, reason: `pane is running ${currentCommand}` };
|
|
41718
|
+
}
|
|
41719
|
+
return { busy: false, reason: null };
|
|
41720
|
+
}
|
|
40307
41721
|
function calculateDelay(message) {
|
|
40308
41722
|
const len = message.length;
|
|
40309
41723
|
const extra = Math.floor(len / 100 * 40);
|
|
40310
41724
|
return Math.min(DELAY_MIN + extra, DELAY_MAX);
|
|
40311
41725
|
}
|
|
40312
|
-
async function sendToTmux(target, message, delayMs,
|
|
41726
|
+
async function sendToTmux(target, message, delayMs, options = false) {
|
|
41727
|
+
const opts = typeof options === "boolean" ? { dryRun: options } : options;
|
|
41728
|
+
const dryRun = opts.dryRun ?? false;
|
|
40313
41729
|
if (dryRun) {
|
|
40314
41730
|
console.log(`[dry-run] sendToTmux target=${target} delay=${delayMs}ms`);
|
|
40315
41731
|
console.log(`[dry-run] message: ${message.slice(0, 200)}`);
|
|
40316
41732
|
return;
|
|
40317
41733
|
}
|
|
41734
|
+
if (!opts.confirmBusy) {
|
|
41735
|
+
const pane = await inspectTmuxPane(target);
|
|
41736
|
+
const status = tmuxPaneBusyStatus(pane);
|
|
41737
|
+
if (status.busy) {
|
|
41738
|
+
throw new Error(`tmux target "${target}" appears busy (${status.reason}). Re-run with --confirm-busy to send anyway.`);
|
|
41739
|
+
}
|
|
41740
|
+
}
|
|
40318
41741
|
const sendProc = Bun.spawn(["tmux", "send-keys", "-t", target, message, ""], {
|
|
40319
41742
|
stdout: "pipe",
|
|
40320
41743
|
stderr: "pipe"
|
|
@@ -40346,7 +41769,10 @@ async function executeDispatch(dispatch, opts = {}, db) {
|
|
|
40346
41769
|
}
|
|
40347
41770
|
const delayMs = dispatch.delay_ms ?? calculateDelay(message);
|
|
40348
41771
|
try {
|
|
40349
|
-
await sendToTmux(dispatch.target_window, message, delayMs,
|
|
41772
|
+
await sendToTmux(dispatch.target_window, message, delayMs, {
|
|
41773
|
+
dryRun: opts.dryRun ?? false,
|
|
41774
|
+
confirmBusy: opts.confirmBusy ?? false
|
|
41775
|
+
});
|
|
40350
41776
|
createDispatchLog({
|
|
40351
41777
|
dispatch_id: dispatch.id,
|
|
40352
41778
|
target_window: dispatch.target_window,
|
|
@@ -40404,7 +41830,7 @@ async function dispatchToMultiple(input, opts = {}, db) {
|
|
|
40404
41830
|
init_redaction();
|
|
40405
41831
|
|
|
40406
41832
|
// src/lib/retention-cleanup.ts
|
|
40407
|
-
import { existsSync as
|
|
41833
|
+
import { existsSync as existsSync27, unlinkSync as unlinkSync2 } from "fs";
|
|
40408
41834
|
init_database();
|
|
40409
41835
|
var RETENTION_CLEANUP_CONFIRMATION = "delete-local-retention-data";
|
|
40410
41836
|
var ALL_SCOPES = ["comments", "runs", "verifications", "expired_artifacts"];
|
|
@@ -40616,7 +42042,7 @@ function applyRetentionCleanup(input, db) {
|
|
|
40616
42042
|
for (const artifact of report.candidates.artifact_files) {
|
|
40617
42043
|
try {
|
|
40618
42044
|
const path = artifactStorePath(artifact.relative_path);
|
|
40619
|
-
if (!
|
|
42045
|
+
if (!existsSync27(path)) {
|
|
40620
42046
|
report.warnings.push(`stored artifact already missing: ${artifact.relative_path}`);
|
|
40621
42047
|
continue;
|
|
40622
42048
|
}
|
|
@@ -40854,8 +42280,8 @@ function renderScalePerformanceReportMarkdown(report) {
|
|
|
40854
42280
|
init_database();
|
|
40855
42281
|
init_migrations();
|
|
40856
42282
|
init_schema();
|
|
40857
|
-
import { chmodSync, copyFileSync as copyFileSync2, existsSync as
|
|
40858
|
-
import { basename as basename6, dirname as dirname18, join as
|
|
42283
|
+
import { chmodSync, copyFileSync as copyFileSync2, existsSync as existsSync28, mkdirSync as mkdirSync21, statSync as statSync10 } from "fs";
|
|
42284
|
+
import { basename as basename6, dirname as dirname18, join as join26 } from "path";
|
|
40859
42285
|
var REQUIRED_TABLES2 = [
|
|
40860
42286
|
"_migrations",
|
|
40861
42287
|
"projects",
|
|
@@ -40962,7 +42388,7 @@ function findMissingProjectRoots(db) {
|
|
|
40962
42388
|
continue;
|
|
40963
42389
|
if (!row.path.startsWith("/"))
|
|
40964
42390
|
continue;
|
|
40965
|
-
if (!
|
|
42391
|
+
if (!existsSync28(row.path))
|
|
40966
42392
|
missing++;
|
|
40967
42393
|
}
|
|
40968
42394
|
return missing;
|
|
@@ -41022,16 +42448,16 @@ function databasePermissionsAreUnsafe(dbPath) {
|
|
|
41022
42448
|
function createBackup(dbPath) {
|
|
41023
42449
|
if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
|
|
41024
42450
|
return;
|
|
41025
|
-
if (!
|
|
42451
|
+
if (!existsSync28(dbPath))
|
|
41026
42452
|
return;
|
|
41027
42453
|
const stamp = now().replace(/[:.]/g, "-");
|
|
41028
|
-
const backupDir =
|
|
42454
|
+
const backupDir = join26(dirname18(dbPath), `${basename6(dbPath)}.backup-${stamp}`);
|
|
41029
42455
|
const files = [];
|
|
41030
42456
|
mkdirSync21(backupDir, { recursive: true });
|
|
41031
42457
|
for (const source9 of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
41032
|
-
if (!
|
|
42458
|
+
if (!existsSync28(source9))
|
|
41033
42459
|
continue;
|
|
41034
|
-
const target =
|
|
42460
|
+
const target = join26(backupDir, basename6(source9));
|
|
41035
42461
|
copyFileSync2(source9, target);
|
|
41036
42462
|
files.push(target);
|
|
41037
42463
|
}
|
|
@@ -41320,7 +42746,7 @@ function upsertCapacityProfile(input) {
|
|
|
41320
42746
|
const store = readStore2();
|
|
41321
42747
|
const key = profileKey(agentId, projectId);
|
|
41322
42748
|
const existing = store.profiles[key];
|
|
41323
|
-
const
|
|
42749
|
+
const now4 = timestamp2();
|
|
41324
42750
|
const profile = {
|
|
41325
42751
|
id: existing?.id ?? key,
|
|
41326
42752
|
agent_id: agentId,
|
|
@@ -41328,8 +42754,8 @@ function upsertCapacityProfile(input) {
|
|
|
41328
42754
|
minutes_per_day: assertMinutes(input.minutes_per_day),
|
|
41329
42755
|
working_days: normalizeWorkingDays(input.working_days),
|
|
41330
42756
|
effective_from: cleanString2(input.effective_from),
|
|
41331
|
-
created_at: existing?.created_at ??
|
|
41332
|
-
updated_at:
|
|
42757
|
+
created_at: existing?.created_at ?? now4,
|
|
42758
|
+
updated_at: now4
|
|
41333
42759
|
};
|
|
41334
42760
|
store.profiles[key] = profile;
|
|
41335
42761
|
writeStore2(store);
|
|
@@ -41726,6 +43152,8 @@ export {
|
|
|
41726
43152
|
uuid,
|
|
41727
43153
|
upsertWorkspaceTrustProfile,
|
|
41728
43154
|
upsertVerificationProvider,
|
|
43155
|
+
upsertTesterIssueReports,
|
|
43156
|
+
upsertTesterIssueReport,
|
|
41729
43157
|
upsertTerminalNotificationRule,
|
|
41730
43158
|
upsertSecretSafetyConfig,
|
|
41731
43159
|
upsertRunnerSandboxProfile,
|
|
@@ -41937,6 +43365,7 @@ export {
|
|
|
41937
43365
|
recordTaskReview,
|
|
41938
43366
|
recordFilesTouched,
|
|
41939
43367
|
recordEnvironmentSnapshot,
|
|
43368
|
+
readTesterIssueReportsPayload,
|
|
41940
43369
|
readLocalBackupFile,
|
|
41941
43370
|
readEnvironmentSnapshot,
|
|
41942
43371
|
readBundleFile,
|
|
@@ -41975,6 +43404,7 @@ export {
|
|
|
41975
43404
|
parseCiLog,
|
|
41976
43405
|
now,
|
|
41977
43406
|
notifyUpcomingDeadlines2 as notifyUpcomingDeadlines,
|
|
43407
|
+
normalizeTesterIssueReport,
|
|
41978
43408
|
normalizeScope,
|
|
41979
43409
|
normalizeGeneratedAgentNames,
|
|
41980
43410
|
normalizeFeatureManifest,
|
|
@@ -42363,6 +43793,7 @@ export {
|
|
|
42363
43793
|
formatAgentWorkflowDemoReport,
|
|
42364
43794
|
formatActivityRecordText,
|
|
42365
43795
|
finishTaskRun,
|
|
43796
|
+
fingerprintTesterIssueReport,
|
|
42366
43797
|
fingerprintInboxInput,
|
|
42367
43798
|
findTasksByFile,
|
|
42368
43799
|
findTaskByCommit,
|
|
@@ -42422,7 +43853,7 @@ export {
|
|
|
42422
43853
|
emitLocalEventHooksQuiet,
|
|
42423
43854
|
emitLocalEventHooks,
|
|
42424
43855
|
downloadRunArtifactsFromS3,
|
|
42425
|
-
dispatchWebhook,
|
|
43856
|
+
dispatchWebhook2 as dispatchWebhook,
|
|
42426
43857
|
dispatchToMultiple,
|
|
42427
43858
|
dismissReminder,
|
|
42428
43859
|
discoverVerificationProviderCapabilities,
|
|
@@ -42665,6 +44096,9 @@ export {
|
|
|
42665
44096
|
TODOS_CLI_MCP_PARITY,
|
|
42666
44097
|
TODOS_CAPABILITIES,
|
|
42667
44098
|
TODOS_API_ROUTES,
|
|
44099
|
+
TESTERS_ISSUE_REPORT_SCHEMA_VERSION,
|
|
44100
|
+
TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
|
|
44101
|
+
TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION,
|
|
42668
44102
|
TASK_STATUSES,
|
|
42669
44103
|
TASK_SCHEDULING_SCHEMA,
|
|
42670
44104
|
TASK_PRIORITIES,
|