@hasna/todos 0.15.19 → 0.15.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cloud-router.d.ts +20 -1
- package/dist/cli/cloud-router.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/index.js +851 -47
- package/dist/cli/stage-a.d.ts +21 -10
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +228 -16
- package/dist/db/audit.d.ts +8 -0
- package/dist/db/audit.d.ts.map +1 -1
- package/dist/db/task-lifecycle.d.ts +7 -1
- package/dist/db/task-lifecycle.d.ts.map +1 -1
- package/dist/db/tasks.d.ts +1 -1
- package/dist/db/tasks.d.ts.map +1 -1
- package/dist/index.js +479 -20
- package/dist/lib/cli-help.d.ts +3 -2
- package/dist/lib/cli-help.d.ts.map +1 -1
- package/dist/lib/stale-lock-handoff.d.ts +25 -0
- package/dist/lib/stale-lock-handoff.d.ts.map +1 -0
- package/dist/mcp/index.js +702 -36
- package/dist/mcp.js +3 -1
- package/dist/project-registration.js +1147 -686
- package/dist/registry.js +228 -16
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/index.js +7 -0
- package/dist/sdk/v1.generated.d.ts +27 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +1035 -369
- package/dist/server/openapi.d.ts +182 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/audit-history-import.d.ts +14 -0
- package/dist/storage/audit-history-import.d.ts.map +1 -0
- package/dist/storage/interfaces.d.ts +12 -1
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/local-sqlite.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
- package/dist/storage.js +476 -19
- package/dist/task-manifest.js +11 -1
- package/dist/types/index.d.ts +43 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +3 -1
|
@@ -117,7 +117,7 @@ function isBlockingDependencyStatus(status) {
|
|
|
117
117
|
function isTerminalStatus(status) {
|
|
118
118
|
return status === "completed" || status === "failed" || status === "cancelled";
|
|
119
119
|
}
|
|
120
|
-
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
|
|
120
|
+
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, StaleLockHandoffError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
|
|
121
121
|
var init_types = __esm(() => {
|
|
122
122
|
TASK_STATUSES = [
|
|
123
123
|
"pending",
|
|
@@ -240,6 +240,16 @@ var init_types = __esm(() => {
|
|
|
240
240
|
this.name = "LockError";
|
|
241
241
|
}
|
|
242
242
|
};
|
|
243
|
+
StaleLockHandoffError = class StaleLockHandoffError extends Error {
|
|
244
|
+
code;
|
|
245
|
+
details;
|
|
246
|
+
constructor(code, message, details = {}) {
|
|
247
|
+
super(message);
|
|
248
|
+
this.code = code;
|
|
249
|
+
this.details = details;
|
|
250
|
+
this.name = "StaleLockHandoffError";
|
|
251
|
+
}
|
|
252
|
+
};
|
|
243
253
|
AgentNotFoundError = class AgentNotFoundError extends Error {
|
|
244
254
|
agentId;
|
|
245
255
|
static code = "AGENT_NOT_FOUND";
|
|
@@ -624,225 +634,593 @@ var init_redaction = __esm(() => {
|
|
|
624
634
|
REDACTION_PLACEHOLDER = String.raw`\[REDACTED(?:_[A-Z_]+)?\]`;
|
|
625
635
|
});
|
|
626
636
|
|
|
627
|
-
// src/
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
receipt_id TEXT PRIMARY KEY,
|
|
632
|
-
authority TEXT NOT NULL CHECK(authority = 'todos'),
|
|
633
|
-
route TEXT NOT NULL,
|
|
634
|
-
package_version TEXT NOT NULL,
|
|
635
|
-
authority_id TEXT NOT NULL,
|
|
636
|
-
tenant_id TEXT NOT NULL,
|
|
637
|
-
corpus_id TEXT NOT NULL,
|
|
638
|
-
operation_id TEXT NOT NULL,
|
|
639
|
-
step_id TEXT NOT NULL,
|
|
640
|
-
resource_kind TEXT NOT NULL CHECK(resource_kind IN ('project', 'task_list')),
|
|
641
|
-
direction TEXT NOT NULL CHECK(direction IN ('forward', 'inverse')),
|
|
642
|
-
target_selector TEXT NOT NULL,
|
|
643
|
-
idempotency_key TEXT NOT NULL,
|
|
644
|
-
request_digest TEXT NOT NULL,
|
|
645
|
-
precondition_digest TEXT NOT NULL,
|
|
646
|
-
normalized_call_digest TEXT NOT NULL,
|
|
647
|
-
outcome TEXT NOT NULL CHECK(outcome IN (
|
|
648
|
-
'accepted', 'duplicate_of_accepted', 'terminal_nonacceptance'
|
|
649
|
-
)),
|
|
650
|
-
reason TEXT,
|
|
651
|
-
target_id TEXT,
|
|
652
|
-
result_revision TEXT,
|
|
653
|
-
result_digest TEXT,
|
|
654
|
-
duplicate_of_receipt_id TEXT,
|
|
655
|
-
accepted_receipt_id TEXT,
|
|
656
|
-
created_by_operation INTEGER NOT NULL CHECK(created_by_operation IN (0, 1)),
|
|
657
|
-
created_at TEXT NOT NULL
|
|
658
|
-
);
|
|
659
|
-
|
|
660
|
-
CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_lookup
|
|
661
|
-
ON todos_project_registration_receipts (
|
|
662
|
-
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
663
|
-
resource_kind, direction, idempotency_key
|
|
664
|
-
);
|
|
665
|
-
CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_step
|
|
666
|
-
ON todos_project_registration_receipts (
|
|
667
|
-
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
668
|
-
resource_kind, direction, outcome
|
|
669
|
-
);
|
|
670
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_accepted_step
|
|
671
|
-
ON todos_project_registration_receipts (
|
|
672
|
-
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
673
|
-
resource_kind, direction
|
|
674
|
-
)
|
|
675
|
-
WHERE outcome = 'accepted';
|
|
676
|
-
CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_target
|
|
677
|
-
ON todos_project_registration_receipts (
|
|
678
|
-
authority_id, tenant_id, corpus_id, resource_kind, target_id
|
|
679
|
-
);
|
|
680
|
-
|
|
681
|
-
CREATE TABLE IF NOT EXISTS todos_project_registration_bindings (
|
|
682
|
-
authority_id TEXT NOT NULL,
|
|
683
|
-
tenant_id TEXT NOT NULL,
|
|
684
|
-
corpus_id TEXT NOT NULL,
|
|
685
|
-
resource_kind TEXT NOT NULL CHECK(resource_kind IN ('project', 'task_list')),
|
|
686
|
-
target_selector TEXT NOT NULL,
|
|
687
|
-
operation_id TEXT NOT NULL,
|
|
688
|
-
step_id TEXT NOT NULL,
|
|
689
|
-
direction TEXT NOT NULL CHECK(direction = 'forward'),
|
|
690
|
-
idempotency_key TEXT NOT NULL,
|
|
691
|
-
request_digest TEXT NOT NULL,
|
|
692
|
-
precondition_digest TEXT NOT NULL,
|
|
693
|
-
normalized_call_digest TEXT NOT NULL,
|
|
694
|
-
state TEXT NOT NULL CHECK(state IN (
|
|
695
|
-
'pending', 'accepted', 'terminal_nonacceptance', 'removed'
|
|
696
|
-
)),
|
|
697
|
-
target_id TEXT,
|
|
698
|
-
accepted_receipt_id TEXT,
|
|
699
|
-
result_revision TEXT,
|
|
700
|
-
result_digest TEXT,
|
|
701
|
-
removed_receipt_id TEXT,
|
|
702
|
-
created_at TEXT NOT NULL,
|
|
703
|
-
updated_at TEXT NOT NULL,
|
|
704
|
-
PRIMARY KEY(
|
|
705
|
-
authority_id, tenant_id, corpus_id, resource_kind, target_selector
|
|
706
|
-
),
|
|
707
|
-
UNIQUE(accepted_receipt_id)
|
|
708
|
-
);
|
|
709
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_todos_project_registration_binding_target
|
|
710
|
-
ON todos_project_registration_bindings(
|
|
711
|
-
authority_id, tenant_id, corpus_id, resource_kind, target_id
|
|
712
|
-
)
|
|
713
|
-
WHERE target_id IS NOT NULL;
|
|
714
|
-
|
|
715
|
-
CREATE TRIGGER IF NOT EXISTS todos_project_registration_receipts_immutable_update
|
|
716
|
-
BEFORE UPDATE ON todos_project_registration_receipts
|
|
717
|
-
BEGIN
|
|
718
|
-
SELECT RAISE(ABORT, 'todos project registration receipts are immutable');
|
|
719
|
-
END;
|
|
720
|
-
|
|
721
|
-
CREATE TRIGGER IF NOT EXISTS todos_project_registration_receipts_immutable_delete
|
|
722
|
-
BEFORE DELETE ON todos_project_registration_receipts
|
|
723
|
-
BEGIN
|
|
724
|
-
SELECT RAISE(ABORT, 'todos project registration receipts are immutable');
|
|
725
|
-
END;
|
|
726
|
-
`;
|
|
637
|
+
// src/lib/secret-redaction.ts
|
|
638
|
+
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
|
|
639
|
+
function registerCustomRedactor(fn) {
|
|
640
|
+
customRedactors.push(fn);
|
|
727
641
|
}
|
|
728
|
-
function
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
direction text NOT NULL CHECK(direction IN ('forward', 'inverse')),
|
|
742
|
-
target_selector text NOT NULL,
|
|
743
|
-
idempotency_key text NOT NULL,
|
|
744
|
-
request_digest text NOT NULL,
|
|
745
|
-
precondition_digest text NOT NULL,
|
|
746
|
-
normalized_call_digest text NOT NULL,
|
|
747
|
-
outcome text NOT NULL CHECK(outcome IN (
|
|
748
|
-
'accepted', 'duplicate_of_accepted', 'terminal_nonacceptance'
|
|
749
|
-
)),
|
|
750
|
-
reason text,
|
|
751
|
-
target_id text,
|
|
752
|
-
result_revision text,
|
|
753
|
-
result_digest text,
|
|
754
|
-
duplicate_of_receipt_id text,
|
|
755
|
-
accepted_receipt_id text,
|
|
756
|
-
created_by_operation boolean NOT NULL,
|
|
757
|
-
created_at timestamptz NOT NULL
|
|
758
|
-
)`,
|
|
759
|
-
`CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_lookup_idx
|
|
760
|
-
ON todos_project_registration_receipts (
|
|
761
|
-
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
762
|
-
resource_kind, direction, idempotency_key
|
|
763
|
-
)`,
|
|
764
|
-
`CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_step_idx
|
|
765
|
-
ON todos_project_registration_receipts (
|
|
766
|
-
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
767
|
-
resource_kind, direction, outcome
|
|
768
|
-
)`,
|
|
769
|
-
`CREATE UNIQUE INDEX IF NOT EXISTS todos_project_registration_receipts_accepted_step_uidx
|
|
770
|
-
ON todos_project_registration_receipts (
|
|
771
|
-
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
772
|
-
resource_kind, direction
|
|
773
|
-
)
|
|
774
|
-
WHERE outcome = 'accepted'`,
|
|
775
|
-
`CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_target_idx
|
|
776
|
-
ON todos_project_registration_receipts (
|
|
777
|
-
authority_id, tenant_id, corpus_id, resource_kind, target_id
|
|
778
|
-
)`,
|
|
779
|
-
`CREATE TABLE IF NOT EXISTS todos_project_registration_bindings (
|
|
780
|
-
authority_id text NOT NULL,
|
|
781
|
-
tenant_id text NOT NULL,
|
|
782
|
-
corpus_id text NOT NULL,
|
|
783
|
-
resource_kind text NOT NULL CHECK(resource_kind IN ('project', 'task_list')),
|
|
784
|
-
target_selector text NOT NULL,
|
|
785
|
-
operation_id text NOT NULL,
|
|
786
|
-
step_id text NOT NULL,
|
|
787
|
-
direction text NOT NULL CHECK(direction = 'forward'),
|
|
788
|
-
idempotency_key text NOT NULL,
|
|
789
|
-
request_digest text NOT NULL,
|
|
790
|
-
precondition_digest text NOT NULL,
|
|
791
|
-
normalized_call_digest text NOT NULL,
|
|
792
|
-
state text NOT NULL CHECK(state IN (
|
|
793
|
-
'pending', 'accepted', 'terminal_nonacceptance', 'removed'
|
|
794
|
-
)),
|
|
795
|
-
target_id text,
|
|
796
|
-
accepted_receipt_id text UNIQUE,
|
|
797
|
-
result_revision text,
|
|
798
|
-
result_digest text,
|
|
799
|
-
removed_receipt_id text,
|
|
800
|
-
created_at timestamptz NOT NULL,
|
|
801
|
-
updated_at timestamptz NOT NULL,
|
|
802
|
-
PRIMARY KEY(
|
|
803
|
-
authority_id, tenant_id, corpus_id, resource_kind, target_selector
|
|
804
|
-
)
|
|
805
|
-
)`,
|
|
806
|
-
`CREATE UNIQUE INDEX IF NOT EXISTS todos_project_registration_binding_target_uidx
|
|
807
|
-
ON todos_project_registration_bindings(
|
|
808
|
-
authority_id, tenant_id, corpus_id, resource_kind, target_id
|
|
809
|
-
)
|
|
810
|
-
WHERE target_id IS NOT NULL`,
|
|
811
|
-
`CREATE OR REPLACE FUNCTION todos_project_registration_receipts_immutable()
|
|
812
|
-
RETURNS trigger
|
|
813
|
-
LANGUAGE plpgsql
|
|
814
|
-
AS $$
|
|
815
|
-
BEGIN
|
|
816
|
-
RAISE EXCEPTION 'todos project registration receipts are immutable';
|
|
817
|
-
END;
|
|
818
|
-
$$`,
|
|
819
|
-
`DROP TRIGGER IF EXISTS todos_project_registration_receipts_immutable
|
|
820
|
-
ON todos_project_registration_receipts`,
|
|
821
|
-
`CREATE TRIGGER todos_project_registration_receipts_immutable
|
|
822
|
-
BEFORE UPDATE OR DELETE ON todos_project_registration_receipts
|
|
823
|
-
FOR EACH ROW EXECUTE FUNCTION todos_project_registration_receipts_immutable()`
|
|
642
|
+
function resetCustomRedactors() {
|
|
643
|
+
customRedactors = [];
|
|
644
|
+
}
|
|
645
|
+
function isAllowlisted(text, match, allowlist) {
|
|
646
|
+
const context = text.slice(Math.max(0, text.indexOf(match) - 20), text.indexOf(match) + match.length + 20);
|
|
647
|
+
return allowlist.some((re) => re.test(context) || re.test(match));
|
|
648
|
+
}
|
|
649
|
+
function scanTextForSecrets(text, options = {}) {
|
|
650
|
+
const allowlist = [...DEFAULT_ALLOWLIST, ...options.allowlist ?? []];
|
|
651
|
+
const matches = [];
|
|
652
|
+
const patterns = [
|
|
653
|
+
...DEFAULT_PATTERNS,
|
|
654
|
+
...options.custom_patterns?.map((p, i) => ({ name: `custom_${i}`, pattern: p })) ?? []
|
|
824
655
|
];
|
|
656
|
+
for (const { name, pattern, allowlist_ok } of patterns) {
|
|
657
|
+
const re = new RegExp(pattern.source, pattern.flags);
|
|
658
|
+
let m;
|
|
659
|
+
while ((m = re.exec(text)) !== null) {
|
|
660
|
+
const match = m[0];
|
|
661
|
+
if (allowlist_ok && isAllowlisted(text, match, allowlist))
|
|
662
|
+
continue;
|
|
663
|
+
if (isAllowlisted(text, match, allowlist))
|
|
664
|
+
continue;
|
|
665
|
+
const line = text.slice(0, m.index).split(`
|
|
666
|
+
`).length;
|
|
667
|
+
matches.push({ pattern: name, match: match.slice(0, 12) + (match.length > 12 ? "\u2026" : ""), index: m.index, line });
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return {
|
|
671
|
+
schema_version: SECRET_REDACTION_SCHEMA,
|
|
672
|
+
clean: matches.length === 0,
|
|
673
|
+
matches
|
|
674
|
+
};
|
|
825
675
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
676
|
+
function redactText(text, options = {}) {
|
|
677
|
+
const placeholder = options.placeholder ?? REDACTION_PLACEHOLDER2;
|
|
678
|
+
let out = text;
|
|
679
|
+
for (const { pattern, allowlist_ok } of DEFAULT_PATTERNS) {
|
|
680
|
+
out = out.replace(new RegExp(pattern.source, pattern.flags), (match) => {
|
|
681
|
+
if (allowlist_ok && isAllowlisted(out, match, [...DEFAULT_ALLOWLIST, ...options.allowlist ?? []])) {
|
|
682
|
+
return match;
|
|
683
|
+
}
|
|
684
|
+
return placeholder;
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
for (const custom of options.custom_patterns ?? []) {
|
|
688
|
+
out = out.replace(custom, placeholder);
|
|
689
|
+
}
|
|
690
|
+
for (const fn of customRedactors) {
|
|
691
|
+
out = fn(out);
|
|
692
|
+
}
|
|
693
|
+
return out;
|
|
694
|
+
}
|
|
695
|
+
function scanAndRedactText(text, options = {}) {
|
|
696
|
+
const scan = scanTextForSecrets(text, options);
|
|
697
|
+
return {
|
|
698
|
+
...scan,
|
|
699
|
+
redacted_text: redactText(text, options)
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
function scanFileForSecrets(path, options = {}) {
|
|
703
|
+
if (!existsSync3(path))
|
|
704
|
+
throw new Error(`File not found: ${path}`);
|
|
705
|
+
const content = readFileSync2(path, "utf8");
|
|
706
|
+
return scanAndRedactText(content, options);
|
|
707
|
+
}
|
|
708
|
+
function safeStringify(value, space) {
|
|
709
|
+
const redacted = typeof value === "object" && value !== null && !Array.isArray(value) ? redactValue(value) : value;
|
|
710
|
+
const json = JSON.stringify(redacted, null, space);
|
|
711
|
+
return redactText(json);
|
|
712
|
+
}
|
|
713
|
+
function assertNoSecrets(text, context) {
|
|
714
|
+
const scan = scanTextForSecrets(text);
|
|
715
|
+
if (!scan.clean) {
|
|
716
|
+
const summary = scan.matches.map((m) => `${m.pattern}@${m.line ?? m.index}`).join(", ");
|
|
717
|
+
throw new Error(`Secret pattern detected${context ? ` in ${context}` : ""}: ${summary}`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
function redactCommentContent(content) {
|
|
721
|
+
return redactText(content);
|
|
722
|
+
}
|
|
723
|
+
function redactHandoffPayload(payload) {
|
|
724
|
+
return redactValue(payload);
|
|
725
|
+
}
|
|
726
|
+
function redactExportRecord(record) {
|
|
727
|
+
const base = redactValue(record);
|
|
728
|
+
for (const [key, value] of Object.entries(base)) {
|
|
729
|
+
if (typeof value === "string") {
|
|
730
|
+
base[key] = redactText(value);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return base;
|
|
734
|
+
}
|
|
735
|
+
function getDefaultSecretPatterns() {
|
|
736
|
+
return DEFAULT_PATTERNS.map((p) => ({ name: p.name, source: p.pattern.source }));
|
|
737
|
+
}
|
|
738
|
+
var SECRET_REDACTION_SCHEMA, REDACTION_PLACEHOLDER2 = "[REDACTED]", DEFAULT_PATTERNS, DEFAULT_ALLOWLIST, customRedactors;
|
|
739
|
+
var init_secret_redaction = __esm(() => {
|
|
740
|
+
init_redaction();
|
|
741
|
+
SECRET_REDACTION_SCHEMA = ["todos", "secret_redaction", "v1"].join(".");
|
|
742
|
+
DEFAULT_PATTERNS = [
|
|
743
|
+
{ name: "openai_sk", pattern: /\bsk-[a-zA-Z0-9]{10,}\b/g },
|
|
744
|
+
{ name: "github_pat", pattern: /\bghp_[a-zA-Z0-9]{20,}\b/g },
|
|
745
|
+
{ name: "github_oauth", pattern: /\bgho_[a-zA-Z0-9]{20,}\b/g },
|
|
746
|
+
{ name: "aws_access_key", pattern: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
747
|
+
{ name: "bearer_token", pattern: /\bBearer\s+[a-zA-Z0-9\-._~+/]+=*\b/gi },
|
|
748
|
+
{ name: "jwt", pattern: /\beyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g },
|
|
749
|
+
{ name: "private_key_block", pattern: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g },
|
|
750
|
+
{ name: "generic_api_key", pattern: /\b(?:api[_-]?key|secret|token|password)\s*[:=]\s*['"]?[a-zA-Z0-9\-._]{8,}['"]?/gi, allowlist_ok: true }
|
|
751
|
+
];
|
|
752
|
+
DEFAULT_ALLOWLIST = [
|
|
753
|
+
/\[REDACTED\]/,
|
|
754
|
+
/example\.com/i,
|
|
755
|
+
/your-api-key-here/i,
|
|
756
|
+
/sk-test/i,
|
|
757
|
+
/ghp_xxx/i
|
|
758
|
+
];
|
|
759
|
+
customRedactors = [];
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
// src/lib/prewrite-secrets.ts
|
|
763
|
+
function mergeFindings(findings) {
|
|
764
|
+
const byPattern = new Map;
|
|
765
|
+
for (const finding of findings) {
|
|
766
|
+
const existing = byPattern.get(finding.pattern);
|
|
767
|
+
if (!existing) {
|
|
768
|
+
byPattern.set(finding.pattern, {
|
|
769
|
+
pattern: finding.pattern,
|
|
770
|
+
count: finding.count,
|
|
771
|
+
lines: Array.from(new Set(finding.lines)).sort((a, b) => a - b)
|
|
772
|
+
});
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
existing.count += finding.count;
|
|
776
|
+
existing.lines = Array.from(new Set([...existing.lines, ...finding.lines])).sort((a, b) => a - b);
|
|
777
|
+
}
|
|
778
|
+
return [...byPattern.values()].sort((left, right) => left.pattern.localeCompare(right.pattern));
|
|
779
|
+
}
|
|
780
|
+
function redactPreWriteText(value) {
|
|
781
|
+
return redactText(redactEvidenceText(value));
|
|
782
|
+
}
|
|
783
|
+
function scanPreWriteText(value, context = "text") {
|
|
784
|
+
const redacted = redactPreWriteText(value);
|
|
785
|
+
if (redacted === value) {
|
|
786
|
+
return {
|
|
787
|
+
schema_version: PREWRITE_SECRET_SCAN_SCHEMA,
|
|
788
|
+
clean: true,
|
|
789
|
+
context,
|
|
790
|
+
findings: []
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
const structural = scanTextForSecrets(value).matches.map((match) => ({
|
|
794
|
+
pattern: match.pattern,
|
|
795
|
+
count: 1,
|
|
796
|
+
lines: match.line ? [match.line] : []
|
|
797
|
+
}));
|
|
798
|
+
const configured = listSecretFindings(value).map((finding) => ({
|
|
799
|
+
pattern: finding.pattern,
|
|
800
|
+
count: finding.count,
|
|
801
|
+
lines: []
|
|
802
|
+
}));
|
|
803
|
+
return {
|
|
804
|
+
schema_version: PREWRITE_SECRET_SCAN_SCHEMA,
|
|
805
|
+
clean: false,
|
|
806
|
+
context,
|
|
807
|
+
findings: mergeFindings([...structural, ...configured])
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
function assertPreWriteTextClean(value, context = "text") {
|
|
811
|
+
const scan = scanPreWriteText(value, context);
|
|
812
|
+
if (!scan.clean)
|
|
813
|
+
throw new PreWriteSecretError(context, scan.findings);
|
|
814
|
+
}
|
|
815
|
+
function sanitizePreWriteText(value, context = "text", options = {}) {
|
|
816
|
+
const mode = options.mode ?? "redact";
|
|
817
|
+
if (mode === "block") {
|
|
818
|
+
assertPreWriteTextClean(value, context);
|
|
819
|
+
return value;
|
|
820
|
+
}
|
|
821
|
+
return redactPreWriteText(value);
|
|
822
|
+
}
|
|
823
|
+
function sanitizePreWriteValue(value, context = "value", options = {}) {
|
|
824
|
+
if (typeof value === "string")
|
|
825
|
+
return sanitizePreWriteText(value, context, options);
|
|
826
|
+
if (Array.isArray(value)) {
|
|
827
|
+
return value.map((item, index) => sanitizePreWriteValue(item, `${context}[${index}]`, options));
|
|
828
|
+
}
|
|
829
|
+
if (value && typeof value === "object") {
|
|
830
|
+
const sanitized = {};
|
|
831
|
+
for (const [key, child] of Object.entries(value)) {
|
|
832
|
+
const safeKey = sanitizePreWriteText(key, `${context}.$key`, options);
|
|
833
|
+
sanitized[safeKey] = sanitizePreWriteValue(child, `${context}.${safeKey}`, options);
|
|
834
|
+
}
|
|
835
|
+
return redactValue(sanitized);
|
|
836
|
+
}
|
|
837
|
+
return value;
|
|
838
|
+
}
|
|
839
|
+
function sanitizePreWriteJsonString(value, context = "json") {
|
|
840
|
+
if (value === undefined || value === null)
|
|
841
|
+
return null;
|
|
842
|
+
try {
|
|
843
|
+
return JSON.stringify(sanitizePreWriteValue(JSON.parse(value), context));
|
|
844
|
+
} catch {
|
|
845
|
+
return sanitizePreWriteText(value, context);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
var PREWRITE_SECRET_SCAN_SCHEMA, PreWriteSecretError;
|
|
849
|
+
var init_prewrite_secrets = __esm(() => {
|
|
850
|
+
init_redaction();
|
|
851
|
+
init_secret_redaction();
|
|
852
|
+
PREWRITE_SECRET_SCAN_SCHEMA = ["todos", "prewrite_secret_scan", "v1"].join(".");
|
|
853
|
+
PreWriteSecretError = class PreWriteSecretError extends Error {
|
|
854
|
+
context;
|
|
855
|
+
findings;
|
|
856
|
+
constructor(context, findings) {
|
|
857
|
+
const summary = findings.map((finding) => `${finding.pattern}:${finding.count}`).join(", ");
|
|
858
|
+
super(`Secret pattern detected before persistence in ${context}: ${summary}`);
|
|
859
|
+
this.name = "PreWriteSecretError";
|
|
860
|
+
this.context = context;
|
|
861
|
+
this.findings = findings;
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
// src/lib/stale-lock-handoff.ts
|
|
867
|
+
function normalizeExactTaskId(value) {
|
|
868
|
+
if (typeof value !== "string" || !EXACT_TASK_UUID_RE.test(value.trim())) {
|
|
869
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_TASK_ID", "stale-lock handoff requires one exact full task UUID", { task_id: typeof value === "string" ? value : null });
|
|
870
|
+
}
|
|
871
|
+
return value.trim().toLowerCase();
|
|
872
|
+
}
|
|
873
|
+
function requireNonEmptyString(value, field) {
|
|
874
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
875
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", `${field} must be a non-empty string`, { field });
|
|
876
|
+
}
|
|
877
|
+
const trimmed = value.trim();
|
|
878
|
+
if (field === "reason" && trimmed.length > MAX_REASON_LENGTH) {
|
|
879
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", `reason must be at most ${MAX_REASON_LENGTH} characters`, { field, max_length: MAX_REASON_LENGTH });
|
|
880
|
+
}
|
|
881
|
+
return trimmed;
|
|
882
|
+
}
|
|
883
|
+
function requireCanonicalLockVersion(value) {
|
|
884
|
+
if (typeof value !== "string" || !CANONICAL_LOCK_VERSION_RE.test(value)) {
|
|
885
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "expected_lock_version must be the exact canonical locked_at timestamp (YYYY-MM-DDTHH:mm:ss.sssZ)", { field: "expected_lock_version" });
|
|
886
|
+
}
|
|
887
|
+
const parsed = Date.parse(value);
|
|
888
|
+
if (Number.isNaN(parsed) || new Date(parsed).toISOString() !== value) {
|
|
889
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "expected_lock_version must name a real canonical UTC instant", { field: "expected_lock_version" });
|
|
890
|
+
}
|
|
891
|
+
return value;
|
|
892
|
+
}
|
|
893
|
+
function requireStaleThreshold(value) {
|
|
894
|
+
if (!Number.isSafeInteger(value) || Number(value) <= 0) {
|
|
895
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "stale_after_seconds must be a positive safe integer", { field: "stale_after_seconds" });
|
|
896
|
+
}
|
|
897
|
+
return Number(value);
|
|
898
|
+
}
|
|
899
|
+
function prepareStaleLockHandoff(input, options = {}) {
|
|
900
|
+
const taskId = normalizeExactTaskId(input.task_id);
|
|
901
|
+
const actor = requireNonEmptyString(input.actor, "actor");
|
|
902
|
+
const expectedHolder = requireNonEmptyString(input.expected_holder, "expected_holder");
|
|
903
|
+
const newHolder = requireNonEmptyString(input.new_holder, "new_holder");
|
|
904
|
+
const expectedLockVersion = requireCanonicalLockVersion(input.expected_lock_version);
|
|
905
|
+
const staleAfterSeconds = requireStaleThreshold(input.stale_after_seconds);
|
|
906
|
+
const reason = sanitizePreWriteText(requireNonEmptyString(input.reason, "reason"), "stale_lock_handoff.reason").trim();
|
|
907
|
+
if (!reason) {
|
|
908
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "reason must remain non-empty after safety filtering", { field: "reason" });
|
|
909
|
+
}
|
|
910
|
+
if (canonicalAgentRef(actor) !== canonicalAgentRef(newHolder)) {
|
|
911
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_ACTOR_MISMATCH", "new_holder must match the authenticated actor", { actor, new_holder: newHolder });
|
|
912
|
+
}
|
|
913
|
+
if (canonicalAgentRef(expectedHolder) === canonicalAgentRef(newHolder)) {
|
|
914
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "new_holder must differ from expected_holder", { field: "new_holder" });
|
|
915
|
+
}
|
|
916
|
+
const operationTimestamp = options.now ?? new Date().toISOString();
|
|
917
|
+
if (!CANONICAL_LOCK_VERSION_RE.test(operationTimestamp) || new Date(Date.parse(operationTimestamp)).toISOString() !== operationTimestamp) {
|
|
918
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "operation timestamp must be a canonical UTC instant");
|
|
919
|
+
}
|
|
920
|
+
const staleCutoff = new Date(Date.parse(operationTimestamp) - staleAfterSeconds * 1000).toISOString();
|
|
921
|
+
return {
|
|
922
|
+
task_id: taskId,
|
|
923
|
+
actor,
|
|
924
|
+
expected_holder: expectedHolder,
|
|
925
|
+
expected_lock_version: expectedLockVersion,
|
|
926
|
+
stale_after_seconds: staleAfterSeconds,
|
|
927
|
+
new_holder: newHolder,
|
|
928
|
+
reason,
|
|
929
|
+
operation_timestamp: operationTimestamp,
|
|
930
|
+
stale_cutoff: staleCutoff,
|
|
931
|
+
receipt_id: options.receiptId ?? crypto.randomUUID()
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
function buildStaleLockHandoffReceipt(input) {
|
|
935
|
+
return {
|
|
936
|
+
schema_version: STALE_LOCK_HANDOFF_SCHEMA_VERSION,
|
|
937
|
+
receipt_id: input.receipt_id,
|
|
938
|
+
task_id: input.task_id,
|
|
939
|
+
actor: input.actor,
|
|
940
|
+
previous_holder: input.expected_holder,
|
|
941
|
+
previous_lock_version: input.expected_lock_version,
|
|
942
|
+
new_holder: input.new_holder,
|
|
943
|
+
new_lock_version: input.operation_timestamp,
|
|
944
|
+
stale_after_seconds: input.stale_after_seconds,
|
|
945
|
+
stale_cutoff: input.stale_cutoff,
|
|
946
|
+
reason: input.reason,
|
|
947
|
+
created_at: input.operation_timestamp
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
function staleLockHandoffHistory(receipt, machineId) {
|
|
951
|
+
return {
|
|
952
|
+
id: receipt.receipt_id,
|
|
953
|
+
task_id: receipt.task_id,
|
|
954
|
+
action: STALE_LOCK_HANDOFF_ACTION,
|
|
955
|
+
field: STALE_LOCK_HANDOFF_FIELD,
|
|
956
|
+
old_value: JSON.stringify({
|
|
957
|
+
holder: receipt.previous_holder,
|
|
958
|
+
lock_version: receipt.previous_lock_version
|
|
959
|
+
}),
|
|
960
|
+
new_value: JSON.stringify(receipt),
|
|
961
|
+
agent_id: receipt.actor,
|
|
962
|
+
created_at: receipt.created_at,
|
|
963
|
+
machine_id: machineId
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
function throwStaleLockHandoffConflict(task, input) {
|
|
967
|
+
if (!task.locked_by || !task.locked_at) {
|
|
968
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_NOT_LOCKED", `Task ${input.task_id} does not have a complete lock to hand off`, { task_id: input.task_id });
|
|
969
|
+
}
|
|
970
|
+
if (task.locked_at !== input.expected_lock_version) {
|
|
971
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_VERSION_MISMATCH", `Task ${input.task_id} lock version changed`, {
|
|
972
|
+
task_id: input.task_id,
|
|
973
|
+
expected_lock_version: input.expected_lock_version,
|
|
974
|
+
current_lock_version: task.locked_at
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
if (task.locked_by !== input.expected_holder) {
|
|
978
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_HOLDER_MISMATCH", `Task ${input.task_id} lock holder changed`, {
|
|
979
|
+
task_id: input.task_id,
|
|
980
|
+
expected_holder: input.expected_holder,
|
|
981
|
+
current_holder: task.locked_by
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
if (isTerminalStatus(task.status)) {
|
|
985
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_TERMINAL", `Task ${input.task_id} is ${task.status} and cannot transfer a lock`, { task_id: input.task_id, status: task.status });
|
|
986
|
+
}
|
|
987
|
+
if (task.locked_at >= input.stale_cutoff) {
|
|
988
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_NOT_STALE", `Task ${input.task_id} lock is not older than the supplied stale threshold`, {
|
|
989
|
+
task_id: input.task_id,
|
|
990
|
+
current_lock_version: task.locked_at,
|
|
991
|
+
stale_cutoff: input.stale_cutoff
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_CONFLICT", `Task ${input.task_id} changed during stale-lock handoff`, { task_id: input.task_id });
|
|
995
|
+
}
|
|
996
|
+
var STALE_LOCK_HANDOFF_SCHEMA_VERSION = "todos.stale-lock-handoff.v1", STALE_LOCK_HANDOFF_ACTION = "stale_lock_handoff", STALE_LOCK_HANDOFF_FIELD = "lock", EXACT_TASK_UUID_RE, CANONICAL_LOCK_VERSION_RE, MAX_REASON_LENGTH = 4096;
|
|
997
|
+
var init_stale_lock_handoff = __esm(() => {
|
|
998
|
+
init_types();
|
|
999
|
+
init_creator_identity();
|
|
1000
|
+
init_prewrite_secrets();
|
|
1001
|
+
EXACT_TASK_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1002
|
+
CANONICAL_LOCK_VERSION_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
// src/project-registration/schema.ts
|
|
1006
|
+
function sqliteTodosProjectRegistrationSchemaSql() {
|
|
1007
|
+
return `
|
|
1008
|
+
CREATE TABLE IF NOT EXISTS todos_project_registration_receipts (
|
|
1009
|
+
receipt_id TEXT PRIMARY KEY,
|
|
1010
|
+
authority TEXT NOT NULL CHECK(authority = 'todos'),
|
|
1011
|
+
route TEXT NOT NULL,
|
|
1012
|
+
package_version TEXT NOT NULL,
|
|
1013
|
+
authority_id TEXT NOT NULL,
|
|
1014
|
+
tenant_id TEXT NOT NULL,
|
|
1015
|
+
corpus_id TEXT NOT NULL,
|
|
1016
|
+
operation_id TEXT NOT NULL,
|
|
1017
|
+
step_id TEXT NOT NULL,
|
|
1018
|
+
resource_kind TEXT NOT NULL CHECK(resource_kind IN ('project', 'task_list')),
|
|
1019
|
+
direction TEXT NOT NULL CHECK(direction IN ('forward', 'inverse')),
|
|
1020
|
+
target_selector TEXT NOT NULL,
|
|
1021
|
+
idempotency_key TEXT NOT NULL,
|
|
1022
|
+
request_digest TEXT NOT NULL,
|
|
1023
|
+
precondition_digest TEXT NOT NULL,
|
|
1024
|
+
normalized_call_digest TEXT NOT NULL,
|
|
1025
|
+
outcome TEXT NOT NULL CHECK(outcome IN (
|
|
1026
|
+
'accepted', 'duplicate_of_accepted', 'terminal_nonacceptance'
|
|
1027
|
+
)),
|
|
1028
|
+
reason TEXT,
|
|
1029
|
+
target_id TEXT,
|
|
1030
|
+
result_revision TEXT,
|
|
1031
|
+
result_digest TEXT,
|
|
1032
|
+
duplicate_of_receipt_id TEXT,
|
|
1033
|
+
accepted_receipt_id TEXT,
|
|
1034
|
+
created_by_operation INTEGER NOT NULL CHECK(created_by_operation IN (0, 1)),
|
|
1035
|
+
created_at TEXT NOT NULL
|
|
1036
|
+
);
|
|
1037
|
+
|
|
1038
|
+
CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_lookup
|
|
1039
|
+
ON todos_project_registration_receipts (
|
|
1040
|
+
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
1041
|
+
resource_kind, direction, idempotency_key
|
|
1042
|
+
);
|
|
1043
|
+
CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_step
|
|
1044
|
+
ON todos_project_registration_receipts (
|
|
1045
|
+
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
1046
|
+
resource_kind, direction, outcome
|
|
1047
|
+
);
|
|
1048
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_accepted_step
|
|
1049
|
+
ON todos_project_registration_receipts (
|
|
1050
|
+
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
1051
|
+
resource_kind, direction
|
|
1052
|
+
)
|
|
1053
|
+
WHERE outcome = 'accepted';
|
|
1054
|
+
CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_target
|
|
1055
|
+
ON todos_project_registration_receipts (
|
|
1056
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_id
|
|
1057
|
+
);
|
|
1058
|
+
|
|
1059
|
+
CREATE TABLE IF NOT EXISTS todos_project_registration_bindings (
|
|
1060
|
+
authority_id TEXT NOT NULL,
|
|
1061
|
+
tenant_id TEXT NOT NULL,
|
|
1062
|
+
corpus_id TEXT NOT NULL,
|
|
1063
|
+
resource_kind TEXT NOT NULL CHECK(resource_kind IN ('project', 'task_list')),
|
|
1064
|
+
target_selector TEXT NOT NULL,
|
|
1065
|
+
operation_id TEXT NOT NULL,
|
|
1066
|
+
step_id TEXT NOT NULL,
|
|
1067
|
+
direction TEXT NOT NULL CHECK(direction = 'forward'),
|
|
1068
|
+
idempotency_key TEXT NOT NULL,
|
|
1069
|
+
request_digest TEXT NOT NULL,
|
|
1070
|
+
precondition_digest TEXT NOT NULL,
|
|
1071
|
+
normalized_call_digest TEXT NOT NULL,
|
|
1072
|
+
state TEXT NOT NULL CHECK(state IN (
|
|
1073
|
+
'pending', 'accepted', 'terminal_nonacceptance', 'removed'
|
|
1074
|
+
)),
|
|
1075
|
+
target_id TEXT,
|
|
1076
|
+
accepted_receipt_id TEXT,
|
|
1077
|
+
result_revision TEXT,
|
|
1078
|
+
result_digest TEXT,
|
|
1079
|
+
removed_receipt_id TEXT,
|
|
1080
|
+
created_at TEXT NOT NULL,
|
|
1081
|
+
updated_at TEXT NOT NULL,
|
|
1082
|
+
PRIMARY KEY(
|
|
1083
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_selector
|
|
1084
|
+
),
|
|
1085
|
+
UNIQUE(accepted_receipt_id)
|
|
1086
|
+
);
|
|
1087
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_todos_project_registration_binding_target
|
|
1088
|
+
ON todos_project_registration_bindings(
|
|
1089
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_id
|
|
1090
|
+
)
|
|
1091
|
+
WHERE target_id IS NOT NULL;
|
|
1092
|
+
|
|
1093
|
+
CREATE TRIGGER IF NOT EXISTS todos_project_registration_receipts_immutable_update
|
|
1094
|
+
BEFORE UPDATE ON todos_project_registration_receipts
|
|
1095
|
+
BEGIN
|
|
1096
|
+
SELECT RAISE(ABORT, 'todos project registration receipts are immutable');
|
|
1097
|
+
END;
|
|
1098
|
+
|
|
1099
|
+
CREATE TRIGGER IF NOT EXISTS todos_project_registration_receipts_immutable_delete
|
|
1100
|
+
BEFORE DELETE ON todos_project_registration_receipts
|
|
1101
|
+
BEGIN
|
|
1102
|
+
SELECT RAISE(ABORT, 'todos project registration receipts are immutable');
|
|
1103
|
+
END;
|
|
1104
|
+
`;
|
|
1105
|
+
}
|
|
1106
|
+
function postgresTodosProjectRegistrationSchemaSql() {
|
|
1107
|
+
return [
|
|
1108
|
+
`CREATE TABLE IF NOT EXISTS todos_project_registration_receipts (
|
|
1109
|
+
receipt_id text PRIMARY KEY,
|
|
1110
|
+
authority text NOT NULL CHECK(authority = 'todos'),
|
|
1111
|
+
route text NOT NULL,
|
|
1112
|
+
package_version text NOT NULL,
|
|
1113
|
+
authority_id text NOT NULL,
|
|
1114
|
+
tenant_id text NOT NULL,
|
|
1115
|
+
corpus_id text NOT NULL,
|
|
1116
|
+
operation_id text NOT NULL,
|
|
1117
|
+
step_id text NOT NULL,
|
|
1118
|
+
resource_kind text NOT NULL CHECK(resource_kind IN ('project', 'task_list')),
|
|
1119
|
+
direction text NOT NULL CHECK(direction IN ('forward', 'inverse')),
|
|
1120
|
+
target_selector text NOT NULL,
|
|
1121
|
+
idempotency_key text NOT NULL,
|
|
1122
|
+
request_digest text NOT NULL,
|
|
1123
|
+
precondition_digest text NOT NULL,
|
|
1124
|
+
normalized_call_digest text NOT NULL,
|
|
1125
|
+
outcome text NOT NULL CHECK(outcome IN (
|
|
1126
|
+
'accepted', 'duplicate_of_accepted', 'terminal_nonacceptance'
|
|
1127
|
+
)),
|
|
1128
|
+
reason text,
|
|
1129
|
+
target_id text,
|
|
1130
|
+
result_revision text,
|
|
1131
|
+
result_digest text,
|
|
1132
|
+
duplicate_of_receipt_id text,
|
|
1133
|
+
accepted_receipt_id text,
|
|
1134
|
+
created_by_operation boolean NOT NULL,
|
|
1135
|
+
created_at timestamptz NOT NULL
|
|
1136
|
+
)`,
|
|
1137
|
+
`CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_lookup_idx
|
|
1138
|
+
ON todos_project_registration_receipts (
|
|
1139
|
+
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
1140
|
+
resource_kind, direction, idempotency_key
|
|
1141
|
+
)`,
|
|
1142
|
+
`CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_step_idx
|
|
1143
|
+
ON todos_project_registration_receipts (
|
|
1144
|
+
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
1145
|
+
resource_kind, direction, outcome
|
|
1146
|
+
)`,
|
|
1147
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS todos_project_registration_receipts_accepted_step_uidx
|
|
1148
|
+
ON todos_project_registration_receipts (
|
|
1149
|
+
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
1150
|
+
resource_kind, direction
|
|
1151
|
+
)
|
|
1152
|
+
WHERE outcome = 'accepted'`,
|
|
1153
|
+
`CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_target_idx
|
|
1154
|
+
ON todos_project_registration_receipts (
|
|
1155
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_id
|
|
1156
|
+
)`,
|
|
1157
|
+
`CREATE TABLE IF NOT EXISTS todos_project_registration_bindings (
|
|
1158
|
+
authority_id text NOT NULL,
|
|
1159
|
+
tenant_id text NOT NULL,
|
|
1160
|
+
corpus_id text NOT NULL,
|
|
1161
|
+
resource_kind text NOT NULL CHECK(resource_kind IN ('project', 'task_list')),
|
|
1162
|
+
target_selector text NOT NULL,
|
|
1163
|
+
operation_id text NOT NULL,
|
|
1164
|
+
step_id text NOT NULL,
|
|
1165
|
+
direction text NOT NULL CHECK(direction = 'forward'),
|
|
1166
|
+
idempotency_key text NOT NULL,
|
|
1167
|
+
request_digest text NOT NULL,
|
|
1168
|
+
precondition_digest text NOT NULL,
|
|
1169
|
+
normalized_call_digest text NOT NULL,
|
|
1170
|
+
state text NOT NULL CHECK(state IN (
|
|
1171
|
+
'pending', 'accepted', 'terminal_nonacceptance', 'removed'
|
|
1172
|
+
)),
|
|
1173
|
+
target_id text,
|
|
1174
|
+
accepted_receipt_id text UNIQUE,
|
|
1175
|
+
result_revision text,
|
|
1176
|
+
result_digest text,
|
|
1177
|
+
removed_receipt_id text,
|
|
1178
|
+
created_at timestamptz NOT NULL,
|
|
1179
|
+
updated_at timestamptz NOT NULL,
|
|
1180
|
+
PRIMARY KEY(
|
|
1181
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_selector
|
|
1182
|
+
)
|
|
1183
|
+
)`,
|
|
1184
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS todos_project_registration_binding_target_uidx
|
|
1185
|
+
ON todos_project_registration_bindings(
|
|
1186
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_id
|
|
1187
|
+
)
|
|
1188
|
+
WHERE target_id IS NOT NULL`,
|
|
1189
|
+
`CREATE OR REPLACE FUNCTION todos_project_registration_receipts_immutable()
|
|
1190
|
+
RETURNS trigger
|
|
1191
|
+
LANGUAGE plpgsql
|
|
1192
|
+
AS $$
|
|
1193
|
+
BEGIN
|
|
1194
|
+
RAISE EXCEPTION 'todos project registration receipts are immutable';
|
|
1195
|
+
END;
|
|
1196
|
+
$$`,
|
|
1197
|
+
`DROP TRIGGER IF EXISTS todos_project_registration_receipts_immutable
|
|
1198
|
+
ON todos_project_registration_receipts`,
|
|
1199
|
+
`CREATE TRIGGER todos_project_registration_receipts_immutable
|
|
1200
|
+
BEFORE UPDATE OR DELETE ON todos_project_registration_receipts
|
|
1201
|
+
FOR EACH ROW EXECUTE FUNCTION todos_project_registration_receipts_immutable()`
|
|
1202
|
+
];
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// src/db/migrations.ts
|
|
1206
|
+
var MIGRATIONS;
|
|
1207
|
+
var init_migrations = __esm(() => {
|
|
1208
|
+
MIGRATIONS = [
|
|
1209
|
+
`
|
|
1210
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
1211
|
+
id TEXT PRIMARY KEY,
|
|
1212
|
+
name TEXT NOT NULL,
|
|
1213
|
+
path TEXT UNIQUE NOT NULL,
|
|
1214
|
+
description TEXT,
|
|
1215
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1216
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1217
|
+
);
|
|
1218
|
+
|
|
1219
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
1220
|
+
id TEXT PRIMARY KEY,
|
|
1221
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
1222
|
+
parent_id TEXT REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1223
|
+
title TEXT NOT NULL,
|
|
846
1224
|
description TEXT,
|
|
847
1225
|
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'in_progress', 'completed', 'failed', 'cancelled')),
|
|
848
1226
|
priority TEXT NOT NULL DEFAULT 'medium' CHECK(priority IN ('low', 'medium', 'high', 'critical')),
|
|
@@ -3935,7 +4313,7 @@ var init_schema = __esm(() => {
|
|
|
3935
4313
|
});
|
|
3936
4314
|
|
|
3937
4315
|
// src/db/machines.ts
|
|
3938
|
-
import { existsSync as
|
|
4316
|
+
import { existsSync as existsSync4 } from "fs";
|
|
3939
4317
|
import { hostname as osHostname, platform as osPlatform, arch as osArch } from "os";
|
|
3940
4318
|
import { resolve } from "path";
|
|
3941
4319
|
import { spawnSync } from "child_process";
|
|
@@ -4145,7 +4523,7 @@ function getMachineTopologyDiagnostics(opts = {}, db, at = new Date) {
|
|
|
4145
4523
|
message: `${project.name} has ${distinctPaths.length} different machine-local paths`
|
|
4146
4524
|
});
|
|
4147
4525
|
}
|
|
4148
|
-
if (localRow && !
|
|
4526
|
+
if (localRow && !existsSync4(localRow.path)) {
|
|
4149
4527
|
pathIssues.push({
|
|
4150
4528
|
type: "path_missing",
|
|
4151
4529
|
project_id: project.id,
|
|
@@ -4156,7 +4534,7 @@ function getMachineTopologyDiagnostics(opts = {}, db, at = new Date) {
|
|
|
4156
4534
|
message: `Local path does not exist on this machine: ${localRow.path}`
|
|
4157
4535
|
});
|
|
4158
4536
|
}
|
|
4159
|
-
if (!localRow && project.path && machineById.has(localMachine.id) && !
|
|
4537
|
+
if (!localRow && project.path && machineById.has(localMachine.id) && !existsSync4(project.path)) {
|
|
4160
4538
|
pathIssues.push({
|
|
4161
4539
|
type: "path_missing",
|
|
4162
4540
|
project_id: project.id,
|
|
@@ -4744,7 +5122,7 @@ __export(exports_database, {
|
|
|
4744
5122
|
LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
|
|
4745
5123
|
});
|
|
4746
5124
|
import { Database } from "bun:sqlite";
|
|
4747
|
-
import { existsSync as
|
|
5125
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
|
|
4748
5126
|
import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
|
|
4749
5127
|
function isInMemoryDb(path) {
|
|
4750
5128
|
return path === ":memory:" || path.startsWith("file::memory:");
|
|
@@ -4755,7 +5133,7 @@ function findNearestProjectDb(startDir) {
|
|
|
4755
5133
|
let dir = resolve2(startDir);
|
|
4756
5134
|
while (true) {
|
|
4757
5135
|
const candidate = join3(dir, ".hasna", "todos", "todos.db");
|
|
4758
|
-
if (
|
|
5136
|
+
if (existsSync5(candidate))
|
|
4759
5137
|
return candidate;
|
|
4760
5138
|
if (dir === stopAt)
|
|
4761
5139
|
break;
|
|
@@ -4769,7 +5147,7 @@ function findNearestProjectDb(startDir) {
|
|
|
4769
5147
|
function findGitRoot(startDir) {
|
|
4770
5148
|
let dir = resolve2(startDir);
|
|
4771
5149
|
while (true) {
|
|
4772
|
-
if (
|
|
5150
|
+
if (existsSync5(join3(dir, ".git")))
|
|
4773
5151
|
return dir;
|
|
4774
5152
|
const parent = dirname2(dir);
|
|
4775
5153
|
if (parent === dir)
|
|
@@ -4829,7 +5207,7 @@ function ensureDir2(filePath) {
|
|
|
4829
5207
|
if (isInMemoryDb(filePath))
|
|
4830
5208
|
return;
|
|
4831
5209
|
const dir = dirname2(resolve2(filePath));
|
|
4832
|
-
if (!
|
|
5210
|
+
if (!existsSync5(dir)) {
|
|
4833
5211
|
mkdirSync2(dir, { recursive: true });
|
|
4834
5212
|
}
|
|
4835
5213
|
}
|
|
@@ -6097,7 +6475,7 @@ var init_event_hooks = __esm(() => {
|
|
|
6097
6475
|
|
|
6098
6476
|
// node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/index.js
|
|
6099
6477
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
6100
|
-
import { existsSync as
|
|
6478
|
+
import { existsSync as existsSync6 } from "fs";
|
|
6101
6479
|
import { homedir as homedir2 } from "os";
|
|
6102
6480
|
import { join as join4 } from "path";
|
|
6103
6481
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
@@ -6284,7 +6662,7 @@ class JsonEventsStore {
|
|
|
6284
6662
|
};
|
|
6285
6663
|
}
|
|
6286
6664
|
async ensureArrayFile(path) {
|
|
6287
|
-
if (!
|
|
6665
|
+
if (!existsSync6(path)) {
|
|
6288
6666
|
await writeFile(path, `[]
|
|
6289
6667
|
`, { encoding: "utf-8", mode: 384 });
|
|
6290
6668
|
}
|
|
@@ -6847,476 +7225,247 @@ function booleanField(value) {
|
|
|
6847
7225
|
if (value === 1)
|
|
6848
7226
|
return true;
|
|
6849
7227
|
if (value === 0)
|
|
6850
|
-
return false;
|
|
6851
|
-
}
|
|
6852
|
-
if (typeof value === "string") {
|
|
6853
|
-
const normalized = value.trim().toLowerCase();
|
|
6854
|
-
if (["true", "1", "yes", "on"].includes(normalized))
|
|
6855
|
-
return true;
|
|
6856
|
-
if (["false", "0", "no", "off"].includes(normalized))
|
|
6857
|
-
return false;
|
|
6858
|
-
}
|
|
6859
|
-
return;
|
|
6860
|
-
}
|
|
6861
|
-
function objectField(value) {
|
|
6862
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
6863
|
-
}
|
|
6864
|
-
function collectBooleans(records, keys) {
|
|
6865
|
-
const values = [];
|
|
6866
|
-
for (const record of records) {
|
|
6867
|
-
if (!record)
|
|
6868
|
-
continue;
|
|
6869
|
-
for (const key of keys) {
|
|
6870
|
-
const value = booleanField(record[key]);
|
|
6871
|
-
if (value !== undefined)
|
|
6872
|
-
values.push(value);
|
|
6873
|
-
}
|
|
6874
|
-
}
|
|
6875
|
-
return values;
|
|
6876
|
-
}
|
|
6877
|
-
function mergedBoolean(records, keys, trueWins) {
|
|
6878
|
-
const values = collectBooleans(records, keys);
|
|
6879
|
-
if (values.length === 0)
|
|
6880
|
-
return;
|
|
6881
|
-
if (trueWins)
|
|
6882
|
-
return values.some(Boolean);
|
|
6883
|
-
return values.includes(false) ? false : true;
|
|
6884
|
-
}
|
|
6885
|
-
function routingAutomationMetadata(task, taskList) {
|
|
6886
|
-
const taskAutomation = objectField(task.metadata.automation);
|
|
6887
|
-
const taskListAutomation = taskList ? objectField(taskList.metadata.automation) : undefined;
|
|
6888
|
-
const records = [task.metadata, taskAutomation, taskList?.metadata, taskListAutomation];
|
|
6889
|
-
const result = {};
|
|
6890
|
-
const aliases = [
|
|
6891
|
-
["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
|
|
6892
|
-
["no_auto", ["no_auto", "noAuto"]],
|
|
6893
|
-
["manual", ["manual"]],
|
|
6894
|
-
["manual_required", ["manual_required", "manualRequired"]],
|
|
6895
|
-
["requires_approval", ["requires_approval", "requiresApproval"]],
|
|
6896
|
-
["approval_required", ["approval_required", "approvalRequired"]]
|
|
6897
|
-
];
|
|
6898
|
-
for (const [canonical, keys] of aliases) {
|
|
6899
|
-
const value = mergedBoolean(records, keys, canonical !== "allowed");
|
|
6900
|
-
if (value !== undefined)
|
|
6901
|
-
result[canonical] = value;
|
|
6902
|
-
}
|
|
6903
|
-
if (task.requires_approval)
|
|
6904
|
-
result.requires_approval = true;
|
|
6905
|
-
return Object.keys(result).length > 0 ? result : undefined;
|
|
6906
|
-
}
|
|
6907
|
-
function routeEnabledForTask(task, taskList) {
|
|
6908
|
-
const explicit = booleanField(task.metadata.route_enabled);
|
|
6909
|
-
if (explicit !== undefined)
|
|
6910
|
-
return explicit;
|
|
6911
|
-
const taskListDefault = taskList ? booleanField(taskList.metadata.route_enabled) : undefined;
|
|
6912
|
-
if (taskListDefault !== undefined)
|
|
6913
|
-
return taskListDefault;
|
|
6914
|
-
return;
|
|
6915
|
-
}
|
|
6916
|
-
function stringField(value) {
|
|
6917
|
-
return typeof value === "string" && value.trim() ? value : undefined;
|
|
6918
|
-
}
|
|
6919
|
-
function workflowPointersFromMetadata(metadata) {
|
|
6920
|
-
const nested = objectField(metadata.workflow_invocation) ?? objectField(metadata.workflow) ?? {};
|
|
6921
|
-
return {
|
|
6922
|
-
current_workflow_invocation_id: stringField(metadata.current_workflow_invocation_id) ?? stringField(nested.current_workflow_invocation_id) ?? stringField(nested.invocation_id),
|
|
6923
|
-
current_run_id: stringField(metadata.current_run_id) ?? stringField(nested.current_run_id) ?? stringField(nested.run_id),
|
|
6924
|
-
latest_manifest_path: stringField(metadata.latest_manifest_path) ?? stringField(nested.latest_manifest_path) ?? stringField(nested.manifest_path),
|
|
6925
|
-
latest_evaluation_path: stringField(metadata.latest_evaluation_path) ?? stringField(nested.latest_evaluation_path) ?? stringField(nested.evaluation_path),
|
|
6926
|
-
workflow_state: stringField(metadata.workflow_state) ?? stringField(nested.workflow_state) ?? stringField(nested.state)
|
|
6927
|
-
};
|
|
6928
|
-
}
|
|
6929
|
-
function compactWorkflowPointers(pointers) {
|
|
6930
|
-
return Object.fromEntries(Object.entries(pointers).filter(([, value]) => typeof value === "string" && value.length > 0));
|
|
6931
|
-
}
|
|
6932
|
-
function metadataStringField(record, keys) {
|
|
6933
|
-
if (!record)
|
|
6934
|
-
return;
|
|
6935
|
-
for (const key of keys) {
|
|
6936
|
-
const value = record[key];
|
|
6937
|
-
if (typeof value === "string" && value.trim())
|
|
6938
|
-
return value.trim();
|
|
6939
|
-
}
|
|
6940
|
-
return;
|
|
6941
|
-
}
|
|
6942
|
-
function projectKindFromMetadata(...records) {
|
|
6943
|
-
for (const record of records) {
|
|
6944
|
-
const value = metadataStringField(record ?? undefined, ["project_kind", "projectKind", "source_kind", "sourceKind"]);
|
|
6945
|
-
if (value)
|
|
6946
|
-
return value;
|
|
6947
|
-
}
|
|
6948
|
-
return null;
|
|
6949
|
-
}
|
|
6950
|
-
function classifyProjectKind(_path, metadata) {
|
|
6951
|
-
return projectKindFromMetadata(metadata);
|
|
6952
|
-
}
|
|
6953
|
-
function isWorktreePath(path) {
|
|
6954
|
-
return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
|
|
6955
|
-
}
|
|
6956
|
-
function inferRootProjectId(project) {
|
|
6957
|
-
return isWorktreePath(project.path) ? null : project.id;
|
|
6958
|
-
}
|
|
6959
|
-
var TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION = "todos.task_route_state.v1", TASK_WORKFLOW_POINTER_SCHEMA_VERSION = "todos.task_workflow_pointer.v1";
|
|
6960
|
-
|
|
6961
|
-
// src/lib/shared-events.ts
|
|
6962
|
-
function taskEventData(task, extra = {}) {
|
|
6963
|
-
return {
|
|
6964
|
-
id: task.id,
|
|
6965
|
-
task_id: task.id,
|
|
6966
|
-
short_id: task.short_id,
|
|
6967
|
-
title: task.title,
|
|
6968
|
-
description: task.description,
|
|
6969
|
-
status: task.status,
|
|
6970
|
-
priority: task.priority,
|
|
6971
|
-
project_id: task.project_id,
|
|
6972
|
-
parent_id: task.parent_id,
|
|
6973
|
-
plan_id: task.plan_id,
|
|
6974
|
-
task_list_id: task.task_list_id,
|
|
6975
|
-
agent_id: task.agent_id,
|
|
6976
|
-
assigned_to: task.assigned_to,
|
|
6977
|
-
session_id: task.session_id,
|
|
6978
|
-
working_dir: task.working_dir,
|
|
6979
|
-
tags: task.tags,
|
|
6980
|
-
metadata: task.metadata,
|
|
6981
|
-
version: task.version,
|
|
6982
|
-
created_at: task.created_at,
|
|
6983
|
-
updated_at: task.updated_at,
|
|
6984
|
-
started_at: task.started_at,
|
|
6985
|
-
completed_at: task.completed_at,
|
|
6986
|
-
due_at: task.due_at,
|
|
6987
|
-
requires_approval: task.requires_approval,
|
|
6988
|
-
approved_by: task.approved_by,
|
|
6989
|
-
approved_at: task.approved_at,
|
|
6990
|
-
...extra
|
|
6991
|
-
};
|
|
6992
|
-
}
|
|
6993
|
-
function taskEventMetadata(task) {
|
|
6994
|
-
const metadata = {
|
|
6995
|
-
package: "@hasna/todos",
|
|
6996
|
-
todos_event_schema_version: 1,
|
|
6997
|
-
route_state_schema_version: TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION,
|
|
6998
|
-
task_id: task.id,
|
|
6999
|
-
task_short_id: task.short_id,
|
|
7000
|
-
project_id: task.project_id,
|
|
7001
|
-
task_list_id: task.task_list_id,
|
|
7002
|
-
working_dir: task.working_dir
|
|
7003
|
-
};
|
|
7004
|
-
const pointers = workflowPointersFromMetadata(task.metadata);
|
|
7005
|
-
for (const [key, value] of Object.entries(pointers)) {
|
|
7006
|
-
if (value)
|
|
7007
|
-
metadata[key] = value;
|
|
7008
|
-
}
|
|
7009
|
-
try {
|
|
7010
|
-
const project = task.project_id ? getProject(task.project_id) : null;
|
|
7011
|
-
const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
|
|
7012
|
-
if (project) {
|
|
7013
|
-
metadata.project_id = project.id;
|
|
7014
|
-
metadata.project_name = project.name;
|
|
7015
|
-
metadata.project_path = projectPath;
|
|
7016
|
-
metadata.project_canonical_path = project.path;
|
|
7017
|
-
metadata.project_default_task_list_slug = project.task_list_id;
|
|
7018
|
-
metadata.root_project_id = inferRootProjectId(project);
|
|
7019
|
-
} else if (projectPath) {
|
|
7020
|
-
metadata.project_path = projectPath;
|
|
7021
|
-
metadata.project_canonical_path = projectPath;
|
|
7022
|
-
}
|
|
7023
|
-
if (projectPath) {
|
|
7024
|
-
metadata.project_is_worktree = isWorktreePath(projectPath);
|
|
7025
|
-
metadata.working_dir = task.working_dir ?? projectPath;
|
|
7026
|
-
}
|
|
7027
|
-
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;
|
|
7028
|
-
if (taskList) {
|
|
7029
|
-
metadata.task_list_id = taskList.id;
|
|
7030
|
-
metadata.task_list_slug = taskList.slug;
|
|
7031
|
-
metadata.task_list_name = taskList.name;
|
|
7032
|
-
metadata.task_list_project_id = taskList.project_id;
|
|
7033
|
-
metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
|
|
7034
|
-
}
|
|
7035
|
-
const projectKind = projectKindFromMetadata(task.metadata, taskList?.metadata);
|
|
7036
|
-
if (projectKind) {
|
|
7037
|
-
metadata.project_kind = classifyProjectKind(projectPath ?? "", { project_kind: projectKind });
|
|
7038
|
-
}
|
|
7039
|
-
const routeEnabled = routeEnabledForTask(task, taskList);
|
|
7040
|
-
if (routeEnabled !== undefined) {
|
|
7041
|
-
metadata.route_enabled = routeEnabled;
|
|
7042
|
-
}
|
|
7043
|
-
const automation = routingAutomationMetadata(task, taskList);
|
|
7044
|
-
if (automation) {
|
|
7045
|
-
metadata.automation = automation;
|
|
7046
|
-
metadata.route_blocked_by_no_auto = automation.no_auto === true;
|
|
7047
|
-
metadata.route_blocked_by_manual = automation.manual === true || automation.manual_required === true;
|
|
7048
|
-
metadata.route_blocked_by_approval = (automation.requires_approval === true || automation.approval_required === true) && !task.approved_by;
|
|
7049
|
-
}
|
|
7050
|
-
} catch {}
|
|
7051
|
-
return metadata;
|
|
7052
|
-
}
|
|
7053
|
-
function readMachineLocalPath(project) {
|
|
7054
|
-
const machineId = process.env["TODOS_MACHINE_ID"];
|
|
7055
|
-
if (!machineId)
|
|
7056
|
-
return null;
|
|
7057
|
-
try {
|
|
7058
|
-
const row = getDatabase().query("SELECT path FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(project.id, machineId);
|
|
7059
|
-
return row?.path ?? null;
|
|
7060
|
-
} catch {
|
|
7061
|
-
return null;
|
|
7062
|
-
}
|
|
7063
|
-
}
|
|
7064
|
-
async function emitSharedTaskEvent(input) {
|
|
7065
|
-
if (!shouldEmitSharedTaskEvents(input.databasePath))
|
|
7066
|
-
return;
|
|
7067
|
-
const data = taskEventData(input.task, input.data);
|
|
7068
|
-
await new EventsClient().emit({
|
|
7069
|
-
source: SOURCE,
|
|
7070
|
-
type: input.type,
|
|
7071
|
-
subject: input.task.id,
|
|
7072
|
-
severity: input.severity ?? "info",
|
|
7073
|
-
message: input.message ?? `${input.type}: ${input.task.title}`,
|
|
7074
|
-
data,
|
|
7075
|
-
dedupeKey: input.dedupeKey ?? `${input.type}:${input.task.id}:${input.task.version}`,
|
|
7076
|
-
metadata: taskEventMetadata(input.task)
|
|
7077
|
-
}, { deliver: true, dedupe: true });
|
|
7078
|
-
}
|
|
7079
|
-
function emitSharedTaskEventQuiet(input) {
|
|
7080
|
-
emitSharedTaskEvent(input).catch(() => {
|
|
7081
|
-
return;
|
|
7082
|
-
});
|
|
7083
|
-
}
|
|
7084
|
-
var SOURCE = "todos";
|
|
7085
|
-
var init_shared_events = __esm(() => {
|
|
7086
|
-
init_dist();
|
|
7087
|
-
init_database();
|
|
7088
|
-
init_projects();
|
|
7089
|
-
init_task_lists();
|
|
7090
|
-
init_event_emission_safety();
|
|
7091
|
-
});
|
|
7092
|
-
|
|
7093
|
-
// src/lib/secret-redaction.ts
|
|
7094
|
-
import { readFileSync as readFileSync2, existsSync as existsSync6 } from "fs";
|
|
7095
|
-
function registerCustomRedactor(fn) {
|
|
7096
|
-
customRedactors.push(fn);
|
|
7097
|
-
}
|
|
7098
|
-
function resetCustomRedactors() {
|
|
7099
|
-
customRedactors = [];
|
|
7228
|
+
return false;
|
|
7229
|
+
}
|
|
7230
|
+
if (typeof value === "string") {
|
|
7231
|
+
const normalized = value.trim().toLowerCase();
|
|
7232
|
+
if (["true", "1", "yes", "on"].includes(normalized))
|
|
7233
|
+
return true;
|
|
7234
|
+
if (["false", "0", "no", "off"].includes(normalized))
|
|
7235
|
+
return false;
|
|
7236
|
+
}
|
|
7237
|
+
return;
|
|
7100
7238
|
}
|
|
7101
|
-
function
|
|
7102
|
-
|
|
7103
|
-
return allowlist.some((re) => re.test(context) || re.test(match));
|
|
7239
|
+
function objectField(value) {
|
|
7240
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
7104
7241
|
}
|
|
7105
|
-
function
|
|
7106
|
-
const
|
|
7107
|
-
const
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
let m;
|
|
7115
|
-
while ((m = re.exec(text)) !== null) {
|
|
7116
|
-
const match = m[0];
|
|
7117
|
-
if (allowlist_ok && isAllowlisted(text, match, allowlist))
|
|
7118
|
-
continue;
|
|
7119
|
-
if (isAllowlisted(text, match, allowlist))
|
|
7120
|
-
continue;
|
|
7121
|
-
const line = text.slice(0, m.index).split(`
|
|
7122
|
-
`).length;
|
|
7123
|
-
matches.push({ pattern: name, match: match.slice(0, 12) + (match.length > 12 ? "\u2026" : ""), index: m.index, line });
|
|
7242
|
+
function collectBooleans(records, keys) {
|
|
7243
|
+
const values = [];
|
|
7244
|
+
for (const record of records) {
|
|
7245
|
+
if (!record)
|
|
7246
|
+
continue;
|
|
7247
|
+
for (const key of keys) {
|
|
7248
|
+
const value = booleanField(record[key]);
|
|
7249
|
+
if (value !== undefined)
|
|
7250
|
+
values.push(value);
|
|
7124
7251
|
}
|
|
7125
7252
|
}
|
|
7126
|
-
return
|
|
7127
|
-
schema_version: SECRET_REDACTION_SCHEMA,
|
|
7128
|
-
clean: matches.length === 0,
|
|
7129
|
-
matches
|
|
7130
|
-
};
|
|
7253
|
+
return values;
|
|
7131
7254
|
}
|
|
7132
|
-
function
|
|
7133
|
-
const
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7255
|
+
function mergedBoolean(records, keys, trueWins) {
|
|
7256
|
+
const values = collectBooleans(records, keys);
|
|
7257
|
+
if (values.length === 0)
|
|
7258
|
+
return;
|
|
7259
|
+
if (trueWins)
|
|
7260
|
+
return values.some(Boolean);
|
|
7261
|
+
return values.includes(false) ? false : true;
|
|
7262
|
+
}
|
|
7263
|
+
function routingAutomationMetadata(task, taskList) {
|
|
7264
|
+
const taskAutomation = objectField(task.metadata.automation);
|
|
7265
|
+
const taskListAutomation = taskList ? objectField(taskList.metadata.automation) : undefined;
|
|
7266
|
+
const records = [task.metadata, taskAutomation, taskList?.metadata, taskListAutomation];
|
|
7267
|
+
const result = {};
|
|
7268
|
+
const aliases = [
|
|
7269
|
+
["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
|
|
7270
|
+
["no_auto", ["no_auto", "noAuto"]],
|
|
7271
|
+
["manual", ["manual"]],
|
|
7272
|
+
["manual_required", ["manual_required", "manualRequired"]],
|
|
7273
|
+
["requires_approval", ["requires_approval", "requiresApproval"]],
|
|
7274
|
+
["approval_required", ["approval_required", "approvalRequired"]]
|
|
7275
|
+
];
|
|
7276
|
+
for (const [canonical, keys] of aliases) {
|
|
7277
|
+
const value = mergedBoolean(records, keys, canonical !== "allowed");
|
|
7278
|
+
if (value !== undefined)
|
|
7279
|
+
result[canonical] = value;
|
|
7148
7280
|
}
|
|
7149
|
-
|
|
7281
|
+
if (task.requires_approval)
|
|
7282
|
+
result.requires_approval = true;
|
|
7283
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
7150
7284
|
}
|
|
7151
|
-
function
|
|
7152
|
-
const
|
|
7285
|
+
function routeEnabledForTask(task, taskList) {
|
|
7286
|
+
const explicit = booleanField(task.metadata.route_enabled);
|
|
7287
|
+
if (explicit !== undefined)
|
|
7288
|
+
return explicit;
|
|
7289
|
+
const taskListDefault = taskList ? booleanField(taskList.metadata.route_enabled) : undefined;
|
|
7290
|
+
if (taskListDefault !== undefined)
|
|
7291
|
+
return taskListDefault;
|
|
7292
|
+
return;
|
|
7293
|
+
}
|
|
7294
|
+
function stringField(value) {
|
|
7295
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
7296
|
+
}
|
|
7297
|
+
function workflowPointersFromMetadata(metadata) {
|
|
7298
|
+
const nested = objectField(metadata.workflow_invocation) ?? objectField(metadata.workflow) ?? {};
|
|
7153
7299
|
return {
|
|
7154
|
-
|
|
7155
|
-
|
|
7300
|
+
current_workflow_invocation_id: stringField(metadata.current_workflow_invocation_id) ?? stringField(nested.current_workflow_invocation_id) ?? stringField(nested.invocation_id),
|
|
7301
|
+
current_run_id: stringField(metadata.current_run_id) ?? stringField(nested.current_run_id) ?? stringField(nested.run_id),
|
|
7302
|
+
latest_manifest_path: stringField(metadata.latest_manifest_path) ?? stringField(nested.latest_manifest_path) ?? stringField(nested.manifest_path),
|
|
7303
|
+
latest_evaluation_path: stringField(metadata.latest_evaluation_path) ?? stringField(nested.latest_evaluation_path) ?? stringField(nested.evaluation_path),
|
|
7304
|
+
workflow_state: stringField(metadata.workflow_state) ?? stringField(nested.workflow_state) ?? stringField(nested.state)
|
|
7156
7305
|
};
|
|
7157
7306
|
}
|
|
7158
|
-
function
|
|
7159
|
-
|
|
7160
|
-
throw new Error(`File not found: ${path}`);
|
|
7161
|
-
const content = readFileSync2(path, "utf8");
|
|
7162
|
-
return scanAndRedactText(content, options);
|
|
7163
|
-
}
|
|
7164
|
-
function safeStringify(value, space) {
|
|
7165
|
-
const redacted = typeof value === "object" && value !== null && !Array.isArray(value) ? redactValue(value) : value;
|
|
7166
|
-
const json = JSON.stringify(redacted, null, space);
|
|
7167
|
-
return redactText(json);
|
|
7307
|
+
function compactWorkflowPointers(pointers) {
|
|
7308
|
+
return Object.fromEntries(Object.entries(pointers).filter(([, value]) => typeof value === "string" && value.length > 0));
|
|
7168
7309
|
}
|
|
7169
|
-
function
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
7310
|
+
function metadataStringField(record, keys) {
|
|
7311
|
+
if (!record)
|
|
7312
|
+
return;
|
|
7313
|
+
for (const key of keys) {
|
|
7314
|
+
const value = record[key];
|
|
7315
|
+
if (typeof value === "string" && value.trim())
|
|
7316
|
+
return value.trim();
|
|
7174
7317
|
}
|
|
7318
|
+
return;
|
|
7175
7319
|
}
|
|
7176
|
-
function
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
}
|
|
7182
|
-
function redactExportRecord(record) {
|
|
7183
|
-
const base = redactValue(record);
|
|
7184
|
-
for (const [key, value] of Object.entries(base)) {
|
|
7185
|
-
if (typeof value === "string") {
|
|
7186
|
-
base[key] = redactText(value);
|
|
7187
|
-
}
|
|
7320
|
+
function projectKindFromMetadata(...records) {
|
|
7321
|
+
for (const record of records) {
|
|
7322
|
+
const value = metadataStringField(record ?? undefined, ["project_kind", "projectKind", "source_kind", "sourceKind"]);
|
|
7323
|
+
if (value)
|
|
7324
|
+
return value;
|
|
7188
7325
|
}
|
|
7189
|
-
return
|
|
7326
|
+
return null;
|
|
7190
7327
|
}
|
|
7191
|
-
function
|
|
7192
|
-
return
|
|
7328
|
+
function classifyProjectKind(_path, metadata) {
|
|
7329
|
+
return projectKindFromMetadata(metadata);
|
|
7193
7330
|
}
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
init_redaction();
|
|
7197
|
-
SECRET_REDACTION_SCHEMA = ["todos", "secret_redaction", "v1"].join(".");
|
|
7198
|
-
DEFAULT_PATTERNS = [
|
|
7199
|
-
{ name: "openai_sk", pattern: /\bsk-[a-zA-Z0-9]{10,}\b/g },
|
|
7200
|
-
{ name: "github_pat", pattern: /\bghp_[a-zA-Z0-9]{20,}\b/g },
|
|
7201
|
-
{ name: "github_oauth", pattern: /\bgho_[a-zA-Z0-9]{20,}\b/g },
|
|
7202
|
-
{ name: "aws_access_key", pattern: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
7203
|
-
{ name: "bearer_token", pattern: /\bBearer\s+[a-zA-Z0-9\-._~+/]+=*\b/gi },
|
|
7204
|
-
{ name: "jwt", pattern: /\beyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g },
|
|
7205
|
-
{ name: "private_key_block", pattern: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g },
|
|
7206
|
-
{ name: "generic_api_key", pattern: /\b(?:api[_-]?key|secret|token|password)\s*[:=]\s*['"]?[a-zA-Z0-9\-._]{8,}['"]?/gi, allowlist_ok: true }
|
|
7207
|
-
];
|
|
7208
|
-
DEFAULT_ALLOWLIST = [
|
|
7209
|
-
/\[REDACTED\]/,
|
|
7210
|
-
/example\.com/i,
|
|
7211
|
-
/your-api-key-here/i,
|
|
7212
|
-
/sk-test/i,
|
|
7213
|
-
/ghp_xxx/i
|
|
7214
|
-
];
|
|
7215
|
-
customRedactors = [];
|
|
7216
|
-
});
|
|
7217
|
-
|
|
7218
|
-
// src/lib/prewrite-secrets.ts
|
|
7219
|
-
function mergeFindings(findings) {
|
|
7220
|
-
const byPattern = new Map;
|
|
7221
|
-
for (const finding of findings) {
|
|
7222
|
-
const existing = byPattern.get(finding.pattern);
|
|
7223
|
-
if (!existing) {
|
|
7224
|
-
byPattern.set(finding.pattern, {
|
|
7225
|
-
pattern: finding.pattern,
|
|
7226
|
-
count: finding.count,
|
|
7227
|
-
lines: Array.from(new Set(finding.lines)).sort((a, b) => a - b)
|
|
7228
|
-
});
|
|
7229
|
-
continue;
|
|
7230
|
-
}
|
|
7231
|
-
existing.count += finding.count;
|
|
7232
|
-
existing.lines = Array.from(new Set([...existing.lines, ...finding.lines])).sort((a, b) => a - b);
|
|
7233
|
-
}
|
|
7234
|
-
return [...byPattern.values()].sort((left, right) => left.pattern.localeCompare(right.pattern));
|
|
7331
|
+
function isWorktreePath(path) {
|
|
7332
|
+
return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
|
|
7235
7333
|
}
|
|
7236
|
-
function
|
|
7237
|
-
return
|
|
7334
|
+
function inferRootProjectId(project) {
|
|
7335
|
+
return isWorktreePath(project.path) ? null : project.id;
|
|
7238
7336
|
}
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
schema_version: PREWRITE_SECRET_SCAN_SCHEMA,
|
|
7244
|
-
clean: true,
|
|
7245
|
-
context,
|
|
7246
|
-
findings: []
|
|
7247
|
-
};
|
|
7248
|
-
}
|
|
7249
|
-
const structural = scanTextForSecrets(value).matches.map((match) => ({
|
|
7250
|
-
pattern: match.pattern,
|
|
7251
|
-
count: 1,
|
|
7252
|
-
lines: match.line ? [match.line] : []
|
|
7253
|
-
}));
|
|
7254
|
-
const configured = listSecretFindings(value).map((finding) => ({
|
|
7255
|
-
pattern: finding.pattern,
|
|
7256
|
-
count: finding.count,
|
|
7257
|
-
lines: []
|
|
7258
|
-
}));
|
|
7337
|
+
var TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION = "todos.task_route_state.v1", TASK_WORKFLOW_POINTER_SCHEMA_VERSION = "todos.task_workflow_pointer.v1";
|
|
7338
|
+
|
|
7339
|
+
// src/lib/shared-events.ts
|
|
7340
|
+
function taskEventData(task, extra = {}) {
|
|
7259
7341
|
return {
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7342
|
+
id: task.id,
|
|
7343
|
+
task_id: task.id,
|
|
7344
|
+
short_id: task.short_id,
|
|
7345
|
+
title: task.title,
|
|
7346
|
+
description: task.description,
|
|
7347
|
+
status: task.status,
|
|
7348
|
+
priority: task.priority,
|
|
7349
|
+
project_id: task.project_id,
|
|
7350
|
+
parent_id: task.parent_id,
|
|
7351
|
+
plan_id: task.plan_id,
|
|
7352
|
+
task_list_id: task.task_list_id,
|
|
7353
|
+
agent_id: task.agent_id,
|
|
7354
|
+
assigned_to: task.assigned_to,
|
|
7355
|
+
session_id: task.session_id,
|
|
7356
|
+
working_dir: task.working_dir,
|
|
7357
|
+
tags: task.tags,
|
|
7358
|
+
metadata: task.metadata,
|
|
7359
|
+
version: task.version,
|
|
7360
|
+
created_at: task.created_at,
|
|
7361
|
+
updated_at: task.updated_at,
|
|
7362
|
+
started_at: task.started_at,
|
|
7363
|
+
completed_at: task.completed_at,
|
|
7364
|
+
due_at: task.due_at,
|
|
7365
|
+
requires_approval: task.requires_approval,
|
|
7366
|
+
approved_by: task.approved_by,
|
|
7367
|
+
approved_at: task.approved_at,
|
|
7368
|
+
...extra
|
|
7264
7369
|
};
|
|
7265
7370
|
}
|
|
7266
|
-
function
|
|
7267
|
-
const
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
}
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
return sanitizePreWriteText(value, context, options);
|
|
7282
|
-
if (Array.isArray(value)) {
|
|
7283
|
-
return value.map((item, index) => sanitizePreWriteValue(item, `${context}[${index}]`, options));
|
|
7371
|
+
function taskEventMetadata(task) {
|
|
7372
|
+
const metadata = {
|
|
7373
|
+
package: "@hasna/todos",
|
|
7374
|
+
todos_event_schema_version: 1,
|
|
7375
|
+
route_state_schema_version: TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION,
|
|
7376
|
+
task_id: task.id,
|
|
7377
|
+
task_short_id: task.short_id,
|
|
7378
|
+
project_id: task.project_id,
|
|
7379
|
+
task_list_id: task.task_list_id,
|
|
7380
|
+
working_dir: task.working_dir
|
|
7381
|
+
};
|
|
7382
|
+
const pointers = workflowPointersFromMetadata(task.metadata);
|
|
7383
|
+
for (const [key, value] of Object.entries(pointers)) {
|
|
7384
|
+
if (value)
|
|
7385
|
+
metadata[key] = value;
|
|
7284
7386
|
}
|
|
7285
|
-
|
|
7286
|
-
const
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7387
|
+
try {
|
|
7388
|
+
const project = task.project_id ? getProject(task.project_id) : null;
|
|
7389
|
+
const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
|
|
7390
|
+
if (project) {
|
|
7391
|
+
metadata.project_id = project.id;
|
|
7392
|
+
metadata.project_name = project.name;
|
|
7393
|
+
metadata.project_path = projectPath;
|
|
7394
|
+
metadata.project_canonical_path = project.path;
|
|
7395
|
+
metadata.project_default_task_list_slug = project.task_list_id;
|
|
7396
|
+
metadata.root_project_id = inferRootProjectId(project);
|
|
7397
|
+
} else if (projectPath) {
|
|
7398
|
+
metadata.project_path = projectPath;
|
|
7399
|
+
metadata.project_canonical_path = projectPath;
|
|
7290
7400
|
}
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7401
|
+
if (projectPath) {
|
|
7402
|
+
metadata.project_is_worktree = isWorktreePath(projectPath);
|
|
7403
|
+
metadata.working_dir = task.working_dir ?? projectPath;
|
|
7404
|
+
}
|
|
7405
|
+
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;
|
|
7406
|
+
if (taskList) {
|
|
7407
|
+
metadata.task_list_id = taskList.id;
|
|
7408
|
+
metadata.task_list_slug = taskList.slug;
|
|
7409
|
+
metadata.task_list_name = taskList.name;
|
|
7410
|
+
metadata.task_list_project_id = taskList.project_id;
|
|
7411
|
+
metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
|
|
7412
|
+
}
|
|
7413
|
+
const projectKind = projectKindFromMetadata(task.metadata, taskList?.metadata);
|
|
7414
|
+
if (projectKind) {
|
|
7415
|
+
metadata.project_kind = classifyProjectKind(projectPath ?? "", { project_kind: projectKind });
|
|
7416
|
+
}
|
|
7417
|
+
const routeEnabled = routeEnabledForTask(task, taskList);
|
|
7418
|
+
if (routeEnabled !== undefined) {
|
|
7419
|
+
metadata.route_enabled = routeEnabled;
|
|
7420
|
+
}
|
|
7421
|
+
const automation = routingAutomationMetadata(task, taskList);
|
|
7422
|
+
if (automation) {
|
|
7423
|
+
metadata.automation = automation;
|
|
7424
|
+
metadata.route_blocked_by_no_auto = automation.no_auto === true;
|
|
7425
|
+
metadata.route_blocked_by_manual = automation.manual === true || automation.manual_required === true;
|
|
7426
|
+
metadata.route_blocked_by_approval = (automation.requires_approval === true || automation.approval_required === true) && !task.approved_by;
|
|
7427
|
+
}
|
|
7428
|
+
} catch {}
|
|
7429
|
+
return metadata;
|
|
7294
7430
|
}
|
|
7295
|
-
function
|
|
7296
|
-
|
|
7431
|
+
function readMachineLocalPath(project) {
|
|
7432
|
+
const machineId = process.env["TODOS_MACHINE_ID"];
|
|
7433
|
+
if (!machineId)
|
|
7297
7434
|
return null;
|
|
7298
7435
|
try {
|
|
7299
|
-
|
|
7436
|
+
const row = getDatabase().query("SELECT path FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(project.id, machineId);
|
|
7437
|
+
return row?.path ?? null;
|
|
7300
7438
|
} catch {
|
|
7301
|
-
return
|
|
7439
|
+
return null;
|
|
7302
7440
|
}
|
|
7303
7441
|
}
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
|
|
7442
|
+
async function emitSharedTaskEvent(input) {
|
|
7443
|
+
if (!shouldEmitSharedTaskEvents(input.databasePath))
|
|
7444
|
+
return;
|
|
7445
|
+
const data = taskEventData(input.task, input.data);
|
|
7446
|
+
await new EventsClient().emit({
|
|
7447
|
+
source: SOURCE,
|
|
7448
|
+
type: input.type,
|
|
7449
|
+
subject: input.task.id,
|
|
7450
|
+
severity: input.severity ?? "info",
|
|
7451
|
+
message: input.message ?? `${input.type}: ${input.task.title}`,
|
|
7452
|
+
data,
|
|
7453
|
+
dedupeKey: input.dedupeKey ?? `${input.type}:${input.task.id}:${input.task.version}`,
|
|
7454
|
+
metadata: taskEventMetadata(input.task)
|
|
7455
|
+
}, { deliver: true, dedupe: true });
|
|
7456
|
+
}
|
|
7457
|
+
function emitSharedTaskEventQuiet(input) {
|
|
7458
|
+
emitSharedTaskEvent(input).catch(() => {
|
|
7459
|
+
return;
|
|
7460
|
+
});
|
|
7461
|
+
}
|
|
7462
|
+
var SOURCE = "todos";
|
|
7463
|
+
var init_shared_events = __esm(() => {
|
|
7464
|
+
init_dist();
|
|
7465
|
+
init_database();
|
|
7466
|
+
init_projects();
|
|
7467
|
+
init_task_lists();
|
|
7468
|
+
init_event_emission_safety();
|
|
7320
7469
|
});
|
|
7321
7470
|
|
|
7322
7471
|
// src/lib/activity-audit.ts
|
|
@@ -7511,28 +7660,55 @@ var init_activity_audit = __esm(() => {
|
|
|
7511
7660
|
function sanitizeHistoryValue(value, context) {
|
|
7512
7661
|
return value === undefined || value === null ? null : sanitizePreWriteText(String(value), context);
|
|
7513
7662
|
}
|
|
7514
|
-
function
|
|
7663
|
+
function insertTaskHistory(entry2, db) {
|
|
7515
7664
|
const d = db || getDatabase();
|
|
7516
|
-
const
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7665
|
+
const safeEntry = {
|
|
7666
|
+
...entry2,
|
|
7667
|
+
field: entry2.field || null,
|
|
7668
|
+
old_value: sanitizeHistoryValue(entry2.old_value, "task_history.old_value"),
|
|
7669
|
+
new_value: sanitizeHistoryValue(entry2.new_value, "task_history.new_value"),
|
|
7670
|
+
agent_id: entry2.agent_id || null,
|
|
7671
|
+
machine_id: entry2.machine_id ?? currentStorageMachineId(d)
|
|
7672
|
+
};
|
|
7521
7673
|
d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at, machine_id)
|
|
7522
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
7674
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
7675
|
+
safeEntry.id,
|
|
7676
|
+
safeEntry.task_id,
|
|
7677
|
+
safeEntry.action,
|
|
7678
|
+
safeEntry.field,
|
|
7679
|
+
safeEntry.old_value,
|
|
7680
|
+
safeEntry.new_value,
|
|
7681
|
+
safeEntry.agent_id,
|
|
7682
|
+
safeEntry.created_at,
|
|
7683
|
+
safeEntry.machine_id ?? null
|
|
7684
|
+
]);
|
|
7523
7685
|
try {
|
|
7524
7686
|
const { logActivity: logActivity2 } = (init_activity_audit(), __toCommonJS(exports_activity_audit));
|
|
7525
7687
|
logActivity2({
|
|
7526
7688
|
entity_type: "task",
|
|
7527
|
-
entity_id:
|
|
7528
|
-
action,
|
|
7529
|
-
field,
|
|
7530
|
-
old_value:
|
|
7531
|
-
new_value:
|
|
7532
|
-
actor_id:
|
|
7689
|
+
entity_id: safeEntry.task_id,
|
|
7690
|
+
action: safeEntry.action,
|
|
7691
|
+
field: safeEntry.field ?? undefined,
|
|
7692
|
+
old_value: safeEntry.old_value,
|
|
7693
|
+
new_value: safeEntry.new_value,
|
|
7694
|
+
actor_id: safeEntry.agent_id ?? undefined
|
|
7533
7695
|
}, d);
|
|
7534
7696
|
} catch {}
|
|
7535
|
-
return
|
|
7697
|
+
return safeEntry;
|
|
7698
|
+
}
|
|
7699
|
+
function logTaskChange2(taskId, action, field, oldValue, newValue, agentId, db) {
|
|
7700
|
+
const d = db || getDatabase();
|
|
7701
|
+
return insertTaskHistory({
|
|
7702
|
+
id: uuid(),
|
|
7703
|
+
task_id: taskId,
|
|
7704
|
+
action,
|
|
7705
|
+
field: field || null,
|
|
7706
|
+
old_value: oldValue ?? null,
|
|
7707
|
+
new_value: newValue ?? null,
|
|
7708
|
+
agent_id: agentId || null,
|
|
7709
|
+
created_at: now(),
|
|
7710
|
+
machine_id: currentStorageMachineId(d)
|
|
7711
|
+
}, d);
|
|
7536
7712
|
}
|
|
7537
7713
|
function getTaskHistory(taskId, db) {
|
|
7538
7714
|
const d = db || getDatabase();
|
|
@@ -8555,6 +8731,7 @@ __export(exports_task_lifecycle, {
|
|
|
8555
8731
|
startTask: () => startTask2,
|
|
8556
8732
|
spawnNextRecurrence: () => spawnNextRecurrence,
|
|
8557
8733
|
lockTask: () => lockTask2,
|
|
8734
|
+
handoffStaleTaskLock: () => handoffStaleTaskLock,
|
|
8558
8735
|
getTasksChangedSince: () => getTasksChangedSince,
|
|
8559
8736
|
getTaskLockStatus: () => getTaskLockStatus,
|
|
8560
8737
|
getStaleTasks: () => getStaleTasks,
|
|
@@ -8823,6 +9000,38 @@ function unlockTask2(id, agentId, db) {
|
|
|
8823
9000
|
WHERE id = ?`, [timestamp2, id]);
|
|
8824
9001
|
return true;
|
|
8825
9002
|
}
|
|
9003
|
+
function handoffStaleTaskLock(input, db) {
|
|
9004
|
+
const d = db || getDatabase();
|
|
9005
|
+
const prepared = prepareStaleLockHandoff(input);
|
|
9006
|
+
const receipt = buildStaleLockHandoffReceipt(prepared);
|
|
9007
|
+
const history = staleLockHandoffHistory(receipt, null);
|
|
9008
|
+
const transfer = d.transaction(() => {
|
|
9009
|
+
const result = d.run(`UPDATE tasks
|
|
9010
|
+
SET locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
|
|
9011
|
+
WHERE id = ?
|
|
9012
|
+
AND locked_by = ?
|
|
9013
|
+
AND locked_at = ?
|
|
9014
|
+
AND julianday(locked_at) < julianday(?)
|
|
9015
|
+
AND status NOT IN ('completed', 'failed', 'cancelled')`, [
|
|
9016
|
+
prepared.new_holder,
|
|
9017
|
+
prepared.operation_timestamp,
|
|
9018
|
+
prepared.operation_timestamp,
|
|
9019
|
+
prepared.task_id,
|
|
9020
|
+
prepared.expected_holder,
|
|
9021
|
+
prepared.expected_lock_version,
|
|
9022
|
+
prepared.stale_cutoff
|
|
9023
|
+
]);
|
|
9024
|
+
if (result.changes === 0) {
|
|
9025
|
+
const current = getTask(prepared.task_id, d);
|
|
9026
|
+
if (!current)
|
|
9027
|
+
throw new TaskNotFoundError(prepared.task_id);
|
|
9028
|
+
throwStaleLockHandoffConflict(current, prepared);
|
|
9029
|
+
}
|
|
9030
|
+
insertTaskHistory(history, d);
|
|
9031
|
+
});
|
|
9032
|
+
transfer();
|
|
9033
|
+
return receipt;
|
|
9034
|
+
}
|
|
8826
9035
|
function getTaskLockStatus(id, db) {
|
|
8827
9036
|
const d = db || getDatabase();
|
|
8828
9037
|
const task = getTask(id, d);
|
|
@@ -9123,6 +9332,7 @@ var init_task_lifecycle = __esm(() => {
|
|
|
9123
9332
|
init_task_crud();
|
|
9124
9333
|
init_task_graph();
|
|
9125
9334
|
init_prewrite_secrets();
|
|
9335
|
+
init_stale_lock_handoff();
|
|
9126
9336
|
});
|
|
9127
9337
|
|
|
9128
9338
|
// src/db/task-crud.ts
|
|
@@ -12438,7 +12648,7 @@ import { createHash as createHash5 } from "crypto";
|
|
|
12438
12648
|
// package.json
|
|
12439
12649
|
var package_default = {
|
|
12440
12650
|
name: "@hasna/todos",
|
|
12441
|
-
version: "0.15.
|
|
12651
|
+
version: "0.15.20",
|
|
12442
12652
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
12443
12653
|
type: "module",
|
|
12444
12654
|
main: "dist/index.js",
|
|
@@ -12509,6 +12719,8 @@ var package_default = {
|
|
|
12509
12719
|
"dev:mcp": "bun run src/mcp/index.ts",
|
|
12510
12720
|
"dev:serve": "bun run src/server/index.ts",
|
|
12511
12721
|
"verify:release": "bun run scripts/verify-public-release.ts --mode=review",
|
|
12722
|
+
"verify:release-review": "bun run scripts/verify-npm-release-agent-review.ts",
|
|
12723
|
+
"issue:release-review": "bun run scripts/issue-npm-release-agent-review.ts",
|
|
12512
12724
|
prepublishOnly: "bun run scripts/verify-public-release.ts --mode=publish",
|
|
12513
12725
|
postinstall: "mkdir -p $HOME/.hasna/todos $HOME/.hasna/todos/training 2>/dev/null || true"
|
|
12514
12726
|
},
|
|
@@ -12636,6 +12848,9 @@ function assertPlanProjectLinkReceipt(value) {
|
|
|
12636
12848
|
return receipt;
|
|
12637
12849
|
}
|
|
12638
12850
|
|
|
12851
|
+
// src/storage/postgres-adapter.ts
|
|
12852
|
+
init_stale_lock_handoff();
|
|
12853
|
+
|
|
12639
12854
|
// src/storage/postgres-sync.ts
|
|
12640
12855
|
var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records";
|
|
12641
12856
|
var DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors";
|
|
@@ -13279,6 +13494,36 @@ function measureIntegrityRows(spec, sets) {
|
|
|
13279
13494
|
|
|
13280
13495
|
// src/storage/postgres-adapter.ts
|
|
13281
13496
|
init_redaction();
|
|
13497
|
+
|
|
13498
|
+
// src/storage/audit-history-import.ts
|
|
13499
|
+
var AUDIT_HISTORY_DIVERGENT_REPLAY = "AUDIT_HISTORY_DIVERGENT_REPLAY";
|
|
13500
|
+
var AUDIT_HISTORY_TOMBSTONE_FORBIDDEN = "AUDIT_HISTORY_TOMBSTONE_FORBIDDEN";
|
|
13501
|
+
var AUDIT_HISTORY_FIELDS = [
|
|
13502
|
+
"id",
|
|
13503
|
+
"task_id",
|
|
13504
|
+
"action",
|
|
13505
|
+
"field",
|
|
13506
|
+
"old_value",
|
|
13507
|
+
"new_value",
|
|
13508
|
+
"agent_id",
|
|
13509
|
+
"created_at",
|
|
13510
|
+
"machine_id"
|
|
13511
|
+
];
|
|
13512
|
+
function auditHistoryRowsAreFieldIdentical(left, right) {
|
|
13513
|
+
return AUDIT_HISTORY_FIELDS.every((field) => {
|
|
13514
|
+
const leftValue = field === "machine_id" ? left[field] ?? null : left[field];
|
|
13515
|
+
const rightValue = field === "machine_id" ? right[field] ?? null : right[field];
|
|
13516
|
+
return leftValue === rightValue;
|
|
13517
|
+
});
|
|
13518
|
+
}
|
|
13519
|
+
function divergentAuditHistoryReplayError(id) {
|
|
13520
|
+
return `${AUDIT_HISTORY_DIVERGENT_REPLAY}: immutable audit_history row ${id} differs from stored row`;
|
|
13521
|
+
}
|
|
13522
|
+
function forbiddenAuditHistoryTombstoneError(id) {
|
|
13523
|
+
return `${AUDIT_HISTORY_TOMBSTONE_FORBIDDEN}: audit_history tombstone ${id} is not allowed`;
|
|
13524
|
+
}
|
|
13525
|
+
|
|
13526
|
+
// src/storage/postgres-adapter.ts
|
|
13282
13527
|
function createPostgresTodosStorageAdapter(options) {
|
|
13283
13528
|
const store = new PostgresJsonRecordStore(options);
|
|
13284
13529
|
const adapter = {
|
|
@@ -13307,6 +13552,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
13307
13552
|
getChangedSince: (since, filters) => getChangedSince(since, filters, store),
|
|
13308
13553
|
lock: (id, agentId) => lockTask(id, agentId, store),
|
|
13309
13554
|
unlock: (id, agentId) => unlockTask(id, agentId, store),
|
|
13555
|
+
handoffStaleLock: (input, context) => store.handoffStaleLock(input, context),
|
|
13310
13556
|
getByFingerprint: (fingerprint) => store.getTaskByFingerprint(fingerprint)
|
|
13311
13557
|
},
|
|
13312
13558
|
dependencies: {
|
|
@@ -13453,6 +13699,91 @@ class PostgresJsonRecordStore {
|
|
|
13453
13699
|
LIMIT 1`, [this.service, type, id]);
|
|
13454
13700
|
return result.rows[0] ? payloadRecord2(result.rows[0].payload) : null;
|
|
13455
13701
|
}
|
|
13702
|
+
async handoffStaleLock(input, context = {}) {
|
|
13703
|
+
const prepared = prepareStaleLockHandoff(input);
|
|
13704
|
+
const receipt = buildStaleLockHandoffReceipt(prepared);
|
|
13705
|
+
const history = staleLockHandoffHistory(receipt, this.machineId(context));
|
|
13706
|
+
await this.ensureSchema();
|
|
13707
|
+
const result = await this.options.client.query(`/* todos:stale-lock-handoff-atomic */ WITH
|
|
13708
|
+
target AS MATERIALIZED (
|
|
13709
|
+
SELECT payload
|
|
13710
|
+
FROM ${this.tableName}
|
|
13711
|
+
WHERE service = $1
|
|
13712
|
+
AND object_type = 'tasks'
|
|
13713
|
+
AND object_id = $2
|
|
13714
|
+
AND deleted_at IS NULL
|
|
13715
|
+
FOR UPDATE
|
|
13716
|
+
),
|
|
13717
|
+
updated AS (
|
|
13718
|
+
UPDATE ${this.tableName} AS task_record
|
|
13719
|
+
SET payload = jsonb_set(
|
|
13720
|
+
jsonb_set(
|
|
13721
|
+
jsonb_set(
|
|
13722
|
+
jsonb_set(
|
|
13723
|
+
task_record.payload,
|
|
13724
|
+
'{locked_by}',
|
|
13725
|
+
to_jsonb($6::text),
|
|
13726
|
+
true
|
|
13727
|
+
),
|
|
13728
|
+
'{locked_at}',
|
|
13729
|
+
to_jsonb($7::text),
|
|
13730
|
+
true
|
|
13731
|
+
),
|
|
13732
|
+
'{updated_at}',
|
|
13733
|
+
to_jsonb($7::text),
|
|
13734
|
+
true
|
|
13735
|
+
),
|
|
13736
|
+
'{version}',
|
|
13737
|
+
to_jsonb(COALESCE((task_record.payload->>'version')::integer, 0) + 1),
|
|
13738
|
+
true
|
|
13739
|
+
),
|
|
13740
|
+
updated_at = $7::timestamptz,
|
|
13741
|
+
source_machine_id = $10,
|
|
13742
|
+
version = COALESCE(task_record.version, 0) + 1
|
|
13743
|
+
FROM target
|
|
13744
|
+
WHERE task_record.service = $1
|
|
13745
|
+
AND task_record.object_type = 'tasks'
|
|
13746
|
+
AND task_record.object_id = $2
|
|
13747
|
+
AND task_record.deleted_at IS NULL
|
|
13748
|
+
AND target.payload->>'locked_by' = $3
|
|
13749
|
+
AND target.payload->>'locked_at' = $4
|
|
13750
|
+
AND todos_try_timestamptz(target.payload->>'locked_at') < $5::timestamptz
|
|
13751
|
+
AND COALESCE(target.payload->>'status', '') NOT IN ('completed', 'failed', 'cancelled')
|
|
13752
|
+
RETURNING task_record.payload
|
|
13753
|
+
),
|
|
13754
|
+
audit AS (
|
|
13755
|
+
INSERT INTO ${this.tableName} (
|
|
13756
|
+
service, object_type, object_id, payload, updated_at,
|
|
13757
|
+
deleted_at, source_machine_id, version
|
|
13758
|
+
)
|
|
13759
|
+
SELECT $1, 'audit_history', $8, $9::jsonb, $7::timestamptz,
|
|
13760
|
+
NULL, $10, NULL
|
|
13761
|
+
FROM updated
|
|
13762
|
+
RETURNING payload
|
|
13763
|
+
)
|
|
13764
|
+
SELECT
|
|
13765
|
+
(SELECT payload FROM target) AS current_payload,
|
|
13766
|
+
(SELECT payload FROM updated) AS updated_payload,
|
|
13767
|
+
(SELECT payload FROM audit) AS audit_payload`, [
|
|
13768
|
+
this.service,
|
|
13769
|
+
prepared.task_id,
|
|
13770
|
+
prepared.expected_holder,
|
|
13771
|
+
prepared.expected_lock_version,
|
|
13772
|
+
prepared.stale_cutoff,
|
|
13773
|
+
prepared.new_holder,
|
|
13774
|
+
prepared.operation_timestamp,
|
|
13775
|
+
receipt.receipt_id,
|
|
13776
|
+
jsonbParam(history),
|
|
13777
|
+
this.machineId(context)
|
|
13778
|
+
]);
|
|
13779
|
+
const row = result.rows[0];
|
|
13780
|
+
if (!row?.current_payload)
|
|
13781
|
+
throw new TaskNotFoundError(prepared.task_id);
|
|
13782
|
+
if (!row.updated_payload || !row.audit_payload) {
|
|
13783
|
+
throwStaleLockHandoffConflict(payloadRecord2(row.current_payload), prepared);
|
|
13784
|
+
}
|
|
13785
|
+
return receipt;
|
|
13786
|
+
}
|
|
13456
13787
|
async list(type) {
|
|
13457
13788
|
return (await this.listRecords(type)).map((record) => record.payload);
|
|
13458
13789
|
}
|
|
@@ -13722,6 +14053,28 @@ class PostgresJsonRecordStore {
|
|
|
13722
14053
|
}
|
|
13723
14054
|
return value;
|
|
13724
14055
|
}
|
|
14056
|
+
async insertImmutableAuditHistory(value, context = {}) {
|
|
14057
|
+
await this.ensureSchema();
|
|
14058
|
+
const inserted = await this.options.client.query(`INSERT INTO ${this.tableName} (
|
|
14059
|
+
service, object_type, object_id, payload, updated_at,
|
|
14060
|
+
deleted_at, source_machine_id, version
|
|
14061
|
+
) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, NULL, $6, NULL)
|
|
14062
|
+
ON CONFLICT (service, object_type, object_id) DO NOTHING
|
|
14063
|
+
RETURNING object_id`, [
|
|
14064
|
+
this.service,
|
|
14065
|
+
"audit_history",
|
|
14066
|
+
value.id,
|
|
14067
|
+
jsonbParam(value),
|
|
14068
|
+
value.created_at,
|
|
14069
|
+
context.requestId ?? this.sourceMachineId ?? null
|
|
14070
|
+
]);
|
|
14071
|
+
if (inserted.rows.length > 0)
|
|
14072
|
+
return "inserted";
|
|
14073
|
+
const existing = await this.get("audit_history", value.id);
|
|
14074
|
+
if (existing && auditHistoryRowsAreFieldIdentical(existing, value))
|
|
14075
|
+
return "identical";
|
|
14076
|
+
throw new Error(divergentAuditHistoryReplayError(value.id));
|
|
14077
|
+
}
|
|
13725
14078
|
async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}) {
|
|
13726
14079
|
const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
|
|
13727
14080
|
if (planIds.length === 0)
|
|
@@ -15213,6 +15566,11 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
15213
15566
|
result.errors.push(...validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists));
|
|
15214
15567
|
if (result.errors.length > 0)
|
|
15215
15568
|
return result;
|
|
15569
|
+
const auditHistory = await preflightAuditHistoryImport(snapshot.auditHistory, snapshot.tombstones ?? [], store);
|
|
15570
|
+
result.errors.push(...auditHistory.errors);
|
|
15571
|
+
if (result.errors.length > 0)
|
|
15572
|
+
return result;
|
|
15573
|
+
result.skipped += auditHistory.identical;
|
|
15216
15574
|
const entries = [
|
|
15217
15575
|
...snapshot.tasks.map((row) => ["tasks", row]),
|
|
15218
15576
|
...snapshot.projects.map((row) => ["projects", row]),
|
|
@@ -15221,9 +15579,20 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
15221
15579
|
...snapshot.agents.map((row) => ["agents", row]),
|
|
15222
15580
|
...snapshot.taskLists.map((row) => ["task_lists", row]),
|
|
15223
15581
|
...snapshot.templates.map((row) => ["templates", row]),
|
|
15224
|
-
...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row])
|
|
15225
|
-
...snapshot.auditHistory.map((row) => ["audit_history", row])
|
|
15582
|
+
...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row])
|
|
15226
15583
|
];
|
|
15584
|
+
for (const row of auditHistory.rowsToInsert) {
|
|
15585
|
+
try {
|
|
15586
|
+
const outcome = await store.insertImmutableAuditHistory(row, context);
|
|
15587
|
+
if (outcome === "inserted")
|
|
15588
|
+
result.inserted += 1;
|
|
15589
|
+
else
|
|
15590
|
+
result.skipped += 1;
|
|
15591
|
+
} catch (error) {
|
|
15592
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
15593
|
+
return result;
|
|
15594
|
+
}
|
|
15595
|
+
}
|
|
15227
15596
|
for (const [type, row] of entries) {
|
|
15228
15597
|
try {
|
|
15229
15598
|
const existing = await store.get(type, row.id);
|
|
@@ -15257,6 +15626,32 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
15257
15626
|
}
|
|
15258
15627
|
return result;
|
|
15259
15628
|
}
|
|
15629
|
+
async function preflightAuditHistoryImport(rows, tombstones, store) {
|
|
15630
|
+
const errors = tombstones.filter((tombstone) => tombstone.object_type === "audit_history").map((tombstone) => forbiddenAuditHistoryTombstoneError(tombstone.object_id));
|
|
15631
|
+
const rowsToInsert = [];
|
|
15632
|
+
const seen = new Map;
|
|
15633
|
+
let identical = 0;
|
|
15634
|
+
for (const row of rows) {
|
|
15635
|
+
const prior = seen.get(row.id);
|
|
15636
|
+
if (prior) {
|
|
15637
|
+
if (auditHistoryRowsAreFieldIdentical(prior, row))
|
|
15638
|
+
identical += 1;
|
|
15639
|
+
else
|
|
15640
|
+
errors.push(divergentAuditHistoryReplayError(row.id));
|
|
15641
|
+
continue;
|
|
15642
|
+
}
|
|
15643
|
+
seen.set(row.id, row);
|
|
15644
|
+
const existing = await store.get("audit_history", row.id);
|
|
15645
|
+
if (!existing) {
|
|
15646
|
+
rowsToInsert.push(row);
|
|
15647
|
+
} else if (auditHistoryRowsAreFieldIdentical(existing, row)) {
|
|
15648
|
+
identical += 1;
|
|
15649
|
+
} else {
|
|
15650
|
+
errors.push(divergentAuditHistoryReplayError(row.id));
|
|
15651
|
+
}
|
|
15652
|
+
}
|
|
15653
|
+
return { rowsToInsert, identical, errors };
|
|
15654
|
+
}
|
|
15260
15655
|
async function requireRecord(type, id, store) {
|
|
15261
15656
|
const record = await store.get(type, id);
|
|
15262
15657
|
if (!record)
|
|
@@ -16981,8 +17376,11 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
16981
17376
|
const existingTaskLists = d.query("SELECT id, project_id, slug FROM task_lists").all();
|
|
16982
17377
|
result.errors.push(...validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists));
|
|
16983
17378
|
}
|
|
17379
|
+
const auditImport = preflightAuditHistoryImport2(d, snapshot.auditHistory, snapshot.tombstones ?? []);
|
|
17380
|
+
result.errors.push(...auditImport.errors);
|
|
16984
17381
|
if (result.errors.length > 0)
|
|
16985
17382
|
return result;
|
|
17383
|
+
result.skipped += auditImport.identicalReplayCount;
|
|
16986
17384
|
const applyRows = (objectType, table, columns, rows, updateClockColumn, acceptEqualClock = true, afterUpsert) => {
|
|
16987
17385
|
for (const row of rows) {
|
|
16988
17386
|
try {
|
|
@@ -17017,10 +17415,72 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
17017
17415
|
replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
|
|
17018
17416
|
}
|
|
17019
17417
|
});
|
|
17020
|
-
|
|
17418
|
+
insertAuditHistoryRows(d, auditImport.rowsToInsert, result);
|
|
17021
17419
|
applyTombstones(d, snapshot.tombstones ?? [], result);
|
|
17022
17420
|
return result;
|
|
17023
17421
|
}
|
|
17422
|
+
function preflightAuditHistoryImport2(db, rows, tombstones) {
|
|
17423
|
+
const errors = tombstones.filter((tombstone) => tombstone.object_type === "audit_history").map((tombstone) => forbiddenAuditHistoryTombstoneError(tombstone.object_id));
|
|
17424
|
+
const rowsToInsert = [];
|
|
17425
|
+
const seen = new Map;
|
|
17426
|
+
let identicalReplayCount = 0;
|
|
17427
|
+
for (const rawRow of rows) {
|
|
17428
|
+
try {
|
|
17429
|
+
const row = asRecord(rawRow);
|
|
17430
|
+
if (typeof row.id !== "string" || !row.id) {
|
|
17431
|
+
throw new Error("task_history row is missing id");
|
|
17432
|
+
}
|
|
17433
|
+
const prior = seen.get(row.id);
|
|
17434
|
+
if (prior) {
|
|
17435
|
+
if (auditHistoryRowsAreFieldIdentical(prior, row))
|
|
17436
|
+
identicalReplayCount += 1;
|
|
17437
|
+
else
|
|
17438
|
+
errors.push(divergentAuditHistoryReplayError(row.id));
|
|
17439
|
+
continue;
|
|
17440
|
+
}
|
|
17441
|
+
seen.set(row.id, row);
|
|
17442
|
+
const existing = getAuditHistoryById(db, row.id);
|
|
17443
|
+
if (!existing) {
|
|
17444
|
+
rowsToInsert.push(row);
|
|
17445
|
+
} else if (auditHistoryRowsAreFieldIdentical(existing, row)) {
|
|
17446
|
+
identicalReplayCount += 1;
|
|
17447
|
+
} else {
|
|
17448
|
+
errors.push(divergentAuditHistoryReplayError(row.id));
|
|
17449
|
+
}
|
|
17450
|
+
} catch (error) {
|
|
17451
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
17452
|
+
}
|
|
17453
|
+
}
|
|
17454
|
+
return { rowsToInsert, identicalReplayCount, errors };
|
|
17455
|
+
}
|
|
17456
|
+
function insertAuditHistoryRows(db, rows, result) {
|
|
17457
|
+
for (const rawRow of rows) {
|
|
17458
|
+
try {
|
|
17459
|
+
const row = asRecord(rawRow);
|
|
17460
|
+
const presentColumns = AUDIT_COLUMNS.filter((column) => (column in row));
|
|
17461
|
+
if (!presentColumns.includes("id"))
|
|
17462
|
+
presentColumns.unshift("id");
|
|
17463
|
+
const placeholders = presentColumns.map(() => "?").join(", ");
|
|
17464
|
+
const values = presentColumns.map((column) => valueForColumn(column, row[column]));
|
|
17465
|
+
const changes = db.run(`INSERT OR IGNORE INTO task_history (${presentColumns.join(", ")}) VALUES (${placeholders})`, values).changes;
|
|
17466
|
+
if (changes > 0) {
|
|
17467
|
+
result.inserted += 1;
|
|
17468
|
+
continue;
|
|
17469
|
+
}
|
|
17470
|
+
const existing = getAuditHistoryById(db, String(row["id"]));
|
|
17471
|
+
if (existing && auditHistoryRowsAreFieldIdentical(existing, row)) {
|
|
17472
|
+
result.skipped += 1;
|
|
17473
|
+
} else {
|
|
17474
|
+
result.errors.push(divergentAuditHistoryReplayError(String(row["id"])));
|
|
17475
|
+
}
|
|
17476
|
+
} catch (error) {
|
|
17477
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
17478
|
+
}
|
|
17479
|
+
}
|
|
17480
|
+
}
|
|
17481
|
+
function getAuditHistoryById(db, id) {
|
|
17482
|
+
return db.query(`SELECT ${AUDIT_COLUMNS.join(", ")} FROM task_history WHERE id = ? LIMIT 1`).get(id);
|
|
17483
|
+
}
|
|
17024
17484
|
function upsertById(db, table, columns, row, updateClockColumn, acceptEqualClock = true) {
|
|
17025
17485
|
const id = row["id"];
|
|
17026
17486
|
if (typeof id !== "string" || !id)
|
|
@@ -17118,7 +17578,7 @@ function tableForTombstone(objectType) {
|
|
|
17118
17578
|
return "task_templates";
|
|
17119
17579
|
if (objectType === "template_tasks")
|
|
17120
17580
|
return "template_tasks";
|
|
17121
|
-
|
|
17581
|
+
throw new Error(`unsupported storage tombstone object_type: ${String(objectType)}`);
|
|
17122
17582
|
}
|
|
17123
17583
|
function listRows(db, table, columns) {
|
|
17124
17584
|
return db.query(`SELECT ${columns.join(", ")} FROM ${table} ORDER BY id`).all();
|
|
@@ -17250,6 +17710,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
17250
17710
|
unlockTask2(id, agentId, database());
|
|
17251
17711
|
return true;
|
|
17252
17712
|
},
|
|
17713
|
+
handoffStaleLock: (input) => handoffStaleTaskLock(input, database()),
|
|
17253
17714
|
delete: (id) => deleteTask(id, database()),
|
|
17254
17715
|
start: (id, agentId) => startTask2(id, agentId, database()),
|
|
17255
17716
|
complete: (id, agentId, options2) => completeTask2(id, agentId, database(), options2),
|