@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.
Files changed (43) hide show
  1. package/dist/cli/cloud-router.d.ts +20 -1
  2. package/dist/cli/cloud-router.d.ts.map +1 -1
  3. package/dist/cli/commands/query-commands.d.ts.map +1 -1
  4. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  5. package/dist/cli/index.js +851 -47
  6. package/dist/cli/stage-a.d.ts +21 -10
  7. package/dist/cli/stage-a.d.ts.map +1 -1
  8. package/dist/contracts.js +228 -16
  9. package/dist/db/audit.d.ts +8 -0
  10. package/dist/db/audit.d.ts.map +1 -1
  11. package/dist/db/task-lifecycle.d.ts +7 -1
  12. package/dist/db/task-lifecycle.d.ts.map +1 -1
  13. package/dist/db/tasks.d.ts +1 -1
  14. package/dist/db/tasks.d.ts.map +1 -1
  15. package/dist/index.js +479 -20
  16. package/dist/lib/cli-help.d.ts +3 -2
  17. package/dist/lib/cli-help.d.ts.map +1 -1
  18. package/dist/lib/stale-lock-handoff.d.ts +25 -0
  19. package/dist/lib/stale-lock-handoff.d.ts.map +1 -0
  20. package/dist/mcp/index.js +702 -36
  21. package/dist/mcp.js +3 -1
  22. package/dist/project-registration.js +1147 -686
  23. package/dist/registry.js +228 -16
  24. package/dist/release-provenance.json +5 -5
  25. package/dist/sdk/index.js +7 -0
  26. package/dist/sdk/v1.generated.d.ts +27 -0
  27. package/dist/sdk/v1.generated.d.ts.map +1 -1
  28. package/dist/server/index.js +1035 -369
  29. package/dist/server/openapi.d.ts +182 -0
  30. package/dist/server/openapi.d.ts.map +1 -1
  31. package/dist/server/v1.d.ts.map +1 -1
  32. package/dist/storage/audit-history-import.d.ts +14 -0
  33. package/dist/storage/audit-history-import.d.ts.map +1 -0
  34. package/dist/storage/interfaces.d.ts +12 -1
  35. package/dist/storage/interfaces.d.ts.map +1 -1
  36. package/dist/storage/local-sqlite.d.ts.map +1 -1
  37. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  38. package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
  39. package/dist/storage.js +476 -19
  40. package/dist/task-manifest.js +11 -1
  41. package/dist/types/index.d.ts +43 -0
  42. package/dist/types/index.d.ts.map +1 -1
  43. 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/project-registration/schema.ts
628
- function sqliteTodosProjectRegistrationSchemaSql() {
629
- return `
630
- CREATE TABLE IF NOT EXISTS todos_project_registration_receipts (
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 postgresTodosProjectRegistrationSchemaSql() {
729
- return [
730
- `CREATE TABLE IF NOT EXISTS todos_project_registration_receipts (
731
- receipt_id text PRIMARY KEY,
732
- authority text NOT NULL CHECK(authority = 'todos'),
733
- route text NOT NULL,
734
- package_version text NOT NULL,
735
- authority_id text NOT NULL,
736
- tenant_id text NOT NULL,
737
- corpus_id text NOT NULL,
738
- operation_id text NOT NULL,
739
- step_id text NOT NULL,
740
- resource_kind text NOT NULL CHECK(resource_kind IN ('project', 'task_list')),
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
- // src/db/migrations.ts
828
- var MIGRATIONS;
829
- var init_migrations = __esm(() => {
830
- MIGRATIONS = [
831
- `
832
- CREATE TABLE IF NOT EXISTS projects (
833
- id TEXT PRIMARY KEY,
834
- name TEXT NOT NULL,
835
- path TEXT UNIQUE NOT NULL,
836
- description TEXT,
837
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
838
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
839
- );
840
-
841
- CREATE TABLE IF NOT EXISTS tasks (
842
- id TEXT PRIMARY KEY,
843
- project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
844
- parent_id TEXT REFERENCES tasks(id) ON DELETE CASCADE,
845
- title TEXT NOT NULL,
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 existsSync3 } from "fs";
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 && !existsSync3(localRow.path)) {
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) && !existsSync3(project.path)) {
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 existsSync4, mkdirSync as mkdirSync2 } from "fs";
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 (existsSync4(candidate))
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 (existsSync4(join3(dir, ".git")))
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 (!existsSync4(dir)) {
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 existsSync5 } from "fs";
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 (!existsSync5(path)) {
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 isAllowlisted(text, match, allowlist) {
7102
- const context = text.slice(Math.max(0, text.indexOf(match) - 20), text.indexOf(match) + match.length + 20);
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 scanTextForSecrets(text, options = {}) {
7106
- const allowlist = [...DEFAULT_ALLOWLIST, ...options.allowlist ?? []];
7107
- const matches = [];
7108
- const patterns = [
7109
- ...DEFAULT_PATTERNS,
7110
- ...options.custom_patterns?.map((p, i) => ({ name: `custom_${i}`, pattern: p })) ?? []
7111
- ];
7112
- for (const { name, pattern, allowlist_ok } of patterns) {
7113
- const re = new RegExp(pattern.source, pattern.flags);
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 redactText(text, options = {}) {
7133
- const placeholder = options.placeholder ?? REDACTION_PLACEHOLDER2;
7134
- let out = text;
7135
- for (const { pattern, allowlist_ok } of DEFAULT_PATTERNS) {
7136
- out = out.replace(new RegExp(pattern.source, pattern.flags), (match) => {
7137
- if (allowlist_ok && isAllowlisted(out, match, [...DEFAULT_ALLOWLIST, ...options.allowlist ?? []])) {
7138
- return match;
7139
- }
7140
- return placeholder;
7141
- });
7142
- }
7143
- for (const custom of options.custom_patterns ?? []) {
7144
- out = out.replace(custom, placeholder);
7145
- }
7146
- for (const fn of customRedactors) {
7147
- out = fn(out);
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
- return out;
7281
+ if (task.requires_approval)
7282
+ result.requires_approval = true;
7283
+ return Object.keys(result).length > 0 ? result : undefined;
7150
7284
  }
7151
- function scanAndRedactText(text, options = {}) {
7152
- const scan = scanTextForSecrets(text, options);
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
- ...scan,
7155
- redacted_text: redactText(text, options)
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 scanFileForSecrets(path, options = {}) {
7159
- if (!existsSync6(path))
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 assertNoSecrets(text, context) {
7170
- const scan = scanTextForSecrets(text);
7171
- if (!scan.clean) {
7172
- const summary = scan.matches.map((m) => `${m.pattern}@${m.line ?? m.index}`).join(", ");
7173
- throw new Error(`Secret pattern detected${context ? ` in ${context}` : ""}: ${summary}`);
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 redactCommentContent(content) {
7177
- return redactText(content);
7178
- }
7179
- function redactHandoffPayload(payload) {
7180
- return redactValue(payload);
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 base;
7326
+ return null;
7190
7327
  }
7191
- function getDefaultSecretPatterns() {
7192
- return DEFAULT_PATTERNS.map((p) => ({ name: p.name, source: p.pattern.source }));
7328
+ function classifyProjectKind(_path, metadata) {
7329
+ return projectKindFromMetadata(metadata);
7193
7330
  }
7194
- var SECRET_REDACTION_SCHEMA, REDACTION_PLACEHOLDER2 = "[REDACTED]", DEFAULT_PATTERNS, DEFAULT_ALLOWLIST, customRedactors;
7195
- var init_secret_redaction = __esm(() => {
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 redactPreWriteText(value) {
7237
- return redactText(redactEvidenceText(value));
7334
+ function inferRootProjectId(project) {
7335
+ return isWorktreePath(project.path) ? null : project.id;
7238
7336
  }
7239
- function scanPreWriteText(value, context = "text") {
7240
- const redacted = redactPreWriteText(value);
7241
- if (redacted === value) {
7242
- return {
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
- schema_version: PREWRITE_SECRET_SCAN_SCHEMA,
7261
- clean: false,
7262
- context,
7263
- findings: mergeFindings([...structural, ...configured])
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 assertPreWriteTextClean(value, context = "text") {
7267
- const scan = scanPreWriteText(value, context);
7268
- if (!scan.clean)
7269
- throw new PreWriteSecretError(context, scan.findings);
7270
- }
7271
- function sanitizePreWriteText(value, context = "text", options = {}) {
7272
- const mode = options.mode ?? "redact";
7273
- if (mode === "block") {
7274
- assertPreWriteTextClean(value, context);
7275
- return value;
7276
- }
7277
- return redactPreWriteText(value);
7278
- }
7279
- function sanitizePreWriteValue(value, context = "value", options = {}) {
7280
- if (typeof value === "string")
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
- if (value && typeof value === "object") {
7286
- const sanitized = {};
7287
- for (const [key, child] of Object.entries(value)) {
7288
- const safeKey = sanitizePreWriteText(key, `${context}.$key`, options);
7289
- sanitized[safeKey] = sanitizePreWriteValue(child, `${context}.${safeKey}`, options);
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
- return redactValue(sanitized);
7292
- }
7293
- return value;
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 sanitizePreWriteJsonString(value, context = "json") {
7296
- if (value === undefined || value === null)
7431
+ function readMachineLocalPath(project) {
7432
+ const machineId = process.env["TODOS_MACHINE_ID"];
7433
+ if (!machineId)
7297
7434
  return null;
7298
7435
  try {
7299
- return JSON.stringify(sanitizePreWriteValue(JSON.parse(value), context));
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 sanitizePreWriteText(value, context);
7439
+ return null;
7302
7440
  }
7303
7441
  }
7304
- var PREWRITE_SECRET_SCAN_SCHEMA, PreWriteSecretError;
7305
- var init_prewrite_secrets = __esm(() => {
7306
- init_redaction();
7307
- init_secret_redaction();
7308
- PREWRITE_SECRET_SCAN_SCHEMA = ["todos", "prewrite_secret_scan", "v1"].join(".");
7309
- PreWriteSecretError = class PreWriteSecretError extends Error {
7310
- context;
7311
- findings;
7312
- constructor(context, findings) {
7313
- const summary = findings.map((finding) => `${finding.pattern}:${finding.count}`).join(", ");
7314
- super(`Secret pattern detected before persistence in ${context}: ${summary}`);
7315
- this.name = "PreWriteSecretError";
7316
- this.context = context;
7317
- this.findings = findings;
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 logTaskChange2(taskId, action, field, oldValue, newValue, agentId, db) {
7663
+ function insertTaskHistory(entry2, db) {
7515
7664
  const d = db || getDatabase();
7516
- const id = uuid();
7517
- const timestamp2 = now();
7518
- const machineId = currentStorageMachineId(d);
7519
- const safeOldValue = sanitizeHistoryValue(oldValue, "task_history.old_value");
7520
- const safeNewValue = sanitizeHistoryValue(newValue, "task_history.new_value");
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 (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field || null, safeOldValue, safeNewValue, agentId || null, timestamp2, machineId]);
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: taskId,
7528
- action,
7529
- field,
7530
- old_value: safeOldValue,
7531
- new_value: safeNewValue,
7532
- actor_id: agentId ?? undefined
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 { id, task_id: taskId, action, field: field || null, old_value: safeOldValue, new_value: safeNewValue, agent_id: agentId || null, created_at: timestamp2, machine_id: machineId };
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.19",
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
- applyRows("audit_history", "task_history", AUDIT_COLUMNS, snapshot.auditHistory);
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
- return "task_history";
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),