@hasna/todos 0.11.45 → 0.11.46

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 (57) hide show
  1. package/README.md +125 -3
  2. package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
  3. package/dist/cli/commands/help-commands.d.ts +3 -0
  4. package/dist/cli/commands/help-commands.d.ts.map +1 -0
  5. package/dist/cli/commands/local-backup-commands.d.ts +3 -0
  6. package/dist/cli/commands/local-backup-commands.d.ts.map +1 -0
  7. package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
  8. package/dist/cli/commands/query-commands.d.ts.map +1 -1
  9. package/dist/cli/commands/scale-hardening-commands.d.ts +3 -0
  10. package/dist/cli/commands/scale-hardening-commands.d.ts.map +1 -0
  11. package/dist/cli/commands/usage-ledger-commands.d.ts +3 -0
  12. package/dist/cli/commands/usage-ledger-commands.d.ts.map +1 -0
  13. package/dist/cli/components/Dashboard.d.ts.map +1 -1
  14. package/dist/cli/index.js +3822 -547
  15. package/dist/cli-mcp-parity.d.ts +1 -1
  16. package/dist/cli-mcp-parity.d.ts.map +1 -1
  17. package/dist/contracts.d.ts +6 -0
  18. package/dist/contracts.d.ts.map +1 -1
  19. package/dist/contracts.js +1506 -24
  20. package/dist/db/agent-names.d.ts +2 -1
  21. package/dist/db/agent-names.d.ts.map +1 -1
  22. package/dist/db/schema.d.ts.map +1 -1
  23. package/dist/db/task-runs.d.ts +3 -0
  24. package/dist/db/task-runs.d.ts.map +1 -1
  25. package/dist/index.d.ts +16 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2822 -282
  28. package/dist/json-contracts.d.ts.map +1 -1
  29. package/dist/lib/cli-help.d.ts +38 -0
  30. package/dist/lib/cli-help.d.ts.map +1 -0
  31. package/dist/lib/config.d.ts +38 -0
  32. package/dist/lib/config.d.ts.map +1 -1
  33. package/dist/lib/local-backups.d.ts +129 -0
  34. package/dist/lib/local-backups.d.ts.map +1 -0
  35. package/dist/lib/local-extensions.d.ts +18 -1
  36. package/dist/lib/local-extensions.d.ts.map +1 -1
  37. package/dist/lib/local-reports.d.ts +149 -0
  38. package/dist/lib/local-reports.d.ts.map +1 -0
  39. package/dist/lib/redaction.d.ts.map +1 -1
  40. package/dist/lib/scale-hardening.d.ts +74 -0
  41. package/dist/lib/scale-hardening.d.ts.map +1 -0
  42. package/dist/lib/tui-dashboard.d.ts +49 -0
  43. package/dist/lib/tui-dashboard.d.ts.map +1 -0
  44. package/dist/lib/usage-ledger.d.ts +82 -0
  45. package/dist/lib/usage-ledger.d.ts.map +1 -0
  46. package/dist/lib/workflow-states.d.ts +70 -0
  47. package/dist/lib/workflow-states.d.ts.map +1 -0
  48. package/dist/mcp/index.js +8245 -6445
  49. package/dist/mcp/token-utils.d.ts.map +1 -1
  50. package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
  51. package/dist/mcp/tools/task-resources.d.ts.map +1 -1
  52. package/dist/mcp.js +12 -0
  53. package/dist/registry.js +1487 -24
  54. package/dist/server/index.js +152 -20
  55. package/dist/storage.js +164 -21
  56. package/package.json +1 -1
  57. package/dist/release-provenance.json +0 -7
package/dist/index.js CHANGED
@@ -1709,6 +1709,10 @@ function ensureSchema(db) {
1709
1709
  ensureColumn("dispatches", "machine_id", "TEXT");
1710
1710
  ensureColumn("dispatches", "synced_at", "TEXT");
1711
1711
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id)");
1712
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id)");
1713
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)");
1714
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_archived_at ON tasks(archived_at)");
1715
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_updated_at ON tasks(updated_at)");
1712
1716
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_task_list ON tasks(task_list_id)");
1713
1717
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_due_at ON tasks(due_at)");
1714
1718
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_short_id ON tasks(short_id) WHERE short_id IS NOT NULL");
@@ -2805,7 +2809,9 @@ var MCP_TOOL_GROUPS = {
2805
2809
  "create_risk",
2806
2810
  "create_handoff",
2807
2811
  "capture_environment_snapshot",
2812
+ "check_local_integrity",
2808
2813
  "compare_environment_snapshots",
2814
+ "create_local_backup",
2809
2815
  "create_inbox_item",
2810
2816
  "delete_comment",
2811
2817
  "detect_file_relationships",
@@ -2825,6 +2831,7 @@ var MCP_TOOL_GROUPS = {
2825
2831
  "add_task_run_event",
2826
2832
  "add_task_run_file",
2827
2833
  "acknowledge_handoff",
2834
+ "build_local_report",
2828
2835
  "cancel_agent_run_dispatch",
2829
2836
  "finish_task_run",
2830
2837
  "find_duplicate_tasks",
@@ -2844,9 +2851,11 @@ var MCP_TOOL_GROUPS = {
2844
2851
  "close_risk",
2845
2852
  "get_task_git_refs",
2846
2853
  "get_task_run_ledger",
2854
+ "get_usage_ledger",
2847
2855
  "list_agent_run_adapters",
2848
2856
  "list_agent_run_queue",
2849
2857
  "verify_task_run_artifacts",
2858
+ "verify_local_backup",
2850
2859
  "get_task_traceability",
2851
2860
  "get_task_commits",
2852
2861
  "get_task_dependencies",
@@ -2863,6 +2872,7 @@ var MCP_TOOL_GROUPS = {
2863
2872
  "list_handoffs",
2864
2873
  "list_inbox_items",
2865
2874
  "list_knowledge_records",
2875
+ "list_local_report_types",
2866
2876
  "list_onboarding_fixtures",
2867
2877
  "list_review_routing_rules",
2868
2878
  "list_local_snapshots",
@@ -2874,6 +2884,7 @@ var MCP_TOOL_GROUPS = {
2874
2884
  "queue_agent_run",
2875
2885
  "remove_agent_run_adapter",
2876
2886
  "remove_review_routing_rule",
2887
+ "restore_local_backup",
2877
2888
  "retry_agent_run_dispatch",
2878
2889
  "resolve_mentions",
2879
2890
  "run_next_agent_dispatch",
@@ -2884,6 +2895,7 @@ var MCP_TOOL_GROUPS = {
2884
2895
  "set_review_routing_rule",
2885
2896
  "set_verification_provider",
2886
2897
  "simulate_agent_replay",
2898
+ "discover_local_extensions",
2887
2899
  "inspect_local_extension",
2888
2900
  "install_local_extension",
2889
2901
  "list_local_extensions",
@@ -2951,11 +2963,15 @@ var MCP_TOOL_GROUPS = {
2951
2963
  "get_task_graph",
2952
2964
  "get_task_history",
2953
2965
  "get_task_stats",
2966
+ "list_workflow_states",
2954
2967
  "list_labels",
2955
2968
  "list_tags",
2969
+ "migrate_workflow_states",
2970
+ "query_tasks_by_workflow_state",
2956
2971
  "query_tasks_by_fields",
2957
2972
  "search_tools",
2958
2973
  "describe_tools",
2974
+ "set_task_workflow_state",
2959
2975
  "set_task_fields",
2960
2976
  "update_label",
2961
2977
  "update_tag"
@@ -3445,6 +3461,161 @@ var TODOS_JSON_CONTRACTS = [
3445
3461
  },
3446
3462
  optional: {}
3447
3463
  }),
3464
+ contract({
3465
+ id: "local_usage_ledger",
3466
+ name: "Local Usage Ledger",
3467
+ description: "Aggregate local report for task, project, run, command, cost, duration, storage, and simulated quota usage.",
3468
+ surfaces: ["cli", "mcp", "sdk"],
3469
+ stability: "stable",
3470
+ required: {
3471
+ schema_version: field("integer", "Report schema version."),
3472
+ local_only: field("boolean", "Always true; report reads only local todos state."),
3473
+ no_network: field("boolean", "Always true; report does not call hosted services."),
3474
+ generated_at: isoDateField,
3475
+ scope: field("object", "Project, agent, and time filters used for the aggregate."),
3476
+ counts: field("object", "Counts for tasks, projects, runs, commands, artifacts, traces, and usage metadata records."),
3477
+ durations: field("object", "Completed run, open run, trace, and total observed duration in milliseconds."),
3478
+ usage: field("object", "Token and USD aggregates from task cost fields, traces, and agent-provided metadata."),
3479
+ storage: field("object", "Evidence storage byte totals from local run artifacts."),
3480
+ quota: field("object", "Optional local quota simulation with exceeded limits."),
3481
+ redaction: field("object", "Aggregate-only guarantees for command and artifact path omission."),
3482
+ sources: field("array", "Local SQLite tables included in the report.")
3483
+ },
3484
+ optional: {}
3485
+ }),
3486
+ contract({
3487
+ id: "local_report",
3488
+ name: "Local Agent Report",
3489
+ description: "Local-only report composing ready, blocked, overdue, plan, run, verification, and agent summaries.",
3490
+ surfaces: ["cli", "mcp", "sdk"],
3491
+ stability: "stable",
3492
+ required: {
3493
+ schema_version: field("integer", "Report schema version."),
3494
+ local_only: field("boolean", "Always true; report reads only local todos state."),
3495
+ no_network: field("boolean", "Always true; report does not call hosted analytics or usage collection."),
3496
+ generated_at: isoDateField,
3497
+ scope: field("object", "Project, plan, agent, and time filters used to build the report."),
3498
+ report_types: field("array", "Stable local report sections included by this package."),
3499
+ views: field("object", "Ready, blocked, and overdue task views."),
3500
+ plans: field("array", "Plan progress summaries with blocked and overdue counts."),
3501
+ runs: field("object", "Run outcome counts and recent run evidence summaries."),
3502
+ verification: field("object", "Verification outcome counts and recent verification evidence summaries."),
3503
+ agents: field("array", "Per-agent task, run, and verification summaries."),
3504
+ exports: field("object", "JSON contract and Markdown support metadata.")
3505
+ },
3506
+ optional: {}
3507
+ }),
3508
+ contract({
3509
+ id: "local_backup_bundle",
3510
+ name: "Local Backup Bundle",
3511
+ description: "Local backup wrapper around a bridge bundle with manifest counts, section checksums, SQLite integrity metadata, and artifact-content coverage.",
3512
+ surfaces: ["cli", "mcp", "sdk"],
3513
+ stability: "stable",
3514
+ required: {
3515
+ schema_version: field("integer", "Backup schema version."),
3516
+ kind: field("string", "Backup bundle kind identifier."),
3517
+ local_only: field("boolean", "Always true; backup creation reads only local state."),
3518
+ no_network: field("boolean", "Always true; backup creation performs no network requests."),
3519
+ created_at: isoDateField,
3520
+ package: field("object", "Package source metadata."),
3521
+ manifest: field("object", "Backup manifest with checksums, counts, source scope, and SQLite integrity."),
3522
+ bridge: field("object", "Embedded local bridge bundle containing tasks, projects, plans, runs, comments, evidence, and stored artifact content."),
3523
+ checksum_algorithm: field("string", "Digest algorithm used for backup and bridge checksums."),
3524
+ checksum: field("string", "SHA-256 checksum of the backup payload excluding this field.")
3525
+ },
3526
+ optional: {}
3527
+ }),
3528
+ contract({
3529
+ id: "local_backup_verification",
3530
+ name: "Local Backup Verification",
3531
+ description: "Verification report for a local backup bundle covering backup checksum, bridge checksum, manifest counts, schema compatibility, and current SQLite integrity.",
3532
+ surfaces: ["cli", "mcp", "sdk"],
3533
+ stability: "stable",
3534
+ required: {
3535
+ schema_version: field("integer", "Verification schema version."),
3536
+ kind: field("string", "Verification report kind identifier."),
3537
+ local_only: field("boolean", "Always true; verification reads local files and SQLite only."),
3538
+ no_network: field("boolean", "Always true; verification performs no network requests."),
3539
+ verified_at: isoDateField,
3540
+ ok: field("boolean", "True when all checks pass."),
3541
+ checksum_algorithm: field("string", "Digest algorithm used for backup and bridge checksums."),
3542
+ checksum: field("object", "Expected and actual backup checksum status."),
3543
+ bridge_checksum: field("object", "Expected and actual bridge checksum status."),
3544
+ bridge_validation: field("object", "Embedded bridge schema validation result."),
3545
+ sqlite: field(["object", "null"], "Current SQLite integrity check, or null when skipped.", true),
3546
+ counts: field("object", "Manifest expected counts, actual bridge counts, and status."),
3547
+ compatible: field("boolean", "True when embedded bridge schema is compatible with this package."),
3548
+ issues: field("array", "Blocking verification issues."),
3549
+ warnings: field("array", "Non-blocking local warnings.")
3550
+ },
3551
+ optional: {}
3552
+ }),
3553
+ contract({
3554
+ id: "local_backup_restore_result",
3555
+ name: "Local Backup Restore Result",
3556
+ description: "Dry-run or applied local backup restore result with verification and bridge import details.",
3557
+ surfaces: ["cli", "mcp", "sdk"],
3558
+ stability: "stable",
3559
+ required: {
3560
+ schema_version: field("integer", "Restore result schema version."),
3561
+ kind: field("string", "Restore result kind identifier."),
3562
+ local_only: field("boolean", "Always true; restore targets only local SQLite state."),
3563
+ no_network: field("boolean", "Always true; restore performs no network requests."),
3564
+ restored_at: isoDateField,
3565
+ dry_run: field("boolean", "True when no local records were written."),
3566
+ ok: field("boolean", "True when verification passes and the import has no blocking issues."),
3567
+ verification: field("object", "Backup verification result run before importing."),
3568
+ import_result: field(["object", "null"], "Bridge import result, or null when verification failed.", true),
3569
+ issues: field("array", "Blocking verification or import issues.")
3570
+ },
3571
+ optional: {}
3572
+ }),
3573
+ contract({
3574
+ id: "local_integrity_report",
3575
+ name: "Local Integrity Report",
3576
+ description: "Local integrity report for SQLite quick_check, foreign keys, bridge validation, backup-relevant counts, and orphan rows.",
3577
+ surfaces: ["cli", "mcp", "sdk"],
3578
+ stability: "stable",
3579
+ required: {
3580
+ schema_version: field("integer", "Integrity report schema version."),
3581
+ kind: field("string", "Integrity report kind identifier."),
3582
+ local_only: field("boolean", "Always true; report reads only local SQLite state."),
3583
+ no_network: field("boolean", "Always true; report performs no network requests."),
3584
+ generated_at: isoDateField,
3585
+ database_path: field("string", "Resolved local SQLite database path."),
3586
+ sqlite: field("object", "SQLite quick_check and foreign key integrity summary."),
3587
+ bridge_validation: field("object", "Bridge validation result for a freshly-created local bridge bundle."),
3588
+ counts: field("object", "Backup-relevant record counts by bridge section."),
3589
+ orphaned_rows: field("object", "Detected orphaned local rows by relationship."),
3590
+ ok: field("boolean", "True when no blocking integrity issues are found."),
3591
+ issues: field("array", "Blocking integrity issues."),
3592
+ warnings: field("array", "Non-blocking local warnings.")
3593
+ },
3594
+ optional: {}
3595
+ }),
3596
+ contract({
3597
+ id: "terminal_dashboard_snapshot",
3598
+ name: "Terminal Dashboard Snapshot",
3599
+ description: "Deterministic local snapshot for the keyboard-first terminal dashboard.",
3600
+ surfaces: ["cli", "sdk"],
3601
+ stability: "stable",
3602
+ required: {
3603
+ generated_at: isoDateField,
3604
+ local_only: field("boolean", "Always true; dashboard snapshots read only local todos state."),
3605
+ project_id: field(["string", "null"], "Optional project scope.", true),
3606
+ active_view: field("string", "Active dashboard tab rendered by the TUI or snapshot command."),
3607
+ keymap: field("array", "Keyboard shortcuts exposed by the TUI."),
3608
+ counts: field("object", "Task counts by status and total."),
3609
+ projects: field("array", "Visible projects with open task counts."),
3610
+ tasks: field("array", "Visible pending and in-progress tasks."),
3611
+ plans: field("array", "Visible plans with open task counts."),
3612
+ runs: field("array", "Recent local task runs."),
3613
+ dependencies: field("array", "Local dependency edges and blocking state."),
3614
+ inbox: field("array", "Recent local inbox items."),
3615
+ search: field("object", "Local search query and matching task results.")
3616
+ },
3617
+ optional: {}
3618
+ }),
3448
3619
  contract({
3449
3620
  id: "mention_resolution_report",
3450
3621
  name: "Mention Resolution Report",
@@ -3692,6 +3863,56 @@ var TODOS_JSON_CONTRACTS = [
3692
3863
  },
3693
3864
  optional: {}
3694
3865
  }),
3866
+ contract({
3867
+ id: "workflow_state_config",
3868
+ name: "Workflow State Config",
3869
+ description: "Local workflow state definition mapped onto a canonical task status.",
3870
+ surfaces: ["cli", "mcp", "sdk"],
3871
+ stability: "stable",
3872
+ required: {
3873
+ name: field("string", "Local workflow state name."),
3874
+ canonical_status: field("string", "Canonical task status used for storage and compatibility."),
3875
+ aliases: field("array", "Alternate input names for this workflow state."),
3876
+ terminal: field("boolean", "Whether this state represents terminal work.")
3877
+ },
3878
+ optional: {
3879
+ description: field("string", "Optional human description."),
3880
+ transitions: field(["array", "null"], "Allowed destination state names, or null for unrestricted.", true),
3881
+ color: field("string", "Optional display color token.")
3882
+ }
3883
+ }),
3884
+ contract({
3885
+ id: "workflow_state_result",
3886
+ name: "Workflow State Result",
3887
+ description: "Result of setting a task's local workflow state.",
3888
+ surfaces: ["cli", "mcp", "sdk"],
3889
+ stability: "stable",
3890
+ required: {
3891
+ task: field("object", "Updated canonical task object."),
3892
+ workflow_state: field("object", "Resolved target workflow state."),
3893
+ previous_workflow_state: field("object", "Resolved prior workflow state."),
3894
+ changed: field("boolean", "Whether the local workflow state changed."),
3895
+ canonical_status_changed: field("boolean", "Whether the task row status changed."),
3896
+ local_only: field("boolean", "Always true; operation uses local state only.")
3897
+ },
3898
+ optional: {}
3899
+ }),
3900
+ contract({
3901
+ id: "workflow_state_migration",
3902
+ name: "Workflow State Migration",
3903
+ description: "Dry-run or applied local migration from canonical statuses to workflow state metadata.",
3904
+ surfaces: ["cli", "mcp", "sdk"],
3905
+ stability: "stable",
3906
+ required: {
3907
+ applied: field("boolean", "Whether metadata writes were applied."),
3908
+ migrated_count: field("integer", "Number of tasks written during an applied migration."),
3909
+ pending_count: field("integer", "Number of tasks that would be migrated in dry-run mode."),
3910
+ skipped_count: field("integer", "Number of tasks already carrying matching workflow metadata."),
3911
+ items: field("array", "Per-task migration preview records."),
3912
+ local_only: field("boolean", "Always true; migration uses local state only.")
3913
+ },
3914
+ optional: {}
3915
+ }),
3695
3916
  contract({
3696
3917
  id: "retention_cleanup_report",
3697
3918
  name: "Retention Cleanup Report",
@@ -3713,6 +3934,45 @@ var TODOS_JSON_CONTRACTS = [
3713
3934
  },
3714
3935
  optional: {}
3715
3936
  }),
3937
+ contract({
3938
+ id: "scale_performance_report",
3939
+ name: "Scale Performance Report",
3940
+ description: "Local scale hardening report for query performance, archive readiness, compaction, SQLite integrity, and expected indexes. It performs no network calls.",
3941
+ surfaces: ["cli", "sdk"],
3942
+ stability: "stable",
3943
+ required: {
3944
+ schema_version: field("integer", "Report schema version."),
3945
+ local_only: field("boolean", "Always true; the report reads only local state."),
3946
+ no_network: field("boolean", "Always true; the report performs no network requests."),
3947
+ generated_at: isoDateField,
3948
+ database_path: field("string", "Resolved local SQLite database path."),
3949
+ counts: field("object", "Local task, project, agent, plan, run, event, comment, and dependency counts."),
3950
+ benchmarks: field("array", "Measured local query timings with thresholds."),
3951
+ archive: field("object", "Archive-readiness counts for old terminal tasks and include-archived visibility."),
3952
+ compaction: field("object", "SQLite page and freelist state with recommended maintenance commands."),
3953
+ integrity: field("object", "SQLite quick_check, foreign key, and required-index results."),
3954
+ warnings: field("array", "Non-fatal warnings for slow queries, missing indexes, old archive candidates, or integrity issues.")
3955
+ },
3956
+ optional: {}
3957
+ }),
3958
+ contract({
3959
+ id: "scale_compaction_result",
3960
+ name: "Scale Compaction Result",
3961
+ description: "Dry-run or applied local SQLite optimization and VACUUM compaction result.",
3962
+ surfaces: ["cli", "sdk"],
3963
+ stability: "stable",
3964
+ required: {
3965
+ schema_version: field("integer", "Result schema version."),
3966
+ local_only: field("boolean", "Always true; compaction targets only the local SQLite database."),
3967
+ no_network: field("boolean", "Always true; compaction performs no network requests."),
3968
+ dry_run: field("boolean", "True when commands were only previewed."),
3969
+ database_path: field("string", "Resolved local SQLite database path."),
3970
+ before: field("object", "Page and freelist counts before compaction."),
3971
+ after: field("object", "Page and freelist counts after compaction or dry-run preview."),
3972
+ actions: field("array", "SQLite maintenance actions planned or applied.")
3973
+ },
3974
+ optional: {}
3975
+ }),
3716
3976
  contract({
3717
3977
  id: "duplicate_task_candidate",
3718
3978
  name: "Duplicate Task Candidate",
@@ -3817,12 +4077,30 @@ var TODOS_JSON_CONTRACTS = [
3817
4077
  manifest: field("object", "Normalized extension manifest."),
3818
4078
  validation: field("object", "Schema, compatibility, permission, and sandbox validation details."),
3819
4079
  ok: field("boolean", "Whether the extension passed hard compatibility checks."),
3820
- summary: field("object", "Counts for commands, MCP tools, hooks, permissions, sandbox checks, and failed dry-runs."),
4080
+ summary: field("object", "Counts for commands, MCP tools, templates, renderers, hooks, permissions, sandbox checks, and failed dry-runs."),
3821
4081
  errors: field("array", "Hard validation or compatibility errors."),
3822
4082
  warnings: field("array", "Non-blocking diagnostics such as sandbox approval requirements.")
3823
4083
  },
3824
4084
  optional: {}
3825
4085
  }),
4086
+ contract({
4087
+ id: "local_extension_discovery",
4088
+ name: "Local Extension Discovery",
4089
+ description: "Local-only discovery report for extension manifests from config, project roots, .todos folders, and installed registry records.",
4090
+ surfaces: ["cli", "mcp", "sdk"],
4091
+ stability: "stable",
4092
+ required: {
4093
+ schema_version: field("integer", "Report schema version."),
4094
+ local_only: field("boolean", "Always true; discovery reads only local files and config."),
4095
+ no_network: field("boolean", "Always true; discovery performs no network requests."),
4096
+ project_path: field(["string", "null"], "Project root used for discovery, or null when omitted.", true),
4097
+ config_sources: field("array", "Resolved extension source paths from config and project discovery."),
4098
+ discovered: field("array", "Validated extension source inspections."),
4099
+ installed: field("array", "Installed local extension registry records when included."),
4100
+ warnings: field("array", "Non-fatal source read or validation warnings.")
4101
+ },
4102
+ optional: {}
4103
+ }),
3826
4104
  contract({
3827
4105
  id: "agent",
3828
4106
  name: "Agent",
@@ -5230,6 +5508,15 @@ var DEFAULT_SECRET_PATTERNS = [
5230
5508
  { name: "bearer-token", regex: /\b(bearer)\s+[A-Za-z0-9._~+/=-]{12,}/gi, replacement: "$1 [REDACTED]" }
5231
5509
  ];
5232
5510
  var DEFAULT_SECRET_KEY_PATTERN = /api[_-]?key|token|secret|password/i;
5511
+ var NON_SECRET_USAGE_KEYS = new Set([
5512
+ "tokens",
5513
+ "total_tokens",
5514
+ "token_count",
5515
+ "input_tokens",
5516
+ "output_tokens",
5517
+ "prompt_tokens",
5518
+ "completion_tokens"
5519
+ ]);
5233
5520
  function unique(values) {
5234
5521
  return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
5235
5522
  }
@@ -5249,6 +5536,8 @@ function secretPatterns() {
5249
5536
  return [...customPatterns(), ...DEFAULT_SECRET_PATTERNS];
5250
5537
  }
5251
5538
  function isSecretKey(key) {
5539
+ if (NON_SECRET_USAGE_KEYS.has(key.toLowerCase()))
5540
+ return false;
5252
5541
  if (DEFAULT_SECRET_KEY_PATTERN.test(key))
5253
5542
  return true;
5254
5543
  return unique(loadConfig().secret_safety?.redaction_keys).some((pattern) => key.toLowerCase().includes(pattern.toLowerCase()));
@@ -9353,7 +9642,18 @@ function addTaskRunCommand(input, db) {
9353
9642
  run_id: run.id,
9354
9643
  event_type: "command",
9355
9644
  message: `${status}: ${command}`,
9356
- data: { command, status, exit_code: input.exit_code ?? null, output_summary: outputSummary, artifact_path: artifactPath },
9645
+ data: {
9646
+ command,
9647
+ status,
9648
+ exit_code: input.exit_code ?? null,
9649
+ output_summary: outputSummary,
9650
+ artifact_path: artifactPath,
9651
+ usage: {
9652
+ tokens: input.tokens ?? null,
9653
+ cost_usd: input.cost_usd ?? null,
9654
+ duration_ms: input.duration_ms ?? null
9655
+ }
9656
+ },
9357
9657
  agent_id: input.agent_id ?? run.agent_id ?? undefined,
9358
9658
  created_at: timestamp
9359
9659
  }, d);
@@ -10723,9 +11023,305 @@ function importOnboardingFixture(options = {}) {
10723
11023
  conflictStrategy: options.conflictStrategy
10724
11024
  });
10725
11025
  }
10726
- // src/lib/local-snapshots.ts
11026
+ // src/lib/local-backups.ts
10727
11027
  init_database();
10728
11028
  import { createHash as createHash3 } from "crypto";
11029
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
11030
+ import { dirname as dirname6, resolve as resolve7 } from "path";
11031
+ import { mkdirSync as mkdirSync6 } from "fs";
11032
+ var TODOS_LOCAL_BACKUP_KIND = "hasna.todos.local-backup";
11033
+ var TODOS_LOCAL_BACKUP_SCHEMA_VERSION = 1;
11034
+ var TODOS_LOCAL_INTEGRITY_KIND = "hasna.todos.local-integrity";
11035
+ var TODOS_LOCAL_INTEGRITY_SCHEMA_VERSION = 1;
11036
+ var LOCAL_BACKUP_CHECKSUM_ALGORITHM = "sha256";
11037
+ function stableJson(value) {
11038
+ if (value === null || typeof value !== "object")
11039
+ return JSON.stringify(value);
11040
+ if (Array.isArray(value))
11041
+ return `[${value.map(stableJson).join(",")}]`;
11042
+ const record = value;
11043
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
11044
+ }
11045
+ function sha2562(value) {
11046
+ return createHash3("sha256").update(stableJson(value)).digest("hex");
11047
+ }
11048
+ function sqliteIntegrity(db) {
11049
+ let quick = "unknown";
11050
+ try {
11051
+ const row = db.query("PRAGMA quick_check").get();
11052
+ quick = row?.quick_check ?? "unknown";
11053
+ } catch (error) {
11054
+ quick = error instanceof Error ? error.message : String(error);
11055
+ }
11056
+ let foreignKeyViolations = 0;
11057
+ try {
11058
+ foreignKeyViolations = db.query("PRAGMA foreign_key_check").all().length;
11059
+ } catch {
11060
+ foreignKeyViolations = 0;
11061
+ }
11062
+ return {
11063
+ quick_check: quick,
11064
+ foreign_key_violations: foreignKeyViolations,
11065
+ ok: quick === "ok" && foreignKeyViolations === 0
11066
+ };
11067
+ }
11068
+ function bridgeStats2(data) {
11069
+ return Object.fromEntries(Object.keys(data).map((key) => [key, data[key].length]));
11070
+ }
11071
+ function sectionChecksums(data) {
11072
+ return Object.fromEntries(Object.keys(data).map((key) => [key, sha2562(data[key])]));
11073
+ }
11074
+ function checksumPayload(bundle) {
11075
+ return sha2562(bundle);
11076
+ }
11077
+ function asRecord(value) {
11078
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
11079
+ }
11080
+ function createLocalBackup(options = {}, db) {
11081
+ const d = db || getDatabase();
11082
+ const createdAt2 = options.generated_at ?? now();
11083
+ const bridge = createLocalBridgeBundle({
11084
+ project_id: options.project_id,
11085
+ generatedAt: createdAt2,
11086
+ version: options.version
11087
+ }, d);
11088
+ const integrity = sqliteIntegrity(d);
11089
+ const bridgeChecksum = sha2562(bridge);
11090
+ const warnings = [];
11091
+ if (!integrity.ok)
11092
+ warnings.push("current SQLite integrity check did not pass");
11093
+ const manifest = {
11094
+ schema_version: TODOS_LOCAL_BACKUP_SCHEMA_VERSION,
11095
+ kind: TODOS_LOCAL_BACKUP_KIND,
11096
+ local_only: true,
11097
+ no_network: true,
11098
+ created_at: createdAt2,
11099
+ package: bridge.package,
11100
+ source: bridge.source,
11101
+ bridge: {
11102
+ kind: TODOS_LOCAL_BRIDGE_KIND,
11103
+ schema_version: TODOS_LOCAL_BRIDGE_SCHEMA_VERSION,
11104
+ exported_at: bridge.exportedAt,
11105
+ checksum: bridgeChecksum,
11106
+ stats: bridge.stats,
11107
+ artifact_contents: bridge.artifact_contents?.length ?? 0
11108
+ },
11109
+ database: {
11110
+ path: getDatabasePath(),
11111
+ integrity
11112
+ },
11113
+ checksum_algorithm: LOCAL_BACKUP_CHECKSUM_ALGORITHM,
11114
+ section_checksums: sectionChecksums(bridge.data),
11115
+ warnings
11116
+ };
11117
+ const withoutChecksum = {
11118
+ schema_version: TODOS_LOCAL_BACKUP_SCHEMA_VERSION,
11119
+ kind: TODOS_LOCAL_BACKUP_KIND,
11120
+ local_only: true,
11121
+ no_network: true,
11122
+ created_at: createdAt2,
11123
+ package: bridge.package,
11124
+ manifest,
11125
+ bridge,
11126
+ checksum_algorithm: LOCAL_BACKUP_CHECKSUM_ALGORITHM
11127
+ };
11128
+ const backup = {
11129
+ ...withoutChecksum,
11130
+ checksum: checksumPayload(withoutChecksum)
11131
+ };
11132
+ if (options.output_path)
11133
+ writeLocalBackupFile(backup, options.output_path);
11134
+ return backup;
11135
+ }
11136
+ function writeLocalBackupFile(backup, outputPath) {
11137
+ const path = resolve7(outputPath);
11138
+ mkdirSync6(dirname6(path), { recursive: true });
11139
+ writeFileSync4(path, `${JSON.stringify(backup, null, 2)}
11140
+ `);
11141
+ return path;
11142
+ }
11143
+ function readLocalBackupFile(path) {
11144
+ return JSON.parse(readFileSync4(resolve7(path), "utf-8"));
11145
+ }
11146
+ function verifyLocalBackup(value, options = {}, db) {
11147
+ const verifiedAt = options.verified_at ?? now();
11148
+ const record = asRecord(value);
11149
+ const issues = [];
11150
+ const warnings = [];
11151
+ const bridge = record?.bridge;
11152
+ const manifest = asRecord(record?.manifest);
11153
+ const expectedChecksum = typeof record?.checksum === "string" ? record.checksum : null;
11154
+ const expectedBridgeChecksum = typeof manifest?.bridge === "object" && manifest.bridge && "checksum" in manifest.bridge ? String(manifest.bridge.checksum) : null;
11155
+ if (!record)
11156
+ issues.push("backup must be an object");
11157
+ if (record?.kind !== TODOS_LOCAL_BACKUP_KIND)
11158
+ issues.push(`kind must be ${TODOS_LOCAL_BACKUP_KIND}`);
11159
+ if (record?.schema_version !== TODOS_LOCAL_BACKUP_SCHEMA_VERSION) {
11160
+ issues.push(`schema_version must be ${TODOS_LOCAL_BACKUP_SCHEMA_VERSION}`);
11161
+ }
11162
+ if (record?.local_only !== true)
11163
+ issues.push("local_only must be true");
11164
+ if (record?.no_network !== true)
11165
+ issues.push("no_network must be true");
11166
+ if (!manifest)
11167
+ issues.push("manifest must be an object");
11168
+ if (!bridge)
11169
+ issues.push("bridge must be an object");
11170
+ const bridgeValidation = validateLocalBridgeBundle(bridge);
11171
+ if (!bridgeValidation.ok)
11172
+ issues.push(...bridgeValidation.issues.map((issue) => `bridge: ${issue}`));
11173
+ const actualBridgeChecksum = bridge ? sha2562(bridge) : null;
11174
+ if (expectedBridgeChecksum && actualBridgeChecksum && expectedBridgeChecksum !== actualBridgeChecksum) {
11175
+ issues.push("bridge checksum mismatch");
11176
+ }
11177
+ const withoutChecksum = record ? { ...record } : null;
11178
+ if (withoutChecksum)
11179
+ delete withoutChecksum.checksum;
11180
+ const actualChecksum = withoutChecksum ? checksumPayload(withoutChecksum) : null;
11181
+ if (expectedChecksum && actualChecksum && expectedChecksum !== actualChecksum) {
11182
+ issues.push("backup checksum mismatch");
11183
+ }
11184
+ const expectedCounts = manifest?.bridge && typeof manifest.bridge === "object" ? manifest.bridge.stats ?? {} : {};
11185
+ const actualCounts = bridge?.data ? bridgeStats2(bridge.data) : {};
11186
+ const countMismatches = Object.entries(expectedCounts).filter(([key, count]) => {
11187
+ const actual = actualCounts[key];
11188
+ return typeof count === "number" && actual !== count;
11189
+ });
11190
+ if (countMismatches.length > 0) {
11191
+ issues.push(`manifest count mismatch: ${countMismatches.map(([key]) => key).join(", ")}`);
11192
+ }
11193
+ if (manifest?.section_checksums && typeof manifest.section_checksums === "object" && bridge?.data) {
11194
+ const actualSections = sectionChecksums(bridge.data);
11195
+ const mismatches = Object.entries(manifest.section_checksums).filter(([key, expected]) => actualSections[key] !== expected);
11196
+ if (mismatches.length > 0)
11197
+ issues.push(`section checksum mismatch: ${mismatches.map(([key]) => key).join(", ")}`);
11198
+ }
11199
+ if (bridge?.schemaVersion !== TODOS_LOCAL_BRIDGE_SCHEMA_VERSION) {
11200
+ issues.push(`bridge schemaVersion must be ${TODOS_LOCAL_BRIDGE_SCHEMA_VERSION}`);
11201
+ }
11202
+ const sqlite = options.check_sqlite === false ? null : sqliteIntegrity(db || getDatabase());
11203
+ if (sqlite && !sqlite.ok)
11204
+ warnings.push("current SQLite integrity check did not pass");
11205
+ return {
11206
+ schema_version: TODOS_LOCAL_BACKUP_SCHEMA_VERSION,
11207
+ kind: "hasna.todos.local-backup-verification",
11208
+ local_only: true,
11209
+ no_network: true,
11210
+ verified_at: verifiedAt,
11211
+ ok: issues.length === 0,
11212
+ checksum_algorithm: LOCAL_BACKUP_CHECKSUM_ALGORITHM,
11213
+ checksum: {
11214
+ expected: expectedChecksum,
11215
+ actual: actualChecksum,
11216
+ ok: Boolean(expectedChecksum && actualChecksum && expectedChecksum === actualChecksum)
11217
+ },
11218
+ bridge_checksum: {
11219
+ expected: expectedBridgeChecksum,
11220
+ actual: actualBridgeChecksum,
11221
+ ok: Boolean(expectedBridgeChecksum && actualBridgeChecksum && expectedBridgeChecksum === actualBridgeChecksum)
11222
+ },
11223
+ bridge_validation: bridgeValidation,
11224
+ sqlite,
11225
+ counts: {
11226
+ expected: expectedCounts,
11227
+ actual: actualCounts,
11228
+ ok: countMismatches.length === 0
11229
+ },
11230
+ compatible: bridge?.schemaVersion === TODOS_LOCAL_BRIDGE_SCHEMA_VERSION,
11231
+ issues,
11232
+ warnings
11233
+ };
11234
+ }
11235
+ function restoreLocalBackup(backup, options = {}, db) {
11236
+ const d = db || getDatabase();
11237
+ const verification = verifyLocalBackup(backup, { verified_at: options.verified_at, check_sqlite: true }, d);
11238
+ const issues = [...verification.issues];
11239
+ let importResult = null;
11240
+ if (verification.ok) {
11241
+ importResult = importLocalBridgeBundle(backup.bridge, {
11242
+ dryRun: !options.apply,
11243
+ conflictStrategy: options.conflict_strategy ?? "skip"
11244
+ }, d);
11245
+ if (!importResult.ok)
11246
+ issues.push(...importResult.issues);
11247
+ }
11248
+ return {
11249
+ schema_version: TODOS_LOCAL_BACKUP_SCHEMA_VERSION,
11250
+ kind: "hasna.todos.local-backup-restore",
11251
+ local_only: true,
11252
+ no_network: true,
11253
+ restored_at: options.verified_at ?? now(),
11254
+ dry_run: !options.apply,
11255
+ ok: verification.ok && Boolean(importResult?.ok),
11256
+ verification,
11257
+ import_result: importResult,
11258
+ issues
11259
+ };
11260
+ }
11261
+ function count(db, table) {
11262
+ const row = db.query(`SELECT COUNT(*) AS count FROM ${table}`).get();
11263
+ return row?.count ?? 0;
11264
+ }
11265
+ function orphanedRows(db) {
11266
+ return {
11267
+ tasks_missing_project: countQuery(db, "SELECT COUNT(*) AS count FROM tasks t WHERE t.project_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM projects p WHERE p.id = t.project_id)"),
11268
+ comments_missing_task: countQuery(db, "SELECT COUNT(*) AS count FROM task_comments c WHERE NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = c.task_id)"),
11269
+ runs_missing_task: countQuery(db, "SELECT COUNT(*) AS count FROM task_runs r WHERE NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = r.task_id)"),
11270
+ run_events_missing_run: countQuery(db, "SELECT COUNT(*) AS count FROM task_run_events e WHERE NOT EXISTS (SELECT 1 FROM task_runs r WHERE r.id = e.run_id)"),
11271
+ run_commands_missing_run: countQuery(db, "SELECT COUNT(*) AS count FROM task_run_commands c WHERE NOT EXISTS (SELECT 1 FROM task_runs r WHERE r.id = c.run_id)"),
11272
+ run_artifacts_missing_run: countQuery(db, "SELECT COUNT(*) AS count FROM task_run_artifacts a WHERE NOT EXISTS (SELECT 1 FROM task_runs r WHERE r.id = a.run_id)"),
11273
+ dependencies_missing_task: countQuery(db, "SELECT COUNT(*) AS count FROM task_dependencies d WHERE NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = d.task_id)"),
11274
+ dependencies_missing_dependency: countQuery(db, "SELECT COUNT(*) AS count FROM task_dependencies d WHERE d.depends_on IS NOT NULL AND NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = d.depends_on)")
11275
+ };
11276
+ }
11277
+ function countQuery(db, sql) {
11278
+ try {
11279
+ const row = db.query(sql).get();
11280
+ return row?.count ?? 0;
11281
+ } catch {
11282
+ return 0;
11283
+ }
11284
+ }
11285
+ function checkLocalIntegrity(options = {}, db) {
11286
+ const d = db || getDatabase();
11287
+ const bridge = createLocalBridgeBundle({
11288
+ project_id: options.project_id,
11289
+ generatedAt: options.generated_at,
11290
+ version: options.version ?? getPackageVersion(import.meta.url)
11291
+ }, d);
11292
+ const bridgeValidation = validateLocalBridgeBundle(bridge);
11293
+ const sqlite = sqliteIntegrity(d);
11294
+ const orphans = orphanedRows(d);
11295
+ const issues = [];
11296
+ const warnings = [];
11297
+ if (!sqlite.ok)
11298
+ issues.push("SQLite integrity check failed");
11299
+ if (!bridgeValidation.ok)
11300
+ issues.push(...bridgeValidation.issues.map((issue) => `bridge: ${issue}`));
11301
+ const orphanTotal = Object.values(orphans).reduce((sum, value) => sum + value, 0);
11302
+ if (orphanTotal > 0)
11303
+ issues.push(`${orphanTotal} orphaned local row(s) detected`);
11304
+ if (count(d, "tasks") === 0)
11305
+ warnings.push("no tasks found in local store");
11306
+ return {
11307
+ schema_version: TODOS_LOCAL_INTEGRITY_SCHEMA_VERSION,
11308
+ kind: TODOS_LOCAL_INTEGRITY_KIND,
11309
+ local_only: true,
11310
+ no_network: true,
11311
+ generated_at: options.generated_at ?? now(),
11312
+ database_path: getDatabasePath(),
11313
+ sqlite,
11314
+ bridge_validation: bridgeValidation,
11315
+ counts: bridge.stats,
11316
+ orphaned_rows: orphans,
11317
+ ok: issues.length === 0,
11318
+ issues,
11319
+ warnings
11320
+ };
11321
+ }
11322
+ // src/lib/local-snapshots.ts
11323
+ init_database();
11324
+ import { createHash as createHash4 } from "crypto";
10729
11325
 
10730
11326
  // src/lib/activity-timeline.ts
10731
11327
  init_database();
@@ -10999,8 +11595,8 @@ function stable(value) {
10999
11595
  return value;
11000
11596
  return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, stable(item)]));
11001
11597
  }
11002
- function sha2562(value) {
11003
- return createHash3("sha256").update(JSON.stringify(stable(value))).digest("hex");
11598
+ function sha2563(value) {
11599
+ return createHash4("sha256").update(JSON.stringify(stable(value))).digest("hex");
11004
11600
  }
11005
11601
  function latestTimestamp(items, fallback) {
11006
11602
  const timestamps = [];
@@ -11180,7 +11776,7 @@ function getLocalSnapshot(options, db) {
11180
11776
  package: source3(getPackageVersion(import.meta.url)),
11181
11777
  filters: body.filters,
11182
11778
  cursor,
11183
- fingerprint: sha2562(body),
11779
+ fingerprint: sha2563(body),
11184
11780
  count: items.length,
11185
11781
  items,
11186
11782
  resources: {
@@ -11237,7 +11833,7 @@ function renderLocalSnapshotMarkdown(snapshot) {
11237
11833
  `;
11238
11834
  }
11239
11835
  // src/lib/sdk-integration-fixtures.ts
11240
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync4 } from "fs";
11836
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync5 } from "fs";
11241
11837
  import { join as join7 } from "path";
11242
11838
 
11243
11839
  // src/cli-mcp-parity.ts
@@ -11418,6 +12014,56 @@ var TODOS_CLI_MCP_PARITY = [
11418
12014
  mcpTool: "get_agent_reliability_scorecard"
11419
12015
  }
11420
12016
  },
12017
+ {
12018
+ domain: "local-reports",
12019
+ cliCommands: [
12020
+ "todos reports local"
12021
+ ],
12022
+ mcpTools: [
12023
+ "list_local_report_types",
12024
+ "build_local_report"
12025
+ ],
12026
+ jsonContracts: ["local_report", "task", "structured_error", "api_error"],
12027
+ errorContracts: ["structured_error", "api_error"],
12028
+ status: "matched",
12029
+ intentionalGaps: [],
12030
+ example: {
12031
+ cli: "todos reports local --agent codex --format markdown",
12032
+ mcpTool: "build_local_report"
12033
+ }
12034
+ },
12035
+ {
12036
+ domain: "local-backups",
12037
+ cliCommands: [
12038
+ "todos backup create",
12039
+ "todos backup verify",
12040
+ "todos backup restore",
12041
+ "todos backup integrity"
12042
+ ],
12043
+ mcpTools: [
12044
+ "create_local_backup",
12045
+ "verify_local_backup",
12046
+ "restore_local_backup",
12047
+ "check_local_integrity"
12048
+ ],
12049
+ jsonContracts: [
12050
+ "local_backup_bundle",
12051
+ "local_backup_verification",
12052
+ "local_backup_restore_result",
12053
+ "local_integrity_report",
12054
+ "local_bridge_bundle",
12055
+ "local_bridge_import_result",
12056
+ "structured_error",
12057
+ "api_error"
12058
+ ],
12059
+ errorContracts: ["structured_error", "api_error"],
12060
+ status: "matched",
12061
+ intentionalGaps: [],
12062
+ example: {
12063
+ cli: "todos backup create --output todos-backup.json --json",
12064
+ mcpTool: "create_local_backup"
12065
+ }
12066
+ },
11421
12067
  {
11422
12068
  domain: "local-fields",
11423
12069
  cliCommands: [
@@ -11439,6 +12085,29 @@ var TODOS_CLI_MCP_PARITY = [
11439
12085
  mcpTool: "set_task_fields"
11440
12086
  }
11441
12087
  },
12088
+ {
12089
+ domain: "workflow-states",
12090
+ cliCommands: [
12091
+ "todos workflow states",
12092
+ "todos workflow set",
12093
+ "todos workflow tasks",
12094
+ "todos workflow migrate"
12095
+ ],
12096
+ mcpTools: [
12097
+ "list_workflow_states",
12098
+ "set_task_workflow_state",
12099
+ "query_tasks_by_workflow_state",
12100
+ "migrate_workflow_states"
12101
+ ],
12102
+ jsonContracts: ["workflow_state_config", "workflow_state_result", "workflow_state_migration", "task", "structured_error", "api_error"],
12103
+ errorContracts: ["structured_error", "api_error"],
12104
+ status: "matched",
12105
+ intentionalGaps: [],
12106
+ example: {
12107
+ cli: "todos workflow set 1234abcd review --json",
12108
+ mcpTool: "set_task_workflow_state"
12109
+ }
12110
+ },
11442
12111
  {
11443
12112
  domain: "dedupe",
11444
12113
  cliCommands: [
@@ -11634,18 +12303,63 @@ var TODOS_CLI_MCP_PARITY = [
11634
12303
  }
11635
12304
  },
11636
12305
  {
11637
- domain: "templates",
11638
- cliCommands: [
11639
- "todos template-library",
11640
- "todos template-init",
11641
- "todos template-preview",
11642
- "todos templates --use",
11643
- "todos template-export",
11644
- "todos template-import",
11645
- "todos template-history"
11646
- ],
11647
- mcpTools: [
11648
- "list_template_library",
12306
+ domain: "usage-ledger",
12307
+ cliCommands: ["todos usage report"],
12308
+ mcpTools: ["get_usage_ledger"],
12309
+ jsonContracts: ["local_usage_ledger", "structured_error", "api_error"],
12310
+ errorContracts: ["structured_error", "api_error"],
12311
+ status: "matched",
12312
+ intentionalGaps: [],
12313
+ example: {
12314
+ cli: "todos usage report --agent codex --max-tasks 1000 --json",
12315
+ mcpTool: "get_usage_ledger"
12316
+ }
12317
+ },
12318
+ {
12319
+ domain: "terminal-dashboard",
12320
+ cliCommands: ["todos dashboard", "todos dashboard --snapshot"],
12321
+ mcpTools: [],
12322
+ jsonContracts: ["terminal_dashboard_snapshot", "structured_error", "api_error"],
12323
+ errorContracts: ["structured_error", "api_error"],
12324
+ status: "intentional-gap",
12325
+ intentionalGaps: [{
12326
+ cliCommand: "todos dashboard",
12327
+ reason: "The interactive terminal dashboard is a human TUI surface; agents should use the underlying task, project, plan, run, dependency, inbox, and search MCP tools directly."
12328
+ }],
12329
+ gapReason: "Terminal UI keyboard navigation is not an MCP interaction model.",
12330
+ example: {
12331
+ cli: "todos dashboard --snapshot --view tasks --search release --json"
12332
+ }
12333
+ },
12334
+ {
12335
+ domain: "scale-hardening",
12336
+ cliCommands: ["todos scale report", "todos scale compact"],
12337
+ mcpTools: [],
12338
+ jsonContracts: ["scale_performance_report", "scale_compaction_result", "structured_error", "api_error"],
12339
+ errorContracts: ["structured_error", "api_error"],
12340
+ status: "intentional-gap",
12341
+ intentionalGaps: [{
12342
+ cliCommand: "todos scale compact",
12343
+ reason: "SQLite VACUUM is a local maintenance operation that can briefly lock the database; MCP agents should request explicit local CLI maintenance instead of invoking compaction through a remote-facing tool surface."
12344
+ }],
12345
+ gapReason: "Scale diagnostics and compaction are local operator maintenance commands, while MCP tools should use domain-specific task, run, and doctor APIs.",
12346
+ example: {
12347
+ cli: "todos scale report --older-than-days 30 --json"
12348
+ }
12349
+ },
12350
+ {
12351
+ domain: "templates",
12352
+ cliCommands: [
12353
+ "todos template-library",
12354
+ "todos template-init",
12355
+ "todos template-preview",
12356
+ "todos templates --use",
12357
+ "todos template-export",
12358
+ "todos template-import",
12359
+ "todos template-history"
12360
+ ],
12361
+ mcpTools: [
12362
+ "list_template_library",
11649
12363
  "write_template_library",
11650
12364
  "init_templates",
11651
12365
  "preview_template",
@@ -11756,6 +12470,7 @@ var TODOS_CLI_MCP_PARITY = [
11756
12470
  domain: "extensions",
11757
12471
  cliCommands: [
11758
12472
  "todos extensions list",
12473
+ "todos extensions discover",
11759
12474
  "todos extensions inspect",
11760
12475
  "todos extensions compat",
11761
12476
  "todos extensions install",
@@ -11764,12 +12479,13 @@ var TODOS_CLI_MCP_PARITY = [
11764
12479
  ],
11765
12480
  mcpTools: [
11766
12481
  "list_local_extensions",
12482
+ "discover_local_extensions",
11767
12483
  "inspect_local_extension",
11768
12484
  "test_local_extension_compatibility",
11769
12485
  "install_local_extension",
11770
12486
  "remove_local_extension"
11771
12487
  ],
11772
- jsonContracts: ["local_extension_compatibility", "structured_error", "api_error"],
12488
+ jsonContracts: ["local_extension_compatibility", "local_extension_discovery", "structured_error", "api_error"],
11773
12489
  errorContracts: ["structured_error", "api_error"],
11774
12490
  status: "matched",
11775
12491
  example: {
@@ -13055,7 +13771,7 @@ function createSdkIntegrationFixturePack(options = {}) {
13055
13771
  };
13056
13772
  }
13057
13773
  function writeSdkIntegrationFixtures(directory, options = {}) {
13058
- mkdirSync6(directory, { recursive: true });
13774
+ mkdirSync7(directory, { recursive: true });
13059
13775
  const pack = createSdkIntegrationFixturePack(options);
13060
13776
  const bundle = getOnboardingFixtureBundle("agent-project-demo");
13061
13777
  const files = [
@@ -13067,7 +13783,7 @@ function writeSdkIntegrationFixtures(directory, options = {}) {
13067
13783
  const written = [];
13068
13784
  for (const [name, payload] of files) {
13069
13785
  const file = join7(directory, name);
13070
- writeFileSync4(file, `${JSON.stringify(payload, null, 2)}
13786
+ writeFileSync5(file, `${JSON.stringify(payload, null, 2)}
13071
13787
  `, "utf-8");
13072
13788
  written.push(file);
13073
13789
  }
@@ -14053,7 +14769,7 @@ function renderRoadmapMarkdown(idOrName, db) {
14053
14769
  }
14054
14770
  // src/lib/audit-ledger.ts
14055
14771
  init_database();
14056
- import { createHash as createHash4 } from "crypto";
14772
+ import { createHash as createHash5 } from "crypto";
14057
14773
  var LOCAL_AUDIT_LEDGER_SCHEMA_VERSION = 1;
14058
14774
  var LOCAL_AUDIT_LEDGER_HASH_ALGORITHM = "sha256";
14059
14775
  var LOCAL_AUDIT_LEDGER_INITIAL_HASH = "0".repeat(64);
@@ -14066,7 +14782,7 @@ function canonicalize(value) {
14066
14782
  return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalize(object[key])}`).join(",")}}`;
14067
14783
  }
14068
14784
  function hash(value) {
14069
- return createHash4("sha256").update(value).digest("hex");
14785
+ return createHash5("sha256").update(value).digest("hex");
14070
14786
  }
14071
14787
  function parsePayload(value) {
14072
14788
  if (!value)
@@ -14325,15 +15041,15 @@ function renderLocalAuditLedgerMarkdown(ledger) {
14325
15041
  `Scope: ${ledger.run_id ?? ledger.task_id ?? ledger.project_id ?? "all local evidence"}`,
14326
15042
  `Root hash: ${ledger.root_hash}`,
14327
15043
  `Entries: ${ledger.entry_count}`,
14328
- `Sources: ${Object.entries(ledger.source_counts).map(([source6, count]) => `${source6}=${count}`).join(", ") || "none"}`
15044
+ `Sources: ${Object.entries(ledger.source_counts).map(([source6, count2]) => `${source6}=${count2}`).join(", ") || "none"}`
14329
15045
  ].join(`
14330
15046
  `);
14331
15047
  }
14332
15048
  // src/lib/release-compatibility.ts
14333
15049
  init_migrations();
14334
15050
  init_schema();
14335
- import { readFileSync as readFileSync4 } from "fs";
14336
- import { join as join8, resolve as resolve7 } from "path";
15051
+ import { readFileSync as readFileSync5 } from "fs";
15052
+ import { join as join8, resolve as resolve8 } from "path";
14337
15053
  import { Database as Database2 } from "bun:sqlite";
14338
15054
  var LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION = 1;
14339
15055
  var EXPECTED_PACKAGE_NAME = "@hasna/todos";
@@ -14379,7 +15095,7 @@ function warn(id, message, details) {
14379
15095
  return { id, status: "warning", message, details };
14380
15096
  }
14381
15097
  function readPackageJson(root) {
14382
- return JSON.parse(readFileSync4(join8(root, "package.json"), "utf8"));
15098
+ return JSON.parse(readFileSync5(join8(root, "package.json"), "utf8"));
14383
15099
  }
14384
15100
  function sortedKeys(value) {
14385
15101
  return Object.keys(value ?? {}).sort((left, right) => left.localeCompare(right));
@@ -14475,7 +15191,7 @@ function checkChangelog() {
14475
15191
  ];
14476
15192
  }
14477
15193
  function createReleaseCompatibilityReport(options = {}) {
14478
- const root = resolve7(options.root ?? process.cwd());
15194
+ const root = resolve8(options.root ?? process.cwd());
14479
15195
  const packageJson = readPackageJson(root);
14480
15196
  const simulatedLevels = options.simulated_levels ?? defaultSimulationLevels();
14481
15197
  const checks = [
@@ -14586,7 +15302,7 @@ function renderReleaseCompatibilityMarkdown(report) {
14586
15302
  `);
14587
15303
  }
14588
15304
  // src/db/inbox.ts
14589
- import { createHash as createHash5 } from "crypto";
15305
+ import { createHash as createHash6 } from "crypto";
14590
15306
 
14591
15307
  // src/lib/github.ts
14592
15308
  import { execFileSync } from "child_process";
@@ -14669,7 +15385,7 @@ function compactWhitespace(value) {
14669
15385
  function fingerprintInboxInput(input) {
14670
15386
  const sourceType = input.source_type || detectInboxSourceType(input.body, input.source_url);
14671
15387
  const normalized = compactWhitespace(redactEvidenceText(input.body)).slice(0, 8000);
14672
- return createHash5("sha256").update(`${sourceType}
15388
+ return createHash6("sha256").update(`${sourceType}
14673
15389
  ${input.source_url || ""}
14674
15390
  ${normalized}`).digest("hex");
14675
15391
  }
@@ -16199,120 +16915,867 @@ async function checkLocalNotifications(input = {}, db) {
16199
16915
  warnings
16200
16916
  };
16201
16917
  }
16202
- // src/lib/local-encryption.ts
16203
- import { createCipheriv, createDecipheriv, createHash as createHash6, randomBytes, scryptSync, timingSafeEqual } from "crypto";
16204
- var TODOS_ENCRYPTED_VALUE_KIND = "hasna.todos.encrypted-value";
16205
- var TODOS_ENCRYPTED_BRIDGE_KIND = "hasna.todos.encrypted-bridge";
16206
- var TODOS_ENCRYPTION_SCHEMA_VERSION = 1;
16207
- var DEFAULT_ENCRYPTION_PROFILE = "default";
16208
- var DEFAULT_ENCRYPTION_KEY_ENV = "TODOS_ENCRYPTION_KEY";
16209
-
16210
- class EncryptionKeyUnavailableError extends Error {
16211
- keyEnv;
16212
- profile;
16213
- constructor(keyEnv, profile) {
16214
- super(`Encryption key is locked: set ${keyEnv} to use profile ${profile}`);
16215
- this.keyEnv = keyEnv;
16216
- this.profile = profile;
16918
+ // src/lib/usage-ledger.ts
16919
+ init_database();
16920
+ var LOCAL_USAGE_LEDGER_SCHEMA_VERSION = 1;
16921
+ function numberValue(value) {
16922
+ if (typeof value === "number" && Number.isFinite(value))
16923
+ return value;
16924
+ if (typeof value === "string" && value.trim() !== "") {
16925
+ const parsed = Number(value);
16926
+ if (Number.isFinite(parsed))
16927
+ return parsed;
16217
16928
  }
16929
+ return 0;
16218
16930
  }
16219
-
16220
- class EncryptedPayloadError extends Error {
16221
- constructor(message) {
16222
- super(message);
16931
+ function sumDirectNumber(record, keys) {
16932
+ let value = 0;
16933
+ for (const [rawKey, rawValue] of Object.entries(record)) {
16934
+ const key = rawKey.toLowerCase();
16935
+ if (keys.includes(key))
16936
+ value += Math.max(0, numberValue(rawValue));
16223
16937
  }
16938
+ return value;
16224
16939
  }
16225
- function now2() {
16226
- return new Date().toISOString();
16940
+ function maxDirectNumber(record, keys) {
16941
+ let value = 0;
16942
+ for (const [rawKey, rawValue] of Object.entries(record)) {
16943
+ const key = rawKey.toLowerCase();
16944
+ if (keys.includes(key))
16945
+ value = Math.max(value, numberValue(rawValue));
16946
+ }
16947
+ return Math.max(0, value);
16227
16948
  }
16228
- function sha2563(value) {
16229
- return createHash6("sha256").update(value).digest("hex");
16949
+ function extractUsage(value) {
16950
+ if (!value || typeof value !== "object")
16951
+ return { tokens: 0, cost_usd: 0, duration_ms: 0, records: 0 };
16952
+ if (Array.isArray(value)) {
16953
+ return value.reduce((acc, item) => {
16954
+ const usage = extractUsage(item);
16955
+ acc.tokens += usage.tokens;
16956
+ acc.cost_usd += usage.cost_usd;
16957
+ acc.duration_ms += usage.duration_ms;
16958
+ acc.records += usage.records;
16959
+ return acc;
16960
+ }, { tokens: 0, cost_usd: 0, duration_ms: 0, records: 0 });
16961
+ }
16962
+ const record = value;
16963
+ const explicitTokens = maxDirectNumber(record, ["tokens", "total_tokens", "token_count"]);
16964
+ const splitTokens2 = sumDirectNumber(record, ["input_tokens", "output_tokens", "prompt_tokens", "completion_tokens"]);
16965
+ const cost = maxDirectNumber(record, ["cost_usd", "usd", "price_usd", "amount_usd", "cost"]);
16966
+ const duration = maxDirectNumber(record, ["duration_ms", "elapsed_ms", "latency_ms"]);
16967
+ const own = {
16968
+ tokens: explicitTokens || splitTokens2,
16969
+ cost_usd: cost,
16970
+ duration_ms: duration,
16971
+ records: explicitTokens || splitTokens2 || cost || duration ? 1 : 0
16972
+ };
16973
+ for (const [key, child] of Object.entries(record)) {
16974
+ if (["tokens", "total_tokens", "token_count", "input_tokens", "output_tokens", "prompt_tokens", "completion_tokens", "cost_usd", "usd", "price_usd", "amount_usd", "cost", "duration_ms", "elapsed_ms", "latency_ms"].includes(key.toLowerCase())) {
16975
+ continue;
16976
+ }
16977
+ const nested = extractUsage(child);
16978
+ own.tokens += nested.tokens;
16979
+ own.cost_usd += nested.cost_usd;
16980
+ own.duration_ms += nested.duration_ms;
16981
+ own.records += nested.records;
16982
+ }
16983
+ return own;
16230
16984
  }
16231
- function normalizeProfileName(value) {
16232
- const name = (value || DEFAULT_ENCRYPTION_PROFILE).trim();
16233
- if (!/^[a-zA-Z0-9._-]+$/.test(name))
16234
- throw new Error("encryption profile names may only contain letters, numbers, dots, underscores, and dashes");
16235
- return name;
16985
+ function parseJsonObject4(value) {
16986
+ if (!value)
16987
+ return {};
16988
+ try {
16989
+ const parsed = JSON.parse(value);
16990
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
16991
+ } catch {
16992
+ return {};
16993
+ }
16236
16994
  }
16237
- function randomBase64(bytes) {
16238
- return randomBytes(bytes).toString("base64");
16995
+ function millisBetween(start, end) {
16996
+ if (!start || !end)
16997
+ return 0;
16998
+ const startMs = Date.parse(start);
16999
+ const endMs = Date.parse(end);
17000
+ if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs < startMs)
17001
+ return 0;
17002
+ return endMs - startMs;
16239
17003
  }
16240
- function deriveKey(secret, salt) {
16241
- if (secret.length < 12)
16242
- throw new Error("encryption key must be at least 12 characters");
16243
- return scryptSync(secret, Buffer.from(salt, "base64"), 32);
17004
+ function addTaskScope(where, params, options, alias = "t") {
17005
+ if (options.project_id) {
17006
+ where.push(`${alias}.project_id = ?`);
17007
+ params.push(options.project_id);
17008
+ }
17009
+ if (options.agent_id) {
17010
+ where.push(`(${alias}.agent_id = ? OR ${alias}.assigned_to = ?)`);
17011
+ params.push(options.agent_id, options.agent_id);
17012
+ }
17013
+ if (options.since) {
17014
+ where.push(`${alias}.created_at >= ?`);
17015
+ params.push(options.since);
17016
+ }
17017
+ if (options.until) {
17018
+ where.push(`${alias}.created_at <= ?`);
17019
+ params.push(options.until);
17020
+ }
16244
17021
  }
16245
- function profileFromConfig(name) {
16246
- return loadConfig().encryption_profiles?.[name] ?? null;
17022
+ function addRunScope(where, params, options, runAlias = "r", taskAlias = "t") {
17023
+ if (options.project_id) {
17024
+ where.push(`${taskAlias}.project_id = ?`);
17025
+ params.push(options.project_id);
17026
+ }
17027
+ if (options.agent_id) {
17028
+ where.push(`(${runAlias}.agent_id = ? OR ${taskAlias}.agent_id = ? OR ${taskAlias}.assigned_to = ?)`);
17029
+ params.push(options.agent_id, options.agent_id, options.agent_id);
17030
+ }
17031
+ if (options.since) {
17032
+ where.push(`${runAlias}.started_at >= ?`);
17033
+ params.push(options.since);
17034
+ }
17035
+ if (options.until) {
17036
+ where.push(`${runAlias}.started_at <= ?`);
17037
+ params.push(options.until);
17038
+ }
16247
17039
  }
16248
- function ensureEncryptionProfile(name = DEFAULT_ENCRYPTION_PROFILE) {
16249
- const normalized = normalizeProfileName(name);
16250
- const existing = profileFromConfig(normalized);
16251
- if (existing)
16252
- return existing;
16253
- return upsertEncryptionProfile({ name: normalized });
17040
+ function addTraceScope(where, params, options, traceAlias = "tr", taskAlias = "t") {
17041
+ if (options.project_id) {
17042
+ where.push(`${taskAlias}.project_id = ?`);
17043
+ params.push(options.project_id);
17044
+ }
17045
+ if (options.agent_id) {
17046
+ where.push(`(${traceAlias}.agent_id = ? OR ${taskAlias}.agent_id = ? OR ${taskAlias}.assigned_to = ?)`);
17047
+ params.push(options.agent_id, options.agent_id, options.agent_id);
17048
+ }
17049
+ if (options.since) {
17050
+ where.push(`${traceAlias}.created_at >= ?`);
17051
+ params.push(options.since);
17052
+ }
17053
+ if (options.until) {
17054
+ where.push(`${traceAlias}.created_at <= ?`);
17055
+ params.push(options.until);
17056
+ }
16254
17057
  }
16255
- function listEncryptionProfiles() {
16256
- return Object.values(loadConfig().encryption_profiles ?? {}).sort((left, right) => left.name.localeCompare(right.name));
17058
+ function queryOne(db, sql, params) {
17059
+ return db.query(sql).get(...params);
16257
17060
  }
16258
- function upsertEncryptionProfile(input) {
16259
- const name = normalizeProfileName(input.name);
16260
- const config = loadConfig();
16261
- const existing = config.encryption_profiles?.[name];
16262
- const timestamp2 = now2();
16263
- const profile = {
17061
+ function queryAll(db, sql, params) {
17062
+ return db.query(sql).all(...params);
17063
+ }
17064
+ function rounded(value, places = 6) {
17065
+ if (!Number.isFinite(value))
17066
+ return 0;
17067
+ const factor = 10 ** places;
17068
+ return Math.round(value * factor) / factor;
17069
+ }
17070
+ function quotaLimit(name, limit, used) {
17071
+ if (limit === undefined || !Number.isFinite(limit) || limit < 0)
17072
+ return null;
17073
+ const normalized = name === "max_cost_usd" ? rounded(limit) : Math.floor(limit);
17074
+ const roundedUsed = name === "max_cost_usd" ? rounded(used) : Math.floor(used);
17075
+ return {
16264
17076
  name,
16265
- algorithm: "aes-256-gcm",
16266
- kdf: "scrypt",
16267
- key_env: input.key_env?.trim() || existing?.key_env || DEFAULT_ENCRYPTION_KEY_ENV,
16268
- salt: input.salt || existing?.salt || randomBase64(16),
16269
- description: input.description ?? existing?.description,
16270
- created_at: existing?.created_at || timestamp2,
16271
- updated_at: timestamp2
17077
+ limit: normalized,
17078
+ used: roundedUsed,
17079
+ remaining: rounded(normalized - roundedUsed),
17080
+ exceeded: roundedUsed > normalized
17081
+ };
17082
+ }
17083
+ function buildQuota(options, report) {
17084
+ const quotas = options.quotas || {};
17085
+ const limits2 = [
17086
+ quotaLimit("max_tasks", quotas.max_tasks, report.counts.tasks),
17087
+ quotaLimit("max_projects", quotas.max_projects, report.counts.projects),
17088
+ quotaLimit("max_runs", quotas.max_runs, report.counts.runs),
17089
+ quotaLimit("max_commands", quotas.max_commands, report.counts.commands),
17090
+ quotaLimit("max_tokens", quotas.max_tokens, report.usage.total_tokens),
17091
+ quotaLimit("max_cost_usd", quotas.max_cost_usd, report.usage.total_cost_usd),
17092
+ quotaLimit("max_storage_bytes", quotas.max_storage_bytes, report.storage.evidence_bytes)
17093
+ ].filter((item) => Boolean(item));
17094
+ const exceeded = limits2.filter((item) => item.exceeded).map((item) => item.name);
17095
+ return {
17096
+ simulated: limits2.length > 0,
17097
+ limits: limits2,
17098
+ exceeded,
17099
+ allowed: exceeded.length === 0
16272
17100
  };
16273
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(profile.key_env))
16274
- throw new Error("key_env must be a valid environment variable name");
16275
- saveConfig({
16276
- ...config,
16277
- encryption_profiles: {
16278
- ...config.encryption_profiles ?? {},
16279
- [name]: profile
17101
+ }
17102
+ function createLocalUsageLedger(options = {}, db) {
17103
+ const d = db || getDatabase();
17104
+ const generatedAt = options.generated_at || new Date().toISOString();
17105
+ const taskWhere = [];
17106
+ const taskParams = [];
17107
+ addTaskScope(taskWhere, taskParams, options);
17108
+ const taskClause = taskWhere.length ? `WHERE ${taskWhere.join(" AND ")}` : "";
17109
+ const taskTotals = queryOne(d, `SELECT COUNT(*) as tasks, COALESCE(SUM(cost_tokens), 0) as task_tokens, COALESCE(SUM(cost_usd), 0) as task_cost_usd FROM tasks t ${taskClause}`, taskParams);
17110
+ let projectCount = 0;
17111
+ if (options.project_id) {
17112
+ projectCount = queryOne(d, "SELECT COUNT(*) as count FROM projects WHERE id = ?", [options.project_id]).count;
17113
+ } else if (options.agent_id) {
17114
+ const projectWhere = ["t.project_id IS NOT NULL"];
17115
+ const projectParams = [];
17116
+ addTaskScope(projectWhere, projectParams, options);
17117
+ projectCount = queryOne(d, `SELECT COUNT(DISTINCT t.project_id) as count FROM tasks t WHERE ${projectWhere.join(" AND ")}`, projectParams).count;
17118
+ } else {
17119
+ projectCount = queryOne(d, "SELECT COUNT(*) as count FROM projects", []).count;
17120
+ }
17121
+ const runWhere = [];
17122
+ const runParams = [];
17123
+ addRunScope(runWhere, runParams, options);
17124
+ const runClause = runWhere.length ? `WHERE ${runWhere.join(" AND ")}` : "";
17125
+ const runs = queryAll(d, `SELECT r.id, r.started_at, r.completed_at, r.metadata
17126
+ FROM task_runs r JOIN tasks t ON t.id = r.task_id
17127
+ ${runClause}`, runParams);
17128
+ const commandTotals = queryOne(d, `SELECT COUNT(*) as commands
17129
+ FROM task_run_commands c
17130
+ JOIN task_runs r ON r.id = c.run_id
17131
+ JOIN tasks t ON t.id = c.task_id
17132
+ ${runClause}`, runParams);
17133
+ const artifactTotals = queryOne(d, `SELECT COUNT(*) as artifacts, COALESCE(SUM(a.size_bytes), 0) as bytes
17134
+ FROM task_run_artifacts a
17135
+ JOIN task_runs r ON r.id = a.run_id
17136
+ JOIN tasks t ON t.id = a.task_id
17137
+ ${runClause}`, runParams);
17138
+ const traceWhere = [];
17139
+ const traceParams = [];
17140
+ addTraceScope(traceWhere, traceParams, options);
17141
+ const traceClause = traceWhere.length ? `WHERE ${traceWhere.join(" AND ")}` : "";
17142
+ const traceTotals = queryOne(d, `SELECT COUNT(*) as traces,
17143
+ COALESCE(SUM(tr.tokens), 0) as tokens,
17144
+ COALESCE(SUM(tr.cost_usd), 0) as cost_usd,
17145
+ COALESCE(SUM(tr.duration_ms), 0) as duration_ms
17146
+ FROM task_traces tr
17147
+ JOIN tasks t ON t.id = tr.task_id
17148
+ ${traceClause}`, traceParams);
17149
+ let completedRunMs = 0;
17150
+ let openRunMs = 0;
17151
+ let metadataUsage = { tokens: 0, cost_usd: 0, duration_ms: 0, records: 0 };
17152
+ for (const run of runs) {
17153
+ if (run.completed_at)
17154
+ completedRunMs += millisBetween(run.started_at, run.completed_at);
17155
+ else
17156
+ openRunMs += millisBetween(run.started_at, generatedAt);
17157
+ const usage = extractUsage(parseJsonObject4(run.metadata));
17158
+ metadataUsage.tokens += usage.tokens;
17159
+ metadataUsage.cost_usd += usage.cost_usd;
17160
+ metadataUsage.duration_ms += usage.duration_ms;
17161
+ metadataUsage.records += usage.records;
17162
+ }
17163
+ const eventRows = queryAll(d, `SELECT e.data
17164
+ FROM task_run_events e
17165
+ JOIN task_runs r ON r.id = e.run_id
17166
+ JOIN tasks t ON t.id = e.task_id
17167
+ ${runClause}`, runParams);
17168
+ for (const event of eventRows) {
17169
+ const usage = extractUsage(parseJsonObject4(event.data));
17170
+ metadataUsage.tokens += usage.tokens;
17171
+ metadataUsage.cost_usd += usage.cost_usd;
17172
+ metadataUsage.duration_ms += usage.duration_ms;
17173
+ metadataUsage.records += usage.records;
17174
+ }
17175
+ const taskTokens = Number(taskTotals.task_tokens || 0);
17176
+ const traceTokens = Number(traceTotals.tokens || 0);
17177
+ const metadataTokens = metadataUsage.tokens;
17178
+ const taskCost = Number(taskTotals.task_cost_usd || 0);
17179
+ const traceCost = Number(traceTotals.cost_usd || 0);
17180
+ const metadataCost = metadataUsage.cost_usd;
17181
+ const traceMs = Number(traceTotals.duration_ms || 0);
17182
+ const baseReport = {
17183
+ schema_version: LOCAL_USAGE_LEDGER_SCHEMA_VERSION,
17184
+ local_only: true,
17185
+ no_network: true,
17186
+ generated_at: generatedAt,
17187
+ scope: {
17188
+ project_id: options.project_id || null,
17189
+ agent_id: options.agent_id || null,
17190
+ since: options.since || null,
17191
+ until: options.until || null
17192
+ },
17193
+ counts: {
17194
+ tasks: Number(taskTotals.tasks || 0),
17195
+ projects: Number(projectCount || 0),
17196
+ runs: runs.length,
17197
+ commands: Number(commandTotals.commands || 0),
17198
+ artifacts: Number(artifactTotals.artifacts || 0),
17199
+ traces: Number(traceTotals.traces || 0),
17200
+ metadata_records: metadataUsage.records
17201
+ },
17202
+ durations: {
17203
+ completed_run_ms: completedRunMs,
17204
+ open_run_ms: openRunMs,
17205
+ trace_ms: traceMs,
17206
+ total_observed_ms: completedRunMs + openRunMs + traceMs + metadataUsage.duration_ms
17207
+ },
17208
+ usage: {
17209
+ task_tokens: taskTokens,
17210
+ trace_tokens: traceTokens,
17211
+ metadata_tokens: metadataTokens,
17212
+ total_tokens: taskTokens + traceTokens + metadataTokens,
17213
+ task_cost_usd: rounded(taskCost),
17214
+ trace_cost_usd: rounded(traceCost),
17215
+ metadata_cost_usd: rounded(metadataCost),
17216
+ total_cost_usd: rounded(taskCost + traceCost + metadataCost)
17217
+ },
17218
+ storage: {
17219
+ artifact_bytes: Number(artifactTotals.bytes || 0),
17220
+ evidence_bytes: Number(artifactTotals.bytes || 0)
17221
+ },
17222
+ redaction: {
17223
+ raw_commands_included: false,
17224
+ raw_artifact_paths_included: false,
17225
+ aggregate_only: true
17226
+ },
17227
+ sources: ["tasks", "projects", "task_runs", "task_run_commands", "task_run_artifacts", "task_run_events", "task_traces"]
17228
+ };
17229
+ return {
17230
+ ...baseReport,
17231
+ quota: buildQuota(options, baseReport)
17232
+ };
17233
+ }
17234
+ function renderLocalUsageLedgerMarkdown(report) {
17235
+ const minutes2 = (report.durations.total_observed_ms / 60000).toFixed(1);
17236
+ const lines = [
17237
+ "# Local Usage Ledger",
17238
+ "",
17239
+ `Generated: ${report.generated_at}`,
17240
+ `Scope: project=${report.scope.project_id || "all"} agent=${report.scope.agent_id || "all"}`,
17241
+ "",
17242
+ "## Counts",
17243
+ `- Tasks: ${report.counts.tasks}`,
17244
+ `- Projects: ${report.counts.projects}`,
17245
+ `- Runs: ${report.counts.runs}`,
17246
+ `- Commands: ${report.counts.commands}`,
17247
+ `- Artifacts: ${report.counts.artifacts}`,
17248
+ `- Traces: ${report.counts.traces}`,
17249
+ "",
17250
+ "## Usage",
17251
+ `- Tokens: ${report.usage.total_tokens}`,
17252
+ `- Cost USD: ${report.usage.total_cost_usd}`,
17253
+ `- Observed duration minutes: ${minutes2}`,
17254
+ `- Evidence bytes: ${report.storage.evidence_bytes}`,
17255
+ "",
17256
+ "## Quota"
17257
+ ];
17258
+ if (!report.quota.simulated) {
17259
+ lines.push("- No local quota limits supplied.");
17260
+ } else {
17261
+ for (const limit of report.quota.limits) {
17262
+ lines.push(`- ${limit.name}: ${limit.used}/${limit.limit}${limit.exceeded ? " exceeded" : ""}`);
16280
17263
  }
16281
- });
16282
- return profile;
17264
+ lines.push(`- Allowed: ${report.quota.allowed ? "yes" : "no"}`);
17265
+ }
17266
+ lines.push("", "Raw commands and artifact paths are not included in this aggregate report.");
17267
+ return lines.join(`
17268
+ `);
16283
17269
  }
16284
- function removeEncryptionProfile(name) {
16285
- const normalized = normalizeProfileName(name);
16286
- const config = loadConfig();
16287
- if (!config.encryption_profiles?.[normalized])
17270
+ // src/lib/local-reports.ts
17271
+ init_database();
17272
+ init_database();
17273
+ var LOCAL_REPORT_SCHEMA_VERSION = 1;
17274
+ var LOCAL_REPORT_TYPES = [
17275
+ "ready",
17276
+ "blocked",
17277
+ "overdue",
17278
+ "standup",
17279
+ "sprint",
17280
+ "progress",
17281
+ "run_outcomes",
17282
+ "verification_evidence",
17283
+ "agent_summary"
17284
+ ];
17285
+ function limitValue(value) {
17286
+ if (value === undefined || !Number.isFinite(value))
17287
+ return 20;
17288
+ return Math.max(1, Math.min(500, Math.trunc(value)));
17289
+ }
17290
+ function isTerminal(task2) {
17291
+ return task2.status === "completed" || task2.status === "failed" || task2.status === "cancelled";
17292
+ }
17293
+ function sameAgent(task2, agentId) {
17294
+ if (!agentId)
17295
+ return true;
17296
+ return task2.assigned_to === agentId || task2.agent_id === agentId;
17297
+ }
17298
+ function withinTaskWindow(task2, options) {
17299
+ const time = Date.parse(task2.updated_at);
17300
+ if (!Number.isFinite(time))
17301
+ return true;
17302
+ if (options.since && time < Date.parse(options.since))
17303
+ return false;
17304
+ if (options.until && time > Date.parse(options.until))
16288
17305
  return false;
16289
- const next = { ...config.encryption_profiles };
16290
- delete next[normalized];
16291
- saveConfig({ ...config, encryption_profiles: next });
16292
17306
  return true;
16293
17307
  }
16294
- function encryptionProfileStatus(name = DEFAULT_ENCRYPTION_PROFILE, env = process.env) {
16295
- const profile = ensureEncryptionProfile(name);
17308
+ function scopedTasks(options, db) {
17309
+ return listTasks({
17310
+ project_id: options.project_id,
17311
+ plan_id: options.plan_id,
17312
+ include_archived: false
17313
+ }, db).filter((task2) => sameAgent(task2, options.agent_id) && withinTaskWindow(task2, options));
17314
+ }
17315
+ function summarizeTask(task2) {
16296
17316
  return {
16297
- profile: redactValue(profile),
16298
- locked: !env[profile.key_env],
16299
- key_env: profile.key_env,
16300
- key_present: Boolean(env[profile.key_env])
17317
+ id: task2.id,
17318
+ short_id: task2.short_id,
17319
+ title: task2.title,
17320
+ status: task2.status,
17321
+ priority: task2.priority,
17322
+ project_id: task2.project_id,
17323
+ plan_id: task2.plan_id,
17324
+ assigned_to: task2.assigned_to,
17325
+ due_at: task2.due_at,
17326
+ updated_at: task2.updated_at
16301
17327
  };
16302
17328
  }
16303
- function keyForProfile(profile, env) {
16304
- const secret = env[profile.key_env];
16305
- if (!secret)
16306
- throw new EncryptionKeyUnavailableError(profile.key_env, profile.name);
16307
- return deriveKey(secret, profile.salt);
17329
+ function overdueTasks(tasks, nowIso) {
17330
+ const now2 = Date.parse(nowIso);
17331
+ return tasks.filter((task2) => {
17332
+ if (isTerminal(task2) || !task2.due_at)
17333
+ return false;
17334
+ const due = Date.parse(task2.due_at);
17335
+ return Number.isFinite(due) && due < now2;
17336
+ });
16308
17337
  }
16309
- function encryptString(plaintext, options = {}) {
16310
- const profile = ensureEncryptionProfile(options.profile);
16311
- const key = keyForProfile(profile, options.env ?? process.env);
16312
- const iv = randomBytes(12);
16313
- const cipher = createCipheriv("aes-256-gcm", key, iv);
16314
- const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
16315
- const authTag = cipher.getAuthTag();
17338
+ function isReady(task2, db) {
17339
+ if (task2.status !== "pending")
17340
+ return false;
17341
+ if (task2.locked_by && !isLockExpired(task2.locked_at))
17342
+ return false;
17343
+ return getBlockingDeps(task2.id, db).length === 0;
17344
+ }
17345
+ function pushTaskCounts(summary, task2, blockedIds, overdueIds) {
17346
+ summary.task_counts.total += 1;
17347
+ summary.task_counts[task2.status] += 1;
17348
+ if (blockedIds.has(task2.id))
17349
+ summary.task_counts.blocked += 1;
17350
+ if (overdueIds.has(task2.id))
17351
+ summary.task_counts.overdue += 1;
17352
+ }
17353
+ function initialAgentSummary(agentId) {
17354
+ return {
17355
+ agent_id: agentId,
17356
+ task_counts: {
17357
+ total: 0,
17358
+ pending: 0,
17359
+ in_progress: 0,
17360
+ completed: 0,
17361
+ failed: 0,
17362
+ cancelled: 0,
17363
+ blocked: 0,
17364
+ overdue: 0
17365
+ },
17366
+ run_outcomes: {
17367
+ running: 0,
17368
+ completed: 0,
17369
+ failed: 0,
17370
+ cancelled: 0
17371
+ },
17372
+ verification_outcomes: {
17373
+ passed: 0,
17374
+ failed: 0,
17375
+ unknown: 0
17376
+ }
17377
+ };
17378
+ }
17379
+ function addScopeClauses(where, params, options, timeColumn) {
17380
+ if (options.project_id) {
17381
+ where.push("t.project_id = ?");
17382
+ params.push(options.project_id);
17383
+ }
17384
+ if (options.plan_id) {
17385
+ where.push("t.plan_id = ?");
17386
+ params.push(options.plan_id);
17387
+ }
17388
+ if (options.agent_id) {
17389
+ where.push("(r.agent_id = ? OR t.agent_id = ? OR t.assigned_to = ?)");
17390
+ params.push(options.agent_id, options.agent_id, options.agent_id);
17391
+ }
17392
+ if (options.since) {
17393
+ where.push(`${timeColumn} >= ?`);
17394
+ params.push(options.since);
17395
+ }
17396
+ if (options.until) {
17397
+ where.push(`${timeColumn} <= ?`);
17398
+ params.push(options.until);
17399
+ }
17400
+ }
17401
+ function loadRuns(options, db) {
17402
+ const where = ["t.archived_at IS NULL"];
17403
+ const params = [];
17404
+ addScopeClauses(where, params, options, "r.started_at");
17405
+ return db.query(`
17406
+ SELECT
17407
+ r.id,
17408
+ r.task_id,
17409
+ t.title AS task_title,
17410
+ t.project_id,
17411
+ t.plan_id,
17412
+ t.agent_id AS task_agent_id,
17413
+ t.assigned_to,
17414
+ r.agent_id,
17415
+ r.status,
17416
+ r.summary,
17417
+ r.started_at,
17418
+ r.completed_at,
17419
+ SUM(CASE WHEN c.status = 'passed' THEN 1 ELSE 0 END) AS passed_commands,
17420
+ SUM(CASE WHEN c.status = 'failed' THEN 1 ELSE 0 END) AS failed_commands,
17421
+ SUM(CASE WHEN c.status = 'unknown' THEN 1 ELSE 0 END) AS unknown_commands,
17422
+ COUNT(DISTINCT a.id) AS artifacts
17423
+ FROM task_runs r
17424
+ JOIN tasks t ON t.id = r.task_id
17425
+ LEFT JOIN task_run_commands c ON c.run_id = r.id
17426
+ LEFT JOIN task_run_artifacts a ON a.run_id = r.id
17427
+ WHERE ${where.join(" AND ")}
17428
+ GROUP BY r.id
17429
+ ORDER BY r.started_at DESC, r.created_at DESC
17430
+ `).all(...params);
17431
+ }
17432
+ function loadVerifications(options, db) {
17433
+ const where = ["t.archived_at IS NULL"];
17434
+ const params = [];
17435
+ if (options.project_id) {
17436
+ where.push("t.project_id = ?");
17437
+ params.push(options.project_id);
17438
+ }
17439
+ if (options.plan_id) {
17440
+ where.push("t.plan_id = ?");
17441
+ params.push(options.plan_id);
17442
+ }
17443
+ if (options.agent_id) {
17444
+ where.push("(v.agent_id = ? OR t.agent_id = ? OR t.assigned_to = ?)");
17445
+ params.push(options.agent_id, options.agent_id, options.agent_id);
17446
+ }
17447
+ if (options.since) {
17448
+ where.push("v.run_at >= ?");
17449
+ params.push(options.since);
17450
+ }
17451
+ if (options.until) {
17452
+ where.push("v.run_at <= ?");
17453
+ params.push(options.until);
17454
+ }
17455
+ return db.query(`
17456
+ SELECT
17457
+ v.id,
17458
+ v.task_id,
17459
+ t.title AS task_title,
17460
+ t.project_id,
17461
+ t.plan_id,
17462
+ t.agent_id AS task_agent_id,
17463
+ t.assigned_to,
17464
+ v.agent_id,
17465
+ v.status,
17466
+ v.command,
17467
+ v.output_summary,
17468
+ v.run_at
17469
+ FROM task_verifications v
17470
+ JOIN tasks t ON t.id = v.task_id
17471
+ WHERE ${where.join(" AND ")}
17472
+ ORDER BY v.run_at DESC, v.created_at DESC
17473
+ `).all(...params);
17474
+ }
17475
+ function agentKey(value) {
17476
+ return value || "unassigned";
17477
+ }
17478
+ function listLocalReportTypes() {
17479
+ return [...LOCAL_REPORT_TYPES];
17480
+ }
17481
+ function createLocalReport(options = {}, db) {
17482
+ const d = db || getDatabase();
17483
+ const limit = limitValue(options.limit);
17484
+ const generatedAt = options.generated_at ?? new Date().toISOString();
17485
+ const nowIso = options.now ?? generatedAt;
17486
+ const tasks = scopedTasks(options, d);
17487
+ const overdue = overdueTasks(tasks, nowIso);
17488
+ const overdueIds = new Set(overdue.map((task2) => task2.id));
17489
+ const blocked = tasks.filter((task2) => task2.status === "pending").map((task2) => ({ task: task2, blockers: getBlockingDeps(task2.id, d) })).filter((item) => item.blockers.length > 0);
17490
+ const blockedIds = new Set(blocked.map((item) => item.task.id));
17491
+ const ready = tasks.filter((task2) => isReady(task2, d));
17492
+ const runs = loadRuns(options, d);
17493
+ const verifications = loadVerifications(options, d);
17494
+ const planSummaries = listPlans(options.project_id, d).filter((plan) => !options.plan_id || plan.id === options.plan_id).map((plan) => {
17495
+ const planTasks = tasks.filter((task2) => task2.plan_id === plan.id);
17496
+ const completed = planTasks.filter((task2) => task2.status === "completed").length;
17497
+ const total = planTasks.length;
17498
+ return {
17499
+ id: plan.id,
17500
+ name: plan.name,
17501
+ status: plan.status,
17502
+ project_id: plan.project_id,
17503
+ agent_id: plan.agent_id,
17504
+ counts: {
17505
+ total,
17506
+ pending: planTasks.filter((task2) => task2.status === "pending").length,
17507
+ in_progress: planTasks.filter((task2) => task2.status === "in_progress").length,
17508
+ completed,
17509
+ failed: planTasks.filter((task2) => task2.status === "failed").length,
17510
+ cancelled: planTasks.filter((task2) => task2.status === "cancelled").length,
17511
+ blocked: planTasks.filter((task2) => blockedIds.has(task2.id)).length,
17512
+ overdue: planTasks.filter((task2) => overdueIds.has(task2.id)).length
17513
+ },
17514
+ progress_percent: total === 0 ? 0 : Math.round(completed / total * 100)
17515
+ };
17516
+ }).filter((plan) => plan.counts.total > 0 || options.plan_id === plan.id);
17517
+ const runOutcomes = { running: 0, completed: 0, failed: 0, cancelled: 0 };
17518
+ for (const run of runs)
17519
+ runOutcomes[run.status] += 1;
17520
+ const verificationOutcomes = { passed: 0, failed: 0, unknown: 0 };
17521
+ for (const verification of verifications)
17522
+ verificationOutcomes[verification.status] += 1;
17523
+ const agents = new Map;
17524
+ function getAgent(id) {
17525
+ if (!agents.has(id))
17526
+ agents.set(id, initialAgentSummary(id));
17527
+ return agents.get(id);
17528
+ }
17529
+ for (const task2 of tasks) {
17530
+ const summary = getAgent(agentKey(task2.assigned_to || task2.agent_id));
17531
+ pushTaskCounts(summary, task2, blockedIds, overdueIds);
17532
+ }
17533
+ for (const run of runs) {
17534
+ const summary = getAgent(agentKey(run.agent_id || run.assigned_to || run.task_agent_id));
17535
+ summary.run_outcomes[run.status] += 1;
17536
+ }
17537
+ for (const verification of verifications) {
17538
+ const summary = getAgent(agentKey(verification.agent_id || verification.assigned_to || verification.task_agent_id));
17539
+ summary.verification_outcomes[verification.status] += 1;
17540
+ }
17541
+ return {
17542
+ schema_version: LOCAL_REPORT_SCHEMA_VERSION,
17543
+ local_only: true,
17544
+ no_network: true,
17545
+ generated_at: generatedAt,
17546
+ scope: {
17547
+ project_id: options.project_id ?? null,
17548
+ plan_id: options.plan_id ?? null,
17549
+ agent_id: options.agent_id ?? null,
17550
+ since: options.since ?? null,
17551
+ until: options.until ?? null
17552
+ },
17553
+ report_types: listLocalReportTypes(),
17554
+ views: {
17555
+ ready: {
17556
+ type: "ready",
17557
+ total: ready.length,
17558
+ items: ready.slice(0, limit).map(summarizeTask)
17559
+ },
17560
+ blocked: {
17561
+ type: "blocked",
17562
+ total: blocked.length,
17563
+ items: blocked.slice(0, limit).map(({ task: task2, blockers }) => ({
17564
+ ...summarizeTask(task2),
17565
+ blocked_by: blockers.map(summarizeTask)
17566
+ }))
17567
+ },
17568
+ overdue: {
17569
+ type: "overdue",
17570
+ total: overdue.length,
17571
+ items: overdue.slice(0, limit).map(summarizeTask)
17572
+ }
17573
+ },
17574
+ plans: planSummaries,
17575
+ runs: {
17576
+ outcomes: runOutcomes,
17577
+ recent: runs.slice(0, limit).map((run) => ({
17578
+ id: run.id,
17579
+ task_id: run.task_id,
17580
+ task_title: run.task_title,
17581
+ agent_id: run.agent_id,
17582
+ status: run.status,
17583
+ summary: run.summary,
17584
+ started_at: run.started_at,
17585
+ completed_at: run.completed_at,
17586
+ command_outcomes: {
17587
+ passed: Number(run.passed_commands || 0),
17588
+ failed: Number(run.failed_commands || 0),
17589
+ unknown: Number(run.unknown_commands || 0)
17590
+ },
17591
+ artifacts: Number(run.artifacts || 0)
17592
+ }))
17593
+ },
17594
+ verification: {
17595
+ outcomes: verificationOutcomes,
17596
+ recent: verifications.slice(0, limit).map((verification) => ({
17597
+ id: verification.id,
17598
+ task_id: verification.task_id,
17599
+ task_title: verification.task_title,
17600
+ agent_id: verification.agent_id,
17601
+ status: verification.status,
17602
+ command: verification.command,
17603
+ output_summary: verification.output_summary,
17604
+ run_at: verification.run_at
17605
+ }))
17606
+ },
17607
+ agents: [...agents.values()].sort((left, right) => left.agent_id.localeCompare(right.agent_id)),
17608
+ exports: {
17609
+ json_contract: "local_report",
17610
+ markdown_supported: true
17611
+ }
17612
+ };
17613
+ }
17614
+ function taskLine(task2) {
17615
+ const due = task2.due_at ? ` due ${task2.due_at.slice(0, 10)}` : "";
17616
+ const assignee = task2.assigned_to ? ` @${task2.assigned_to}` : "";
17617
+ return `- ${task2.short_id || task2.id.slice(0, 8)} ${task2.title} [${task2.status}/${task2.priority}]${assignee}${due}`;
17618
+ }
17619
+ function outcomeLine(values) {
17620
+ return Object.entries(values).map(([key, value]) => `${key}: ${value}`).join(", ");
17621
+ }
17622
+ function renderLocalReportMarkdown(report) {
17623
+ const lines = [
17624
+ "# Local Agent Report",
17625
+ "",
17626
+ `Generated: ${report.generated_at}`,
17627
+ `Scope: project ${report.scope.project_id ?? "all"}; plan ${report.scope.plan_id ?? "all"}; agent ${report.scope.agent_id ?? "all"}`,
17628
+ "",
17629
+ "## Task Views",
17630
+ "",
17631
+ `Ready (${report.views.ready.total})`,
17632
+ ...report.views.ready.items.length ? report.views.ready.items.map(taskLine) : ["- none"],
17633
+ "",
17634
+ `Blocked (${report.views.blocked.total})`,
17635
+ ...report.views.blocked.items.length ? report.views.blocked.items.map((task2) => `${taskLine(task2)}; blocked by ${task2.blocked_by.map((item) => item.short_id || item.id.slice(0, 8)).join(", ")}`) : ["- none"],
17636
+ "",
17637
+ `Overdue (${report.views.overdue.total})`,
17638
+ ...report.views.overdue.items.length ? report.views.overdue.items.map(taskLine) : ["- none"],
17639
+ "",
17640
+ "## Plans"
17641
+ ];
17642
+ if (report.plans.length === 0)
17643
+ lines.push("- none");
17644
+ for (const plan of report.plans) {
17645
+ lines.push(`- ${plan.name}: ${plan.progress_percent}% complete, ${plan.counts.blocked} blocked, ${plan.counts.overdue} overdue`);
17646
+ }
17647
+ lines.push("", "## Runs", outcomeLine(report.runs.outcomes));
17648
+ for (const run of report.runs.recent) {
17649
+ lines.push(`- ${run.id.slice(0, 8)} ${run.status} ${run.task_title}${run.summary ? `: ${run.summary}` : ""}`);
17650
+ }
17651
+ lines.push("", "## Verification", outcomeLine(report.verification.outcomes));
17652
+ for (const verification of report.verification.recent) {
17653
+ lines.push(`- ${verification.status} ${verification.task_title}: ${verification.output_summary || verification.command}`);
17654
+ }
17655
+ lines.push("", "## Agents");
17656
+ if (report.agents.length === 0)
17657
+ lines.push("- none");
17658
+ for (const agent of report.agents) {
17659
+ lines.push(`- ${agent.agent_id}: ${agent.task_counts.total} tasks, ${agent.task_counts.blocked} blocked, ${agent.task_counts.overdue} overdue, runs ${outcomeLine(agent.run_outcomes)}, verification ${outcomeLine(agent.verification_outcomes)}`);
17660
+ }
17661
+ return `${lines.join(`
17662
+ `)}
17663
+ `;
17664
+ }
17665
+ // src/lib/local-encryption.ts
17666
+ import { createCipheriv, createDecipheriv, createHash as createHash7, randomBytes, scryptSync, timingSafeEqual } from "crypto";
17667
+ var TODOS_ENCRYPTED_VALUE_KIND = "hasna.todos.encrypted-value";
17668
+ var TODOS_ENCRYPTED_BRIDGE_KIND = "hasna.todos.encrypted-bridge";
17669
+ var TODOS_ENCRYPTION_SCHEMA_VERSION = 1;
17670
+ var DEFAULT_ENCRYPTION_PROFILE = "default";
17671
+ var DEFAULT_ENCRYPTION_KEY_ENV = "TODOS_ENCRYPTION_KEY";
17672
+
17673
+ class EncryptionKeyUnavailableError extends Error {
17674
+ keyEnv;
17675
+ profile;
17676
+ constructor(keyEnv, profile) {
17677
+ super(`Encryption key is locked: set ${keyEnv} to use profile ${profile}`);
17678
+ this.keyEnv = keyEnv;
17679
+ this.profile = profile;
17680
+ }
17681
+ }
17682
+
17683
+ class EncryptedPayloadError extends Error {
17684
+ constructor(message) {
17685
+ super(message);
17686
+ }
17687
+ }
17688
+ function now2() {
17689
+ return new Date().toISOString();
17690
+ }
17691
+ function sha2564(value) {
17692
+ return createHash7("sha256").update(value).digest("hex");
17693
+ }
17694
+ function normalizeProfileName(value) {
17695
+ const name = (value || DEFAULT_ENCRYPTION_PROFILE).trim();
17696
+ if (!/^[a-zA-Z0-9._-]+$/.test(name))
17697
+ throw new Error("encryption profile names may only contain letters, numbers, dots, underscores, and dashes");
17698
+ return name;
17699
+ }
17700
+ function randomBase64(bytes) {
17701
+ return randomBytes(bytes).toString("base64");
17702
+ }
17703
+ function deriveKey(secret, salt) {
17704
+ if (secret.length < 12)
17705
+ throw new Error("encryption key must be at least 12 characters");
17706
+ return scryptSync(secret, Buffer.from(salt, "base64"), 32);
17707
+ }
17708
+ function profileFromConfig(name) {
17709
+ return loadConfig().encryption_profiles?.[name] ?? null;
17710
+ }
17711
+ function ensureEncryptionProfile(name = DEFAULT_ENCRYPTION_PROFILE) {
17712
+ const normalized = normalizeProfileName(name);
17713
+ const existing = profileFromConfig(normalized);
17714
+ if (existing)
17715
+ return existing;
17716
+ return upsertEncryptionProfile({ name: normalized });
17717
+ }
17718
+ function listEncryptionProfiles() {
17719
+ return Object.values(loadConfig().encryption_profiles ?? {}).sort((left, right) => left.name.localeCompare(right.name));
17720
+ }
17721
+ function upsertEncryptionProfile(input) {
17722
+ const name = normalizeProfileName(input.name);
17723
+ const config = loadConfig();
17724
+ const existing = config.encryption_profiles?.[name];
17725
+ const timestamp2 = now2();
17726
+ const profile = {
17727
+ name,
17728
+ algorithm: "aes-256-gcm",
17729
+ kdf: "scrypt",
17730
+ key_env: input.key_env?.trim() || existing?.key_env || DEFAULT_ENCRYPTION_KEY_ENV,
17731
+ salt: input.salt || existing?.salt || randomBase64(16),
17732
+ description: input.description ?? existing?.description,
17733
+ created_at: existing?.created_at || timestamp2,
17734
+ updated_at: timestamp2
17735
+ };
17736
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(profile.key_env))
17737
+ throw new Error("key_env must be a valid environment variable name");
17738
+ saveConfig({
17739
+ ...config,
17740
+ encryption_profiles: {
17741
+ ...config.encryption_profiles ?? {},
17742
+ [name]: profile
17743
+ }
17744
+ });
17745
+ return profile;
17746
+ }
17747
+ function removeEncryptionProfile(name) {
17748
+ const normalized = normalizeProfileName(name);
17749
+ const config = loadConfig();
17750
+ if (!config.encryption_profiles?.[normalized])
17751
+ return false;
17752
+ const next = { ...config.encryption_profiles };
17753
+ delete next[normalized];
17754
+ saveConfig({ ...config, encryption_profiles: next });
17755
+ return true;
17756
+ }
17757
+ function encryptionProfileStatus(name = DEFAULT_ENCRYPTION_PROFILE, env = process.env) {
17758
+ const profile = ensureEncryptionProfile(name);
17759
+ return {
17760
+ profile: redactValue(profile),
17761
+ locked: !env[profile.key_env],
17762
+ key_env: profile.key_env,
17763
+ key_present: Boolean(env[profile.key_env])
17764
+ };
17765
+ }
17766
+ function keyForProfile(profile, env) {
17767
+ const secret = env[profile.key_env];
17768
+ if (!secret)
17769
+ throw new EncryptionKeyUnavailableError(profile.key_env, profile.name);
17770
+ return deriveKey(secret, profile.salt);
17771
+ }
17772
+ function encryptString(plaintext, options = {}) {
17773
+ const profile = ensureEncryptionProfile(options.profile);
17774
+ const key = keyForProfile(profile, options.env ?? process.env);
17775
+ const iv = randomBytes(12);
17776
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
17777
+ const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
17778
+ const authTag = cipher.getAuthTag();
16316
17779
  return {
16317
17780
  schemaVersion: TODOS_ENCRYPTION_SCHEMA_VERSION,
16318
17781
  kind: TODOS_ENCRYPTED_VALUE_KIND,
@@ -16325,7 +17788,7 @@ function encryptString(plaintext, options = {}) {
16325
17788
  iv: iv.toString("base64"),
16326
17789
  auth_tag: authTag.toString("base64"),
16327
17790
  ciphertext: ciphertext.toString("base64"),
16328
- plaintext_sha256: sha2563(plaintext)
17791
+ plaintext_sha256: sha2564(plaintext)
16329
17792
  };
16330
17793
  }
16331
17794
  function isEncryptedValue(value) {
@@ -16350,7 +17813,7 @@ function decryptString(envelope, env = process.env) {
16350
17813
  decipher.final()
16351
17814
  ]).toString("utf8");
16352
17815
  const expected = Buffer.from(envelope.plaintext_sha256, "hex");
16353
- const actual = Buffer.from(sha2563(plaintext), "hex");
17816
+ const actual = Buffer.from(sha2564(plaintext), "hex");
16354
17817
  if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
16355
17818
  throw new EncryptedPayloadError("decrypted payload checksum mismatch");
16356
17819
  }
@@ -17056,10 +18519,63 @@ var NICE_AGENT_NAMES = [
17056
18519
  "vesper",
17057
18520
  "zephyr"
17058
18521
  ];
18522
+ var EXTENDED_AGENT_NAMES = [
18523
+ "agrippa",
18524
+ "antonius",
18525
+ "aurelian",
18526
+ "aurelius",
18527
+ "camillus",
18528
+ "cassius",
18529
+ "celer",
18530
+ "cincinnatus",
18531
+ "corvus",
18532
+ "drusus",
18533
+ "fabius",
18534
+ "faustus",
18535
+ "flaccus",
18536
+ "gallus",
18537
+ "gaius",
18538
+ "horatius",
18539
+ "lucius",
18540
+ "lucullus",
18541
+ "marius",
18542
+ "marcellus",
18543
+ "maximus",
18544
+ "nerva",
18545
+ "pompey",
18546
+ "quintus",
18547
+ "regulus",
18548
+ "romulus",
18549
+ "scipio",
18550
+ "seneca",
18551
+ "sertorius",
18552
+ "sulla",
18553
+ "tacitus",
18554
+ "varro",
18555
+ "vitruvius",
18556
+ "plato",
18557
+ "socrates",
18558
+ "aristotle",
18559
+ "heraclitus",
18560
+ "democritus",
18561
+ "pythagoras",
18562
+ "hipparchus",
18563
+ "euclid",
18564
+ "archimedes",
18565
+ "zeno",
18566
+ "anaximander",
18567
+ "epictetus",
18568
+ "aeschylus",
18569
+ "sophocles",
18570
+ "euripides",
18571
+ "xenophon",
18572
+ "diogenes"
18573
+ ];
17059
18574
  var PREFERRED_AGENT_NAMES = [
17060
18575
  ...ROMAN_AGENT_NAMES,
17061
18576
  ...GREEK_AGENT_NAMES,
17062
- ...NICE_AGENT_NAMES
18577
+ ...NICE_AGENT_NAMES,
18578
+ ...EXTENDED_AGENT_NAMES
17063
18579
  ];
17064
18580
  var RESERVED_GENERIC_NAMES = new Set([
17065
18581
  "agent",
@@ -17101,29 +18617,93 @@ function isBlockedAgentName(name) {
17101
18617
  const normalized = normalizeAgentNameInput(name);
17102
18618
  return isGenericAgentName(normalized) || hasGeneratedNumericSuffix(normalized) || !ONE_WORD_NAME_RE.test(normalized);
17103
18619
  }
17104
- function alphabeticSuffix(index) {
17105
- const letters = "abcdefghijklmnopqrstuvwxyz";
17106
- let value = index;
17107
- let suffix = "";
17108
- do {
17109
- suffix = letters[value % letters.length] + suffix;
17110
- value = Math.floor(value / letters.length) - 1;
17111
- } while (value >= 0);
17112
- return suffix;
18620
+ var FALLBACK_PREFIXES = [
18621
+ "arv",
18622
+ "bel",
18623
+ "cyr",
18624
+ "dax",
18625
+ "elun",
18626
+ "feno",
18627
+ "gavor",
18628
+ "hiro",
18629
+ "ivar",
18630
+ "jaro",
18631
+ "kavo",
18632
+ "lumo",
18633
+ "myr",
18634
+ "navo",
18635
+ "prax",
18636
+ "quor",
18637
+ "riven",
18638
+ "sovan",
18639
+ "tavor",
18640
+ "ulmor",
18641
+ "vexo",
18642
+ "wiro",
18643
+ "yaro",
18644
+ "zel"
18645
+ ];
18646
+ var FALLBACK_STEMS = [
18647
+ "al",
18648
+ "ber",
18649
+ "cor",
18650
+ "dren",
18651
+ "el",
18652
+ "far",
18653
+ "gor",
18654
+ "hal",
18655
+ "ion",
18656
+ "jor",
18657
+ "kel",
18658
+ "lor",
18659
+ "mor",
18660
+ "nel",
18661
+ "or",
18662
+ "per",
18663
+ "quil",
18664
+ "ron",
18665
+ "ser",
18666
+ "tor",
18667
+ "um",
18668
+ "ver",
18669
+ "wyn",
18670
+ "xil"
18671
+ ];
18672
+ var FALLBACK_ENDINGS = [
18673
+ "a",
18674
+ "en",
18675
+ "ia",
18676
+ "is",
18677
+ "on",
18678
+ "or",
18679
+ "um",
18680
+ "us",
18681
+ "yn",
18682
+ "ar",
18683
+ "el",
18684
+ "ir"
18685
+ ];
18686
+ function generatedFallbackAgentName(index) {
18687
+ const perPrefix = FALLBACK_STEMS.length * FALLBACK_ENDINGS.length;
18688
+ const total = FALLBACK_PREFIXES.length * perPrefix;
18689
+ if (index >= total)
18690
+ return null;
18691
+ const prefix = FALLBACK_PREFIXES[Math.floor(index / perPrefix)];
18692
+ const rest = index % perPrefix;
18693
+ const stem = FALLBACK_STEMS[Math.floor(rest / FALLBACK_ENDINGS.length)];
18694
+ const ending = FALLBACK_ENDINGS[rest % FALLBACK_ENDINGS.length];
18695
+ return `${prefix}${stem}${ending}`;
17113
18696
  }
17114
18697
  function suggestAgentNames(existingNames = []) {
17115
18698
  const existing = new Set([...existingNames].map(normalizeAgentNameInput));
17116
18699
  const suggestions = PREFERRED_AGENT_NAMES.filter((name) => !existing.has(name));
17117
- for (let suffixIndex = 0;suggestions.length < 20 && suffixIndex < 1000; suffixIndex++) {
17118
- const suffix = alphabeticSuffix(suffixIndex);
17119
- for (const base of PREFERRED_AGENT_NAMES) {
17120
- const candidate = `${base}${suffix}`;
17121
- if (existing.has(candidate) || suggestions.includes(candidate))
17122
- continue;
17123
- suggestions.push(candidate);
17124
- if (suggestions.length >= 20)
17125
- break;
17126
- }
18700
+ for (let index = 0;suggestions.length < 20; index++) {
18701
+ const candidate = generatedFallbackAgentName(index);
18702
+ if (!candidate)
18703
+ break;
18704
+ if (existing.has(candidate) || suggestions.includes(candidate))
18705
+ continue;
18706
+ suggestions.push(candidate);
17127
18707
  }
17128
18708
  return suggestions;
17129
18709
  }
@@ -18124,7 +19704,7 @@ class TodosClient {
18124
19704
  return this._fetchWithRetry(path, { method: "DELETE" });
18125
19705
  }
18126
19706
  _sleep(ms) {
18127
- return new Promise((resolve8) => setTimeout(resolve8, ms));
19707
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
18128
19708
  }
18129
19709
  async getHealth() {
18130
19710
  return this._get("/api/health");
@@ -19376,8 +20956,8 @@ function renderRetrospectiveMarkdown(record) {
19376
20956
  }
19377
20957
  // src/lib/project-bootstrap.ts
19378
20958
  init_database();
19379
- import { existsSync as existsSync7, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
19380
- import { basename, dirname as dirname6, resolve as resolve8 } from "path";
20959
+ import { existsSync as existsSync7, readFileSync as readFileSync6, statSync as statSync3 } from "fs";
20960
+ import { basename, dirname as dirname7, resolve as resolve9 } from "path";
19381
20961
  function safeStat(path) {
19382
20962
  try {
19383
20963
  return statSync3(path);
@@ -19386,18 +20966,18 @@ function safeStat(path) {
19386
20966
  }
19387
20967
  }
19388
20968
  function canonicalPath(input) {
19389
- const resolved = resolve8(input);
20969
+ const resolved = resolve9(input);
19390
20970
  const stats2 = safeStat(resolved);
19391
20971
  if (stats2?.isFile())
19392
- return dirname6(resolved);
20972
+ return dirname7(resolved);
19393
20973
  return resolved;
19394
20974
  }
19395
20975
  function findUp(start, marker) {
19396
20976
  let current = canonicalPath(start);
19397
20977
  while (true) {
19398
- if (existsSync7(resolve8(current, marker)))
20978
+ if (existsSync7(resolve9(current, marker)))
19399
20979
  return current;
19400
- const parent = dirname6(current);
20980
+ const parent = dirname7(current);
19401
20981
  if (parent === current)
19402
20982
  return null;
19403
20983
  current = parent;
@@ -19406,11 +20986,11 @@ function findUp(start, marker) {
19406
20986
  function readPackageJson2(path) {
19407
20987
  if (!path)
19408
20988
  return null;
19409
- const file = resolve8(path, "package.json");
20989
+ const file = resolve9(path, "package.json");
19410
20990
  if (!existsSync7(file))
19411
20991
  return null;
19412
20992
  try {
19413
- const parsed = JSON.parse(readFileSync5(file, "utf-8"));
20993
+ const parsed = JSON.parse(readFileSync6(file, "utf-8"));
19414
20994
  return parsed && typeof parsed === "object" ? parsed : null;
19415
20995
  } catch {
19416
20996
  return null;
@@ -19429,7 +21009,7 @@ function workspaceMarker(root, rootPackage) {
19429
21009
  if (rootPackage?.workspaces)
19430
21010
  markers.push("package.json#workspaces");
19431
21011
  for (const marker of ["pnpm-workspace.yaml", "turbo.json", "nx.json", "lerna.json", "rush.json", "bun.lock", "bun.lockb"]) {
19432
- if (existsSync7(resolve8(root, marker)))
21012
+ if (existsSync7(resolve9(root, marker)))
19433
21013
  markers.push(marker);
19434
21014
  }
19435
21015
  const kind = markers.find((marker) => marker !== "bun.lock" && marker !== "bun.lockb") ?? null;
@@ -19537,7 +21117,7 @@ function getProjectByPathForBootstrap(path, db) {
19537
21117
  }
19538
21118
  // src/db/api-keys.ts
19539
21119
  init_database();
19540
- import { createHash as createHash7, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "crypto";
21120
+ import { createHash as createHash8, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "crypto";
19541
21121
  function rowToRecord(row) {
19542
21122
  return {
19543
21123
  id: row.id,
@@ -19551,7 +21131,7 @@ function rowToRecord(row) {
19551
21131
  };
19552
21132
  }
19553
21133
  function hashApiKey(key) {
19554
- return createHash7("sha256").update(key).digest("hex");
21134
+ return createHash8("sha256").update(key).digest("hex");
19555
21135
  }
19556
21136
  function safeEqualHex(a, b) {
19557
21137
  if (a.length !== b.length)
@@ -19741,7 +21321,7 @@ var gatherTrainingData = async (options = {}) => {
19741
21321
  };
19742
21322
  };
19743
21323
  // src/lib/model-config.ts
19744
- import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
21324
+ import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
19745
21325
  import { homedir } from "os";
19746
21326
  import { join as join9 } from "path";
19747
21327
  var DEFAULT_MODEL = "gpt-4o-mini";
@@ -19759,7 +21339,7 @@ function readConfig() {
19759
21339
  if (!existsSync8(CONFIG_PATH))
19760
21340
  return {};
19761
21341
  try {
19762
- const raw = readFileSync6(CONFIG_PATH, "utf-8");
21342
+ const raw = readFileSync7(CONFIG_PATH, "utf-8");
19763
21343
  return JSON.parse(raw);
19764
21344
  } catch {
19765
21345
  return {};
@@ -19767,9 +21347,9 @@ function readConfig() {
19767
21347
  }
19768
21348
  function writeConfig(config) {
19769
21349
  if (!existsSync8(CONFIG_DIR)) {
19770
- mkdirSync7(CONFIG_DIR, { recursive: true });
21350
+ mkdirSync8(CONFIG_DIR, { recursive: true });
19771
21351
  }
19772
- writeFileSync5(CONFIG_PATH, JSON.stringify(config, null, 2) + `
21352
+ writeFileSync6(CONFIG_PATH, JSON.stringify(config, null, 2) + `
19773
21353
  `, "utf-8");
19774
21354
  }
19775
21355
  function getActiveModel() {
@@ -19788,7 +21368,7 @@ function clearActiveModel() {
19788
21368
  }
19789
21369
  // src/db/builtin-templates.ts
19790
21370
  init_database();
19791
- import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync6 } from "fs";
21371
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync7 } from "fs";
19792
21372
  import { join as join10 } from "path";
19793
21373
  var BUILTIN_TEMPLATE_LIBRARY_VERSION = "2026-05-21";
19794
21374
  var BUILTIN_TEMPLATE_LIBRARY_SOURCE = "bundled-local-template-library";
@@ -19999,11 +21579,11 @@ function exportBuiltinTemplateFiles() {
19999
21579
  }));
20000
21580
  }
20001
21581
  function writeBuiltinTemplateFiles(directory) {
20002
- mkdirSync8(directory, { recursive: true });
21582
+ mkdirSync9(directory, { recursive: true });
20003
21583
  const files = [];
20004
21584
  for (const entry of exportBuiltinTemplateFiles()) {
20005
21585
  const path = join10(directory, entry.filename);
20006
- writeFileSync6(path, `${JSON.stringify(entry.template, null, 2)}
21586
+ writeFileSync7(path, `${JSON.stringify(entry.template, null, 2)}
20007
21587
  `, "utf-8");
20008
21588
  files.push(path);
20009
21589
  }
@@ -20692,8 +22272,8 @@ function patrolTasks(opts, db) {
20692
22272
  detail: `Completed with confidence ${task2.confidence} (threshold: ${confidenceThreshold})`
20693
22273
  });
20694
22274
  }
20695
- const orphanedRows = d.query(`SELECT * FROM tasks WHERE status = 'pending' AND project_id IS NULL AND agent_id IS NULL AND assigned_to IS NULL ORDER BY created_at ASC`).all();
20696
- for (const row of orphanedRows) {
22275
+ const orphanedRows2 = d.query(`SELECT * FROM tasks WHERE status = 'pending' AND project_id IS NULL AND agent_id IS NULL AND assigned_to IS NULL ORDER BY created_at ASC`).all();
22276
+ for (const row of orphanedRows2) {
20697
22277
  const task2 = rowToTask3(row);
20698
22278
  issues.push({
20699
22279
  type: "orphaned",
@@ -20790,18 +22370,18 @@ function getAgentMetrics(agentId, opts, db) {
20790
22370
  let reviewScoreAvg = null;
20791
22371
  if (reviewTasks.length > 0) {
20792
22372
  let total2 = 0;
20793
- let count = 0;
22373
+ let count2 = 0;
20794
22374
  for (const row of reviewTasks) {
20795
22375
  try {
20796
22376
  const meta = JSON.parse(row.metadata);
20797
22377
  if (typeof meta._review_score === "number") {
20798
22378
  total2 += meta._review_score;
20799
- count++;
22379
+ count2++;
20800
22380
  }
20801
22381
  } catch {}
20802
22382
  }
20803
- if (count > 0)
20804
- reviewScoreAvg = total2 / count;
22383
+ if (count2 > 0)
22384
+ reviewScoreAvg = total2 / count2;
20805
22385
  }
20806
22386
  const speedScore = avgTime?.avg_minutes != null ? Math.max(0, 1 - avgTime.avg_minutes / (60 * 24)) : 0.5;
20807
22387
  const confidenceScore = avgConf?.avg_confidence ?? 0.5;
@@ -21422,7 +23002,7 @@ function likePattern(query) {
21422
23002
  return null;
21423
23003
  return `%${trimmed}%`;
21424
23004
  }
21425
- function parseJsonObject4(value) {
23005
+ function parseJsonObject5(value) {
21426
23006
  if (!value)
21427
23007
  return {};
21428
23008
  if (typeof value === "object" && !Array.isArray(value))
@@ -21437,7 +23017,7 @@ function parseJsonObject4(value) {
21437
23017
  }
21438
23018
  }
21439
23019
  function rowToTaskRun(row) {
21440
- return { ...row, metadata: parseJsonObject4(row.metadata) };
23020
+ return { ...row, metadata: parseJsonObject5(row.metadata) };
21441
23021
  }
21442
23022
  function taskMatchesSavedFilters(task2, filters, db) {
21443
23023
  if (filters.plan_id && task2.plan_id !== filters.plan_id)
@@ -21689,7 +23269,7 @@ function runSearchView(idOrName, db) {
21689
23269
  return { ...runSavedSearch(view.filters, view.scope, d), view };
21690
23270
  }
21691
23271
  // src/lib/claude-tasks.ts
21692
- import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync as readdirSync2, writeFileSync as writeFileSync7 } from "fs";
23272
+ import { existsSync as existsSync9, readFileSync as readFileSync8, readdirSync as readdirSync2, writeFileSync as writeFileSync8 } from "fs";
21693
23273
  import { join as join11 } from "path";
21694
23274
  function getTaskListDir(taskListId) {
21695
23275
  return join11(HOME, ".claude", "tasks", taskListId);
@@ -21713,11 +23293,11 @@ function readPrefixCounter(dir) {
21713
23293
  const path = join11(dir, ".prefix-counter");
21714
23294
  if (!existsSync9(path))
21715
23295
  return 0;
21716
- const val = parseInt(readFileSync7(path, "utf-8").trim(), 10);
23296
+ const val = parseInt(readFileSync8(path, "utf-8").trim(), 10);
21717
23297
  return isNaN(val) ? 0 : val;
21718
23298
  }
21719
23299
  function writePrefixCounter(dir, value) {
21720
- writeFileSync7(join11(dir, ".prefix-counter"), String(value));
23300
+ writeFileSync8(join11(dir, ".prefix-counter"), String(value));
21721
23301
  }
21722
23302
  function formatPrefixedSubject(title, prefix, counter) {
21723
23303
  const padded = String(counter).padStart(5, "0");
@@ -22212,9 +23792,9 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
22212
23792
  return { pushed, pulled, errors };
22213
23793
  }
22214
23794
  // src/lib/extract.ts
22215
- import { existsSync as existsSync11, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
22216
- import { createHash as createHash8 } from "crypto";
22217
- import { relative as relative3, resolve as resolve9, join as join13 } from "path";
23795
+ import { existsSync as existsSync11, readFileSync as readFileSync9, statSync as statSync4 } from "fs";
23796
+ import { createHash as createHash9 } from "crypto";
23797
+ import { relative as relative3, resolve as resolve10, join as join13 } from "path";
22218
23798
  var EXTRACT_TAGS = ["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"];
22219
23799
  var DEFAULT_EXTENSIONS = new Set([
22220
23800
  ".ts",
@@ -22278,18 +23858,18 @@ var SKIP_DIRS = new Set([
22278
23858
  ".parcel-cache"
22279
23859
  ]);
22280
23860
  function stableHash(value) {
22281
- return createHash8("sha256").update(value).digest("hex");
23861
+ return createHash9("sha256").update(value).digest("hex");
22282
23862
  }
22283
23863
  function normalizePathForMatch(value) {
22284
23864
  return value.replace(/\\/g, "/").replace(/^\.\//, "");
22285
23865
  }
22286
23866
  function readGitignorePatterns(basePath) {
22287
- const root = statSync4(basePath).isFile() ? resolve9(basePath, "..") : basePath;
23867
+ const root = statSync4(basePath).isFile() ? resolve10(basePath, "..") : basePath;
22288
23868
  const gitignorePath = join13(root, ".gitignore");
22289
23869
  if (!existsSync11(gitignorePath))
22290
23870
  return [];
22291
23871
  try {
22292
- return readFileSync8(gitignorePath, "utf-8").split(`
23872
+ return readFileSync9(gitignorePath, "utf-8").split(`
22293
23873
  `).map((line) => line.trim()).filter((line) => line && !line.startsWith("#") && !line.startsWith("!"));
22294
23874
  } catch {
22295
23875
  return [];
@@ -22420,7 +24000,7 @@ function collectFiles(basePath, extensions, excludes, respectGitignore) {
22420
24000
  return files.sort();
22421
24001
  }
22422
24002
  function buildCodebaseIndex(options) {
22423
- const basePath = resolve9(options.path);
24003
+ const basePath = resolve10(options.path);
22424
24004
  const tags = options.patterns || [...EXTRACT_TAGS];
22425
24005
  const extensions = options.extensions ? new Set(options.extensions.map((e) => e.startsWith(".") ? e : `.${e}`)) : DEFAULT_EXTENSIONS;
22426
24006
  const excludes = options.exclude || [];
@@ -22430,8 +24010,8 @@ function buildCodebaseIndex(options) {
22430
24010
  for (const file of files) {
22431
24011
  const fullPath = statSync4(basePath).isFile() ? basePath : join13(basePath, file);
22432
24012
  try {
22433
- const source9 = readFileSync8(fullPath, "utf-8");
22434
- const relPath = statSync4(basePath).isFile() ? relative3(resolve9(basePath, ".."), fullPath) : file;
24013
+ const source9 = readFileSync9(fullPath, "utf-8");
24014
+ const relPath = statSync4(basePath).isFile() ? relative3(resolve10(basePath, ".."), fullPath) : file;
22435
24015
  indexed.push({
22436
24016
  file: relPath,
22437
24017
  checksum: stableHash(source9).slice(0, 24),
@@ -22451,7 +24031,7 @@ function buildCodebaseIndex(options) {
22451
24031
  };
22452
24032
  }
22453
24033
  function extractTodos(options, db) {
22454
- const basePath = resolve9(options.path);
24034
+ const basePath = resolve10(options.path);
22455
24035
  const tags = options.patterns || [...EXTRACT_TAGS];
22456
24036
  const extensions = options.extensions ? new Set(options.extensions.map((e) => e.startsWith(".") ? e : `.${e}`)) : DEFAULT_EXTENSIONS;
22457
24037
  const excludes = options.exclude || [];
@@ -22461,8 +24041,8 @@ function extractTodos(options, db) {
22461
24041
  for (const file of files) {
22462
24042
  const fullPath = statSync4(basePath).isFile() ? basePath : join13(basePath, file);
22463
24043
  try {
22464
- const source9 = readFileSync8(fullPath, "utf-8");
22465
- const relPath = statSync4(basePath).isFile() ? relative3(resolve9(basePath, ".."), fullPath) : file;
24044
+ const source9 = readFileSync9(fullPath, "utf-8");
24045
+ const relPath = statSync4(basePath).isFile() ? relative3(resolve10(basePath, ".."), fullPath) : file;
22466
24046
  const comments = extractFromSource(source9, relPath, tags);
22467
24047
  allComments.push(...comments);
22468
24048
  } catch {}
@@ -22556,7 +24136,7 @@ async function watchSourceTodos(options, onRun) {
22556
24136
  const interval = Math.max(100, options.interval_ms || 2000);
22557
24137
  const once = options.once !== false && (!options.max_runs || options.max_runs <= 1);
22558
24138
  const maxRuns = options.max_runs ?? (once ? 1 : Number.POSITIVE_INFINITY);
22559
- const root = resolve9(options.path);
24139
+ const root = resolve10(options.path);
22560
24140
  const runs = [];
22561
24141
  let previous = new Map;
22562
24142
  for (let runNumber = 1;runNumber <= maxRuns; runNumber++) {
@@ -22949,10 +24529,10 @@ async function runNextAgentDispatch(input = {}, db) {
22949
24529
  return { run_id: next.run.id, task_id: next.run.task_id, command, dry_run: false, status, exit_code: exitCode, output_summary: outputSummary };
22950
24530
  }
22951
24531
  // src/lib/policy-packs.ts
22952
- import { relative as relative4, resolve as resolve10 } from "path";
24532
+ import { relative as relative4, resolve as resolve11 } from "path";
22953
24533
  init_database();
22954
24534
  function normalizePath3(path) {
22955
- return resolve10(path);
24535
+ return resolve11(path);
22956
24536
  }
22957
24537
  function unique4(values) {
22958
24538
  return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
@@ -23007,7 +24587,7 @@ function commandMatches(commands, pattern) {
23007
24587
  }
23008
24588
  function pathMatches(paths, pattern, root) {
23009
24589
  return paths.filter((path) => {
23010
- const candidate = path.startsWith("/") ? path : resolve10(root, path);
24590
+ const candidate = path.startsWith("/") ? path : resolve11(root, path);
23011
24591
  if (!isPathInside3(root, candidate))
23012
24592
  return matchesPattern3(path, pattern);
23013
24593
  return matchesPattern3(path, pattern) || matchesPattern3(relative4(root, candidate), pattern);
@@ -23825,8 +25405,8 @@ ${line}`,
23825
25405
  };
23826
25406
  }
23827
25407
  // src/lib/mention-resolver.ts
23828
- import { existsSync as existsSync12, readdirSync as readdirSync3, readFileSync as readFileSync9, statSync as statSync5 } from "fs";
23829
- import { basename as basename2, isAbsolute, join as join14, relative as relative5, resolve as resolve11, sep } from "path";
25408
+ import { existsSync as existsSync12, readdirSync as readdirSync3, readFileSync as readFileSync10, statSync as statSync5 } from "fs";
25409
+ import { basename as basename2, isAbsolute, join as join14, relative as relative5, resolve as resolve12, sep } from "path";
23830
25410
  init_database();
23831
25411
  var PREFIXES = {
23832
25412
  file: "file",
@@ -23902,7 +25482,7 @@ function backlink(kind, key, label, target = key) {
23902
25482
  return { kind, key, label, target };
23903
25483
  }
23904
25484
  function normalizeWorkspace(workspace) {
23905
- return resolve11(workspace || process.cwd());
25485
+ return resolve12(workspace || process.cwd());
23906
25486
  }
23907
25487
  function isInside(root, absolutePath) {
23908
25488
  const rel = relative5(root, absolutePath);
@@ -23970,7 +25550,7 @@ function resolveFile(parsed, workspace) {
23970
25550
  resolution.warnings.push("path is empty or escapes the workspace");
23971
25551
  return resolution;
23972
25552
  }
23973
- const absolutePath = resolve11(workspace, relPath);
25553
+ const absolutePath = resolve12(workspace, relPath);
23974
25554
  if (!isInside(workspace, absolutePath)) {
23975
25555
  resolution.path = relPath;
23976
25556
  resolution.warnings.push("path escapes the workspace");
@@ -23987,7 +25567,7 @@ function resolveFile(parsed, workspace) {
23987
25567
  return resolution;
23988
25568
  }
23989
25569
  if (parsed.line !== undefined) {
23990
- const lineCount = readFileSync9(absolutePath, "utf-8").split(/\r?\n/).length;
25570
+ const lineCount = readFileSync10(absolutePath, "utf-8").split(/\r?\n/).length;
23991
25571
  if (parsed.line < 1 || parsed.line > lineCount) {
23992
25572
  resolution.warnings.push(`line ${parsed.line} is outside the file range 1-${lineCount}`);
23993
25573
  return resolution;
@@ -24040,7 +25620,7 @@ function resolveSymbol(parsed, workspace, maxMatches) {
24040
25620
  const pattern = symbolPattern(name);
24041
25621
  const matches = [];
24042
25622
  for (const file of walkSourceFiles(workspace)) {
24043
- const lines = readFileSync9(file, "utf-8").split(/\r?\n/);
25623
+ const lines = readFileSync10(file, "utf-8").split(/\r?\n/);
24044
25624
  for (let index = 0;index < lines.length; index += 1) {
24045
25625
  const line = lines[index];
24046
25626
  const found = pattern.exec(line);
@@ -24253,24 +25833,225 @@ function resolveMentions(input, db) {
24253
25833
  warnings
24254
25834
  };
24255
25835
  }
24256
- // src/lib/verification-providers.ts
24257
- import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
25836
+ // src/lib/workflow-states.ts
25837
+ init_types();
24258
25838
  init_database();
24259
- var DEFAULT_RETRY = {
24260
- attempts: 1,
24261
- backoff_ms: 0
24262
- };
24263
- var DEFAULT_CAPABILITIES = {
24264
- command: ["command", "retry", "evidence"],
24265
- testbox: ["testbox", "command", "retry", "evidence"],
24266
- ci_log: ["ci_log", "log_import", "evidence"],
24267
- browser: ["browser", "screenshot", "artifact", "evidence"],
24268
- script: ["script", "command", "retry", "evidence"]
24269
- };
24270
- function normalizeName2(name) {
24271
- const normalized = name.trim().toLowerCase();
24272
- if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(normalized)) {
24273
- throw new Error("verification provider name must use lowercase letters, numbers, dashes, or underscores");
25839
+ var LOCAL_FIELDS_KEY2 = "local_fields";
25840
+ var WORKFLOW_STATE_KEY = "workflow_state";
25841
+ var DEFAULT_WORKFLOW_STATES = [
25842
+ { name: "pending", canonical_status: "pending", aliases: ["todo", "backlog"], transitions: null, terminal: false },
25843
+ { name: "in_progress", canonical_status: "in_progress", aliases: ["doing", "started"], transitions: null, terminal: false },
25844
+ { name: "completed", canonical_status: "completed", aliases: ["done", "complete"], transitions: null, terminal: true },
25845
+ { name: "failed", canonical_status: "failed", aliases: [], transitions: null, terminal: true },
25846
+ { name: "cancelled", canonical_status: "cancelled", aliases: ["canceled"], transitions: null, terminal: true }
25847
+ ];
25848
+ function normalizeName2(value) {
25849
+ return value.trim().toLowerCase().replace(/[\s-]+/g, "_");
25850
+ }
25851
+ function isTaskStatus(value) {
25852
+ return TASK_STATUSES.includes(value);
25853
+ }
25854
+ function normalizeState(input) {
25855
+ const name = normalizeName2(input.name || "");
25856
+ if (!name || !isTaskStatus(input.canonical_status))
25857
+ return null;
25858
+ const aliases = [...new Set((input.aliases || []).map(normalizeName2).filter((alias) => alias && alias !== name))].sort();
25859
+ const transitions = input.transitions ? [...new Set(input.transitions.map(normalizeName2).filter(Boolean))].sort() : null;
25860
+ return {
25861
+ name,
25862
+ canonical_status: input.canonical_status,
25863
+ aliases,
25864
+ description: input.description,
25865
+ transitions,
25866
+ terminal: input.terminal ?? (input.canonical_status === "completed" || input.canonical_status === "failed" || input.canonical_status === "cancelled"),
25867
+ color: input.color
25868
+ };
25869
+ }
25870
+ function mergeWorkflowConfig(config) {
25871
+ const states = new Map(DEFAULT_WORKFLOW_STATES.map((state) => [state.name, state]));
25872
+ for (const input of config?.states || []) {
25873
+ const state = normalizeState(input);
25874
+ if (state)
25875
+ states.set(state.name, state);
25876
+ }
25877
+ return [...states.values()];
25878
+ }
25879
+ function getWorkflowConfig(projectPath) {
25880
+ const config = loadConfig();
25881
+ return projectPath && config.project_overrides?.[projectPath]?.workflow_states ? config.project_overrides[projectPath].workflow_states : config.workflow_states;
25882
+ }
25883
+ function listWorkflowStates(projectPath) {
25884
+ const workflowConfig = getWorkflowConfig(projectPath);
25885
+ return mergeWorkflowConfig(workflowConfig);
25886
+ }
25887
+ function resolveWorkflowState(input, projectPath) {
25888
+ const normalized = normalizeName2(input);
25889
+ const states = listWorkflowStates(projectPath);
25890
+ const byName = states.find((state) => state.name === normalized);
25891
+ if (byName)
25892
+ return { input, state: byName, matched_by: "name" };
25893
+ const byAlias = states.find((state) => state.aliases.includes(normalized));
25894
+ if (byAlias)
25895
+ return { input, state: byAlias, matched_by: "alias" };
25896
+ const byCanonical = states.find((state) => state.canonical_status === normalized);
25897
+ if (byCanonical)
25898
+ return { input, state: byCanonical, matched_by: "canonical_status" };
25899
+ throw new Error(`Unknown workflow state: ${input}`);
25900
+ }
25901
+ function stateForCanonicalStatus(status, projectPath) {
25902
+ const configured = (getWorkflowConfig(projectPath)?.states || []).map(normalizeState).find((state) => Boolean(state && state.canonical_status === status));
25903
+ if (configured)
25904
+ return configured;
25905
+ return listWorkflowStates(projectPath).find((state) => state.canonical_status === status) || resolveWorkflowState(status, projectPath).state;
25906
+ }
25907
+ function getTaskWorkflowState(taskId, db, projectPath) {
25908
+ const d = db || getDatabase();
25909
+ const task2 = getTask(taskId, d);
25910
+ if (!task2)
25911
+ throw new TaskNotFoundError(taskId);
25912
+ const fields = getTaskLocalFields(taskId, d);
25913
+ const stored = fields.custom[WORKFLOW_STATE_KEY];
25914
+ if (typeof stored === "string") {
25915
+ try {
25916
+ const state = resolveWorkflowState(stored, projectPath).state;
25917
+ if (state.canonical_status === task2.status)
25918
+ return state;
25919
+ } catch {}
25920
+ }
25921
+ return stateForCanonicalStatus(task2.status, projectPath);
25922
+ }
25923
+ function assertAllowedTransition(from, to, force) {
25924
+ if (force || from.name === to.name)
25925
+ return;
25926
+ if (from.transitions === null)
25927
+ return;
25928
+ if (from.transitions.includes(to.name))
25929
+ return;
25930
+ throw new Error(`Cannot transition workflow state from ${from.name} to ${to.name}`);
25931
+ }
25932
+ function metadataWithWorkflowState(task2, stateName, db) {
25933
+ const fields = getTaskLocalFields(task2.id, db);
25934
+ return {
25935
+ ...task2.metadata,
25936
+ [LOCAL_FIELDS_KEY2]: {
25937
+ ...fields,
25938
+ custom: {
25939
+ ...fields.custom,
25940
+ [WORKFLOW_STATE_KEY]: stateName
25941
+ }
25942
+ }
25943
+ };
25944
+ }
25945
+ function setTaskWorkflowState(taskId, stateInput, options = {}, db) {
25946
+ const d = db || getDatabase();
25947
+ const target = resolveWorkflowState(stateInput, options.project_path).state;
25948
+ for (let attempt = 0;attempt < 3; attempt++) {
25949
+ const task2 = getTask(taskId, d);
25950
+ if (!task2)
25951
+ throw new TaskNotFoundError(taskId);
25952
+ const previous = getTaskWorkflowState(task2.id, d, options.project_path);
25953
+ assertAllowedTransition(previous, target, options.force);
25954
+ const metadata = metadataWithWorkflowState(task2, target.name, d);
25955
+ try {
25956
+ const updated = updateTask(task2.id, {
25957
+ version: task2.version,
25958
+ status: target.canonical_status,
25959
+ metadata: {
25960
+ ...metadata,
25961
+ workflow_state_updated_by: options.actor || null
25962
+ }
25963
+ }, d);
25964
+ return {
25965
+ task: updated,
25966
+ workflow_state: target,
25967
+ previous_workflow_state: previous,
25968
+ changed: previous.name !== target.name,
25969
+ canonical_status_changed: task2.status !== target.canonical_status,
25970
+ local_only: true
25971
+ };
25972
+ } catch (error) {
25973
+ if (error instanceof VersionConflictError && attempt < 2)
25974
+ continue;
25975
+ throw error;
25976
+ }
25977
+ }
25978
+ throw new Error("Failed to set workflow state after 3 attempts");
25979
+ }
25980
+ function queryTasksByWorkflowState(query, db) {
25981
+ const d = db || getDatabase();
25982
+ const target = resolveWorkflowState(query.state, query.project_path).state;
25983
+ const tasks = listTasks({
25984
+ project_id: query.project_id,
25985
+ task_list_id: query.task_list_id,
25986
+ status: target.canonical_status,
25987
+ limit: 1e4
25988
+ }, d).filter((task2) => getTaskWorkflowState(task2.id, d, query.project_path).name === target.name).slice(0, query.limit || 100);
25989
+ return { state: target, tasks, count: tasks.length, local_only: true };
25990
+ }
25991
+ function migrateWorkflowStates(options = {}, db) {
25992
+ const d = db || getDatabase();
25993
+ const tasks = listTasks({
25994
+ project_id: options.project_id,
25995
+ task_list_id: options.task_list_id,
25996
+ limit: options.limit || 1e4,
25997
+ include_archived: true
25998
+ }, d);
25999
+ const items = [];
26000
+ let migrated = 0;
26001
+ for (const task2 of tasks) {
26002
+ const target = stateForCanonicalStatus(task2.status, options.project_path);
26003
+ const current = getTaskWorkflowState(task2.id, d, options.project_path);
26004
+ const changed = current.name !== target.name || getTaskLocalFields(task2.id, d).custom[WORKFLOW_STATE_KEY] !== target.name;
26005
+ items.push({
26006
+ task_id: task2.id,
26007
+ short_id: task2.short_id,
26008
+ title: task2.title,
26009
+ canonical_status: task2.status,
26010
+ workflow_state: target.name,
26011
+ changed
26012
+ });
26013
+ if (options.apply && changed) {
26014
+ setTaskWorkflowState(task2.id, target.name, { project_path: options.project_path, force: true }, d);
26015
+ migrated += 1;
26016
+ }
26017
+ }
26018
+ return {
26019
+ applied: Boolean(options.apply),
26020
+ migrated_count: migrated,
26021
+ pending_count: options.apply ? 0 : items.filter((item) => item.changed).length,
26022
+ skipped_count: items.filter((item) => !item.changed).length,
26023
+ items,
26024
+ local_only: true
26025
+ };
26026
+ }
26027
+ function renderWorkflowStatesMarkdown(states = listWorkflowStates()) {
26028
+ return [
26029
+ "# Workflow States",
26030
+ "",
26031
+ "| State | Canonical status | Aliases | Transitions | Terminal |",
26032
+ "| --- | --- | --- | --- | --- |",
26033
+ ...states.map((state) => `| ${state.name} | ${state.canonical_status} | ${state.aliases.join(", ") || "-"} | ${state.transitions?.join(", ") || "any"} | ${state.terminal ? "yes" : "no"} |`)
26034
+ ].join(`
26035
+ `);
26036
+ }
26037
+ // src/lib/verification-providers.ts
26038
+ import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
26039
+ init_database();
26040
+ var DEFAULT_RETRY = {
26041
+ attempts: 1,
26042
+ backoff_ms: 0
26043
+ };
26044
+ var DEFAULT_CAPABILITIES = {
26045
+ command: ["command", "retry", "evidence"],
26046
+ testbox: ["testbox", "command", "retry", "evidence"],
26047
+ ci_log: ["ci_log", "log_import", "evidence"],
26048
+ browser: ["browser", "screenshot", "artifact", "evidence"],
26049
+ script: ["script", "command", "retry", "evidence"]
26050
+ };
26051
+ function normalizeName3(name) {
26052
+ const normalized = name.trim().toLowerCase();
26053
+ if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(normalized)) {
26054
+ throw new Error("verification provider name must use lowercase letters, numbers, dashes, or underscores");
24274
26055
  }
24275
26056
  return normalized;
24276
26057
  }
@@ -24286,10 +26067,10 @@ function timeoutMs(value) {
24286
26067
  return Math.max(1, Math.min(24 * 60 * 60000, Math.floor(value)));
24287
26068
  }
24288
26069
  function getProvider(name) {
24289
- return loadConfig().verification_providers?.[normalizeName2(name)] || null;
26070
+ return loadConfig().verification_providers?.[normalizeName3(name)] || null;
24290
26071
  }
24291
26072
  function upsertVerificationProvider(input) {
24292
- const name = normalizeName2(input.name);
26073
+ const name = normalizeName3(input.name);
24293
26074
  const config = loadConfig();
24294
26075
  const existing = config.verification_providers?.[name];
24295
26076
  const timestamp2 = new Date().toISOString();
@@ -24319,7 +26100,7 @@ function listVerificationProviders() {
24319
26100
  return Object.values(loadConfig().verification_providers || {}).sort((a, b) => a.name.localeCompare(b.name));
24320
26101
  }
24321
26102
  function removeVerificationProvider(name) {
24322
- const normalized = normalizeName2(name);
26103
+ const normalized = normalizeName3(name);
24323
26104
  const config = loadConfig();
24324
26105
  if (!config.verification_providers?.[normalized])
24325
26106
  return false;
@@ -24363,7 +26144,7 @@ function classifyLog(text) {
24363
26144
  async function sleep3(ms) {
24364
26145
  if (ms <= 0)
24365
26146
  return;
24366
- await new Promise((resolve12) => setTimeout(resolve12, ms));
26147
+ await new Promise((resolve13) => setTimeout(resolve13, ms));
24367
26148
  }
24368
26149
  async function runCommandProvider(provider, input) {
24369
26150
  const commandTemplate = input.command || provider.command;
@@ -24418,7 +26199,7 @@ Timed out after ${provider.timeout_ms}ms`);
24418
26199
  };
24419
26200
  }
24420
26201
  function runCiLogProvider(input) {
24421
- const text = input.log_text ?? (input.log_path && existsSync13(input.log_path) ? readFileSync10(input.log_path, "utf-8") : "");
26202
+ const text = input.log_text ?? (input.log_path && existsSync13(input.log_path) ? readFileSync11(input.log_path, "utf-8") : "");
24422
26203
  return {
24423
26204
  status: classifyLog(text),
24424
26205
  attempts: 1,
@@ -24656,8 +26437,8 @@ function renderReleaseNotesMarkdown(document) {
24656
26437
  `;
24657
26438
  }
24658
26439
  // src/lib/agent-replay-simulator.ts
24659
- import { createHash as createHash9 } from "crypto";
24660
- import { readFileSync as readFileSync11 } from "fs";
26440
+ import { createHash as createHash10 } from "crypto";
26441
+ import { readFileSync as readFileSync12 } from "fs";
24661
26442
  function isObject(value) {
24662
26443
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
24663
26444
  }
@@ -24678,7 +26459,7 @@ function stable2(value) {
24678
26459
  return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable2(value[key])]));
24679
26460
  }
24680
26461
  function fingerprint2(value) {
24681
- return createHash9("sha256").update(JSON.stringify(stable2(value))).digest("hex");
26462
+ return createHash10("sha256").update(JSON.stringify(stable2(value))).digest("hex");
24682
26463
  }
24683
26464
  function unpackFixture(input) {
24684
26465
  if (!isObject(input))
@@ -24890,7 +26671,7 @@ function simulateAgentReplay(input, options = {}) {
24890
26671
  };
24891
26672
  }
24892
26673
  function simulateAgentReplayFile(path, options = {}) {
24893
- const parsed = JSON.parse(readFileSync11(path, "utf8"));
26674
+ const parsed = JSON.parse(readFileSync12(path, "utf8"));
24894
26675
  return simulateAgentReplay(parsed, options);
24895
26676
  }
24896
26677
  function renderAgentReplaySimulationMarkdown(simulation) {
@@ -24916,13 +26697,13 @@ function renderAgentReplaySimulationMarkdown(simulation) {
24916
26697
  `);
24917
26698
  }
24918
26699
  // src/lib/local-extensions.ts
24919
- import { createHash as createHash10, createVerify } from "crypto";
24920
- import { existsSync as existsSync14, readFileSync as readFileSync12, statSync as statSync6 } from "fs";
24921
- import { basename as basename3, join as join15, resolve as resolve12 } from "path";
26700
+ import { createHash as createHash11, createVerify } from "crypto";
26701
+ import { existsSync as existsSync14, readdirSync as readdirSync4, readFileSync as readFileSync13, statSync as statSync6 } from "fs";
26702
+ import { basename as basename3, join as join15, resolve as resolve13 } from "path";
24922
26703
  function isObject2(value) {
24923
26704
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
24924
26705
  }
24925
- function normalizeName3(name) {
26706
+ function normalizeName4(name) {
24926
26707
  const normalized = name.trim().toLowerCase();
24927
26708
  if (!/^[a-z0-9][a-z0-9_.-]{0,63}$/.test(normalized)) {
24928
26709
  throw new Error("extension name must use lowercase letters, numbers, dots, dashes, or underscores");
@@ -24940,7 +26721,7 @@ function permissionList(value) {
24940
26721
  function normalizeManifest(input) {
24941
26722
  if (!isObject2(input))
24942
26723
  throw new Error("extension manifest must be a JSON object");
24943
- const name = normalizeName3(String(input["name"] || ""));
26724
+ const name = normalizeName4(String(input["name"] || ""));
24944
26725
  const version = String(input["version"] || "").trim();
24945
26726
  if (!version)
24946
26727
  throw new Error("extension manifest requires version");
@@ -24948,7 +26729,7 @@ function normalizeManifest(input) {
24948
26729
  todos: typeof input["compatibility"]["todos"] === "string" ? input["compatibility"]["todos"] : undefined
24949
26730
  } : undefined;
24950
26731
  const commands = Array.isArray(input["commands"]) ? input["commands"].filter(isObject2).map((command) => ({
24951
- name: normalizeName3(String(command["name"] || "")),
26732
+ name: normalizeName4(String(command["name"] || "")),
24952
26733
  command: typeof command["command"] === "string" ? command["command"] : undefined,
24953
26734
  description: typeof command["description"] === "string" ? command["description"] : undefined,
24954
26735
  permissions: permissionList(command["permissions"]),
@@ -24957,10 +26738,30 @@ function normalizeManifest(input) {
24957
26738
  network: typeof command["network"] === "boolean" ? command["network"] : undefined
24958
26739
  })) : [];
24959
26740
  const mcpTools = Array.isArray(input["mcp_tools"]) ? input["mcp_tools"].filter(isObject2).map((tool) => ({
24960
- name: normalizeName3(String(tool["name"] || "")),
26741
+ name: normalizeName4(String(tool["name"] || "")),
24961
26742
  description: typeof tool["description"] === "string" ? tool["description"] : undefined,
24962
26743
  permissions: permissionList(tool["permissions"])
24963
26744
  })) : [];
26745
+ const templates = Array.isArray(input["templates"]) ? input["templates"].filter(isObject2).map((template) => ({
26746
+ name: normalizeName4(String(template["name"] || "")),
26747
+ kind: typeof template["kind"] === "string" ? template["kind"] : undefined,
26748
+ description: typeof template["description"] === "string" ? template["description"] : undefined,
26749
+ path: typeof template["path"] === "string" ? template["path"] : undefined,
26750
+ content: typeof template["content"] === "string" ? template["content"] : undefined,
26751
+ variables: unique5(template["variables"]),
26752
+ permissions: permissionList(template["permissions"])
26753
+ })) : [];
26754
+ const renderers = Array.isArray(input["renderers"]) ? input["renderers"].filter(isObject2).map((renderer) => ({
26755
+ name: normalizeName4(String(renderer["name"] || "")),
26756
+ target: typeof renderer["target"] === "string" ? renderer["target"] : "",
26757
+ description: typeof renderer["description"] === "string" ? renderer["description"] : undefined,
26758
+ command: typeof renderer["command"] === "string" ? renderer["command"] : undefined,
26759
+ template: typeof renderer["template"] === "string" ? renderer["template"] : undefined,
26760
+ permissions: permissionList(renderer["permissions"]),
26761
+ write_paths: unique5(renderer["write_paths"]),
26762
+ env: unique5(renderer["env"]),
26763
+ network: typeof renderer["network"] === "boolean" ? renderer["network"] : undefined
26764
+ })) : [];
24964
26765
  return {
24965
26766
  schema_version: typeof input["schema_version"] === "number" ? input["schema_version"] : 1,
24966
26767
  name,
@@ -24970,6 +26771,8 @@ function normalizeManifest(input) {
24970
26771
  permissions: unique5(input["permissions"]),
24971
26772
  commands,
24972
26773
  mcp_tools: mcpTools,
26774
+ templates,
26775
+ renderers,
24973
26776
  hooks: unique5(input["hooks"]),
24974
26777
  checksum: typeof input["checksum"] === "string" ? input["checksum"] : undefined,
24975
26778
  signature: typeof input["signature"] === "string" ? input["signature"] : undefined,
@@ -24977,10 +26780,10 @@ function normalizeManifest(input) {
24977
26780
  };
24978
26781
  }
24979
26782
  function parseJson(path) {
24980
- return JSON.parse(readFileSync12(path, "utf8"));
26783
+ return JSON.parse(readFileSync13(path, "utf8"));
24981
26784
  }
24982
- function sha2564(bytes) {
24983
- return `sha256:${createHash10("sha256").update(bytes).digest("hex")}`;
26785
+ function sha2565(bytes) {
26786
+ return `sha256:${createHash11("sha256").update(bytes).digest("hex")}`;
24984
26787
  }
24985
26788
  function compareVersions(a, b) {
24986
26789
  const left = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
@@ -25053,7 +26856,7 @@ function duplicateChecks(surface, names) {
25053
26856
  const counts = new Map;
25054
26857
  for (const name of names)
25055
26858
  counts.set(name, (counts.get(name) || 0) + 1);
25056
- return [...counts.entries()].filter(([, count]) => count > 1).map(([name]) => ({
26859
+ return [...counts.entries()].filter(([, count2]) => count2 > 1).map(([name]) => ({
25057
26860
  surface,
25058
26861
  name,
25059
26862
  ok: false,
@@ -25065,9 +26868,13 @@ function compatibilityChecks(manifest) {
25065
26868
  const checks = [];
25066
26869
  const commands = manifest.commands || [];
25067
26870
  const tools = manifest.mcp_tools || [];
26871
+ const templates = manifest.templates || [];
26872
+ const renderers = manifest.renderers || [];
25068
26873
  const hooks = manifest.hooks || [];
25069
26874
  checks.push(...duplicateChecks("cli", commands.map((command) => command.name)));
25070
26875
  checks.push(...duplicateChecks("mcp", tools.map((tool) => tool.name)));
26876
+ checks.push(...duplicateChecks("template", templates.map((template) => template.name)));
26877
+ checks.push(...duplicateChecks("renderer", renderers.map((renderer) => renderer.name)));
25071
26878
  checks.push(...duplicateChecks("hook", hooks));
25072
26879
  for (const command of commands) {
25073
26880
  checks.push({
@@ -25088,16 +26895,43 @@ function compatibilityChecks(manifest) {
25088
26895
  warnings: []
25089
26896
  });
25090
26897
  }
26898
+ for (const template of templates) {
26899
+ checks.push({
26900
+ surface: "template",
26901
+ name: template.name,
26902
+ ok: Boolean(template.path || template.content),
26903
+ errors: template.path || template.content ? [] : [`template ${template.name} needs path or inline content`],
26904
+ warnings: []
26905
+ });
26906
+ }
26907
+ const templateNames = new Set(templates.map((template) => template.name));
26908
+ for (const renderer of renderers) {
26909
+ const missingTarget = !renderer.target;
26910
+ const missingImplementation = !renderer.command && !renderer.template;
26911
+ checks.push({
26912
+ surface: "renderer",
26913
+ name: renderer.name,
26914
+ ok: !missingTarget && !missingImplementation && (!renderer.template || templateNames.has(renderer.template)),
26915
+ errors: [
26916
+ ...missingTarget ? [`renderer ${renderer.name} needs a target`] : [],
26917
+ ...missingImplementation ? [`renderer ${renderer.name} needs command or template`] : [],
26918
+ ...renderer.template && !templateNames.has(renderer.template) ? [`renderer ${renderer.name} references unknown template ${renderer.template}`] : []
26919
+ ],
26920
+ warnings: renderer.command ? ["renderer command will be checked by the local runner sandbox"] : []
26921
+ });
26922
+ }
25091
26923
  const permissions = [
25092
26924
  ...manifest.permissions || [],
25093
26925
  ...commands.flatMap((command) => command.permissions || []),
25094
- ...tools.flatMap((tool) => tool.permissions || [])
26926
+ ...tools.flatMap((tool) => tool.permissions || []),
26927
+ ...templates.flatMap((template) => template.permissions || []),
26928
+ ...renderers.flatMap((renderer) => renderer.permissions || [])
25095
26929
  ];
25096
26930
  checks.push(...permissions.map(validatePermission));
25097
26931
  return checks;
25098
26932
  }
25099
26933
  function sandboxChecks(manifest, source9) {
25100
- return (manifest.commands || []).filter((command) => Boolean(command.command)).map((command) => {
26934
+ const commandChecks = (manifest.commands || []).filter((command) => Boolean(command.command)).map((command) => {
25101
26935
  const check = checkRunnerSandbox({
25102
26936
  path: source9,
25103
26937
  cwd: source9,
@@ -25115,6 +26949,25 @@ function sandboxChecks(manifest, source9) {
25115
26949
  audit_evidence: check.audit_evidence
25116
26950
  };
25117
26951
  });
26952
+ const rendererChecks = (manifest.renderers || []).filter((renderer) => Boolean(renderer.command)).map((renderer) => {
26953
+ const check = checkRunnerSandbox({
26954
+ path: source9,
26955
+ cwd: source9,
26956
+ command: renderer.command,
26957
+ write_paths: renderer.write_paths,
26958
+ env: Object.fromEntries((renderer.env || []).map((key) => [key, "declared"])),
26959
+ network: renderer.network
26960
+ });
26961
+ return {
26962
+ command_name: `renderer:${renderer.name}`,
26963
+ command: renderer.command,
26964
+ allowed: check.allowed,
26965
+ requires_approval: check.requires_approval,
26966
+ reasons: check.reasons,
26967
+ audit_evidence: check.audit_evidence
26968
+ };
26969
+ });
26970
+ return [...commandChecks, ...rendererChecks];
25118
26971
  }
25119
26972
  function verifyExtensionSignature(input) {
25120
26973
  if (!input.signature || !input.public_key)
@@ -25125,19 +26978,19 @@ function verifyExtensionSignature(input) {
25125
26978
  return verifier.verify(input.public_key, decodeSignature(input.signature));
25126
26979
  }
25127
26980
  function inspectExtensionSource(source9) {
25128
- const resolved = resolve12(source9);
26981
+ const resolved = resolve13(source9);
25129
26982
  if (!existsSync14(resolved))
25130
26983
  throw new Error(`extension source not found: ${source9}`);
25131
26984
  const stat = statSync6(resolved);
25132
26985
  const manifestPath = stat.isDirectory() ? [join15(resolved, "todos.extension.json"), join15(resolved, "extension.json")].find(existsSync14) : resolved;
25133
26986
  if (!manifestPath)
25134
26987
  throw new Error(`extension directory ${source9} is missing todos.extension.json`);
25135
- const raw = readFileSync12(manifestPath);
26988
+ const raw = readFileSync13(manifestPath);
25136
26989
  const parsed = parseJson(manifestPath);
25137
26990
  const bundle = isObject2(parsed) && isObject2(parsed["manifest"]);
25138
26991
  const manifest = normalizeManifest(bundle ? parsed["manifest"] : parsed);
25139
26992
  const sourceType = stat.isDirectory() ? "directory" : bundle ? "bundle" : "manifest";
25140
- const checksum = sha2564(raw);
26993
+ const checksum = sha2565(raw);
25141
26994
  return {
25142
26995
  source: resolved,
25143
26996
  source_type: sourceType,
@@ -25165,8 +27018,8 @@ function validateExtensionManifest(manifest) {
25165
27018
  errors.push(`extension requires @hasna/todos ${compatibilityRange}, current version is ${todosVersion}`);
25166
27019
  if ((candidate.permissions || []).length === 0)
25167
27020
  warnings.push("extension declares no permissions");
25168
- if ((candidate.commands || []).length === 0 && (candidate.mcp_tools || []).length === 0 && (candidate.hooks || []).length === 0) {
25169
- warnings.push("extension declares no commands, MCP tools, or hooks");
27021
+ if ((candidate.commands || []).length === 0 && (candidate.mcp_tools || []).length === 0 && (candidate.templates || []).length === 0 && (candidate.renderers || []).length === 0 && (candidate.hooks || []).length === 0) {
27022
+ warnings.push("extension declares no commands, MCP tools, templates, renderers, or hooks");
25170
27023
  }
25171
27024
  const cliMcpChecks = compatibilityChecks(candidate);
25172
27025
  for (const check of cliMcpChecks) {
@@ -25209,6 +27062,8 @@ function testExtensionCompatibility(sourceOrManifest) {
25209
27062
  summary: {
25210
27063
  commands: manifest.commands?.length || 0,
25211
27064
  mcp_tools: manifest.mcp_tools?.length || 0,
27065
+ templates: manifest.templates?.length || 0,
27066
+ renderers: manifest.renderers?.length || 0,
25212
27067
  hooks: manifest.hooks?.length || 0,
25213
27068
  permissions: validation.permission_declarations.length,
25214
27069
  sandbox_checks: validation.sandbox_checks.length,
@@ -25218,6 +27073,57 @@ function testExtensionCompatibility(sourceOrManifest) {
25218
27073
  warnings
25219
27074
  };
25220
27075
  }
27076
+ function projectExtensionSources(projectPath) {
27077
+ if (!projectPath)
27078
+ return [];
27079
+ const root = resolve13(projectPath);
27080
+ const candidates = [
27081
+ join15(root, "todos.extension.json"),
27082
+ join15(root, ".todos", "todos.extension.json")
27083
+ ];
27084
+ const extensionDir = join15(root, ".todos", "extensions");
27085
+ if (existsSync14(extensionDir)) {
27086
+ for (const entry of readdirSync4(extensionDir)) {
27087
+ if (entry.startsWith("."))
27088
+ continue;
27089
+ const full = join15(extensionDir, entry);
27090
+ if (statSync6(full).isDirectory() || entry.endsWith(".json"))
27091
+ candidates.push(full);
27092
+ }
27093
+ }
27094
+ return candidates.filter(existsSync14);
27095
+ }
27096
+ function discoverLocalExtensions(options = {}) {
27097
+ const config = loadConfig();
27098
+ const projectPath = options.project_path ? resolve13(options.project_path) : null;
27099
+ const configuredSources = [
27100
+ ...config.extension_sources || [],
27101
+ ...projectPath ? config.project_overrides?.[projectPath]?.extension_sources || [] : []
27102
+ ];
27103
+ const sources = Array.from(new Set([
27104
+ ...configuredSources,
27105
+ ...projectExtensionSources(projectPath || undefined)
27106
+ ])).map((source9) => projectPath && !source9.startsWith("/") ? resolve13(projectPath, source9) : resolve13(source9));
27107
+ const warnings = [];
27108
+ const discovered = [];
27109
+ for (const source9 of sources) {
27110
+ try {
27111
+ discovered.push(inspectExtensionSource(source9));
27112
+ } catch (error) {
27113
+ warnings.push(`${source9}: ${error instanceof Error ? error.message : String(error)}`);
27114
+ }
27115
+ }
27116
+ return {
27117
+ schema_version: 1,
27118
+ local_only: true,
27119
+ no_network: true,
27120
+ project_path: projectPath,
27121
+ config_sources: sources,
27122
+ discovered,
27123
+ installed: options.include_installed === false ? [] : listLocalExtensions(),
27124
+ warnings
27125
+ };
27126
+ }
25221
27127
  function installLocalExtension(input) {
25222
27128
  const inspected = inspectExtensionSource(input.source);
25223
27129
  const expectedChecksum = input.checksum || inspected.manifest.checksum;
@@ -25266,10 +27172,10 @@ function listLocalExtensions() {
25266
27172
  return Object.values(loadConfig().extension_registry || {}).sort((a, b) => a.name.localeCompare(b.name));
25267
27173
  }
25268
27174
  function getLocalExtension(name) {
25269
- return loadConfig().extension_registry?.[normalizeName3(name)] || null;
27175
+ return loadConfig().extension_registry?.[normalizeName4(name)] || null;
25270
27176
  }
25271
27177
  function removeLocalExtension(name) {
25272
- const normalized = normalizeName3(name);
27178
+ const normalized = normalizeName4(name);
25273
27179
  const config = loadConfig();
25274
27180
  if (!config.extension_registry?.[normalized])
25275
27181
  return false;
@@ -25599,14 +27505,14 @@ async function executeDispatch(dispatch, opts = {}, db) {
25599
27505
  async function runDueDispatches(opts = {}, db) {
25600
27506
  const _db2 = db ?? getDatabase();
25601
27507
  const due = getDueDispatches(_db2);
25602
- let count = 0;
27508
+ let count2 = 0;
25603
27509
  for (const dispatch of due) {
25604
27510
  try {
25605
27511
  await executeDispatch(dispatch, opts, _db2);
25606
- count++;
27512
+ count2++;
25607
27513
  } catch {}
25608
27514
  }
25609
- return count;
27515
+ return count2;
25610
27516
  }
25611
27517
  async function dispatchToMultiple(input, opts = {}, db) {
25612
27518
  const _db2 = db ?? getDatabase();
@@ -25849,6 +27755,227 @@ function applyRetentionCleanup(input, db) {
25849
27755
  }
25850
27756
  return report;
25851
27757
  }
27758
+ // src/lib/scale-hardening.ts
27759
+ init_database();
27760
+ var REQUIRED_INDEXES = [
27761
+ "idx_tasks_project",
27762
+ "idx_tasks_status",
27763
+ "idx_tasks_archived_at",
27764
+ "idx_tasks_updated_at",
27765
+ "idx_task_runs_task",
27766
+ "idx_task_run_events_run",
27767
+ "idx_comments_task",
27768
+ "idx_task_dependencies_task",
27769
+ "idx_task_dependencies_depends_on"
27770
+ ];
27771
+ function count2(db, sql) {
27772
+ const row = db.query(sql).get();
27773
+ return row?.count ?? 0;
27774
+ }
27775
+ function pragmaNumber(db, name) {
27776
+ const row = db.query(`PRAGMA ${name}`).get();
27777
+ const value = row ? Object.values(row)[0] : 0;
27778
+ return typeof value === "number" ? value : 0;
27779
+ }
27780
+ function quickCheck(db) {
27781
+ try {
27782
+ const row = db.query("PRAGMA quick_check").get();
27783
+ return row?.quick_check ?? "unknown";
27784
+ } catch (error) {
27785
+ return error instanceof Error ? error.message : String(error);
27786
+ }
27787
+ }
27788
+ function foreignKeyViolations(db) {
27789
+ try {
27790
+ return db.query("PRAGMA foreign_key_check").all().length;
27791
+ } catch {
27792
+ return 0;
27793
+ }
27794
+ }
27795
+ function missingIndexes(db) {
27796
+ const rows = db.query("SELECT name FROM sqlite_master WHERE type='index'").all();
27797
+ const existing = new Set(rows.map((row) => row.name));
27798
+ return REQUIRED_INDEXES.filter((index) => !existing.has(index));
27799
+ }
27800
+ function benchmark(name, thresholdMs, fn) {
27801
+ const start = performance.now();
27802
+ const rows = fn();
27803
+ const elapsed = Math.max(0, performance.now() - start);
27804
+ const elapsed_ms = Math.round(elapsed * 1000) / 1000;
27805
+ return {
27806
+ name,
27807
+ rows,
27808
+ elapsed_ms,
27809
+ threshold_ms: thresholdMs,
27810
+ ok: elapsed_ms <= thresholdMs
27811
+ };
27812
+ }
27813
+ function cutoffDate(days) {
27814
+ return new Date(Date.now() - days * 86400000).toISOString();
27815
+ }
27816
+ function normalizedPositiveDays(value) {
27817
+ if (value === undefined)
27818
+ return 30;
27819
+ if (!Number.isFinite(value))
27820
+ return 30;
27821
+ return Math.max(1, Math.trunc(value));
27822
+ }
27823
+ function formatNumber(value) {
27824
+ return new Intl.NumberFormat("en-US").format(value);
27825
+ }
27826
+ function formatMs(value) {
27827
+ return `${value.toFixed(value >= 10 ? 1 : 3)}ms`;
27828
+ }
27829
+ function createScalePerformanceReport(options = {}, db) {
27830
+ const d = db || getDatabase();
27831
+ const olderThanDays = normalizedPositiveDays(options.older_than_days);
27832
+ const pageCount = pragmaNumber(d, "page_count");
27833
+ const freelistCount = pragmaNumber(d, "freelist_count");
27834
+ const freelistRatio = pageCount > 0 ? Math.round(freelistCount / pageCount * 1000) / 1000 : 0;
27835
+ const quick = quickCheck(d);
27836
+ const fkViolations = foreignKeyViolations(d);
27837
+ const missing = missingIndexes(d);
27838
+ const oldCutoff = cutoffDate(olderThanDays);
27839
+ const terminalUnarchived = count2(d, "SELECT COUNT(*) AS count FROM tasks WHERE archived_at IS NULL AND status IN ('completed', 'failed', 'cancelled')");
27840
+ const oldTerminalUnarchived = count2(d, `SELECT COUNT(*) AS count FROM tasks WHERE archived_at IS NULL AND status IN ('completed', 'failed', 'cancelled') AND updated_at < '${oldCutoff}'`);
27841
+ const archivedCount = count2(d, "SELECT COUNT(*) AS count FROM tasks WHERE archived_at IS NOT NULL");
27842
+ const benchmarks = [
27843
+ benchmark("open_task_list", 50, () => listTasks({ status: ["pending", "in_progress"], limit: 100 }, d).length),
27844
+ benchmark("search_recent_tasks", 75, () => searchTasks({ query: "*", status: ["pending", "in_progress"] }, undefined, undefined, d).length),
27845
+ benchmark("archived_task_scan", 75, () => listTasks({ include_archived: true, limit: 100 }, d).length),
27846
+ benchmark("run_ledger_recent", 75, () => count2(d, "SELECT COUNT(*) AS count FROM (SELECT id FROM task_runs ORDER BY started_at DESC LIMIT 100)")),
27847
+ benchmark("dependency_blockers", 75, () => count2(d, "SELECT COUNT(*) AS count FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed'"))
27848
+ ];
27849
+ const warnings = [];
27850
+ if (freelistRatio >= 0.2 && freelistCount >= 100)
27851
+ warnings.push("database has enough free pages to consider VACUUM during a maintenance window");
27852
+ if (oldTerminalUnarchived > 0)
27853
+ warnings.push(`${oldTerminalUnarchived} old terminal tasks can be archived without deleting evidence`);
27854
+ if (missing.length > 0)
27855
+ warnings.push(`missing expected index(es): ${missing.join(", ")}`);
27856
+ if (fkViolations > 0)
27857
+ warnings.push(`${fkViolations} foreign key violation(s) detected`);
27858
+ for (const item of benchmarks) {
27859
+ if (!item.ok)
27860
+ warnings.push(`${item.name} exceeded ${item.threshold_ms}ms threshold`);
27861
+ }
27862
+ return {
27863
+ schema_version: 1,
27864
+ local_only: true,
27865
+ no_network: true,
27866
+ generated_at: options.generated_at ?? new Date().toISOString(),
27867
+ database_path: getDatabasePath(),
27868
+ counts: {
27869
+ tasks: count2(d, "SELECT COUNT(*) AS count FROM tasks"),
27870
+ archived_tasks: archivedCount,
27871
+ projects: count2(d, "SELECT COUNT(*) AS count FROM projects"),
27872
+ agents: count2(d, "SELECT COUNT(*) AS count FROM agents"),
27873
+ plans: count2(d, "SELECT COUNT(*) AS count FROM plans"),
27874
+ runs: count2(d, "SELECT COUNT(*) AS count FROM task_runs"),
27875
+ run_events: count2(d, "SELECT COUNT(*) AS count FROM task_run_events"),
27876
+ comments: count2(d, "SELECT COUNT(*) AS count FROM task_comments"),
27877
+ dependencies: count2(d, "SELECT COUNT(*) AS count FROM task_dependencies")
27878
+ },
27879
+ benchmarks,
27880
+ archive: {
27881
+ terminal_unarchived_tasks: terminalUnarchived,
27882
+ older_than_days: olderThanDays,
27883
+ older_terminal_unarchived_tasks: oldTerminalUnarchived,
27884
+ archived_tasks_visible_with_include_archived: archivedCount === 0 || listTasks({ include_archived: true, limit: 5000 }, d).some((task2) => Boolean(task2.archived_at))
27885
+ },
27886
+ compaction: {
27887
+ page_count: pageCount,
27888
+ freelist_count: freelistCount,
27889
+ freelist_ratio: freelistRatio,
27890
+ recommended: freelistRatio >= 0.2 && freelistCount >= 100,
27891
+ commands: ["PRAGMA optimize", "VACUUM"]
27892
+ },
27893
+ integrity: {
27894
+ quick_check: quick,
27895
+ foreign_key_violations: fkViolations,
27896
+ missing_indexes: missing,
27897
+ ok: quick === "ok" && fkViolations === 0 && missing.length === 0
27898
+ },
27899
+ warnings
27900
+ };
27901
+ }
27902
+ function compactScaleStorage(options = {}, db) {
27903
+ const d = db || getDatabase();
27904
+ const before = {
27905
+ page_count: pragmaNumber(d, "page_count"),
27906
+ freelist_count: pragmaNumber(d, "freelist_count")
27907
+ };
27908
+ const actions = ["PRAGMA optimize", "VACUUM"];
27909
+ if (options.apply) {
27910
+ d.run("PRAGMA optimize");
27911
+ d.run("VACUUM");
27912
+ }
27913
+ const after = {
27914
+ page_count: pragmaNumber(d, "page_count"),
27915
+ freelist_count: pragmaNumber(d, "freelist_count")
27916
+ };
27917
+ return {
27918
+ schema_version: 1,
27919
+ local_only: true,
27920
+ no_network: true,
27921
+ dry_run: !options.apply,
27922
+ database_path: getDatabasePath(),
27923
+ before,
27924
+ after,
27925
+ actions
27926
+ };
27927
+ }
27928
+ function renderScalePerformanceReportMarkdown(report) {
27929
+ const lines = [
27930
+ "# todos scale hardening report",
27931
+ "",
27932
+ `Generated: ${report.generated_at}`,
27933
+ `Database: ${report.database_path}`,
27934
+ `Local only: ${report.local_only ? "yes" : "no"}`,
27935
+ "",
27936
+ "## Counts",
27937
+ "",
27938
+ `- Tasks: ${formatNumber(report.counts.tasks)} (${formatNumber(report.counts.archived_tasks)} archived)`,
27939
+ `- Projects: ${formatNumber(report.counts.projects)}`,
27940
+ `- Agents: ${formatNumber(report.counts.agents)}`,
27941
+ `- Plans: ${formatNumber(report.counts.plans)}`,
27942
+ `- Runs: ${formatNumber(report.counts.runs)} with ${formatNumber(report.counts.run_events)} events`,
27943
+ `- Comments: ${formatNumber(report.counts.comments)}`,
27944
+ `- Dependencies: ${formatNumber(report.counts.dependencies)}`,
27945
+ "",
27946
+ "## Benchmarks",
27947
+ "",
27948
+ "| Query | Rows | Time | Threshold | Status |",
27949
+ "| --- | ---: | ---: | ---: | --- |",
27950
+ ...report.benchmarks.map((item) => `| ${item.name} | ${formatNumber(item.rows)} | ${formatMs(item.elapsed_ms)} | ${formatMs(item.threshold_ms)} | ${item.ok ? "ok" : "slow"} |`),
27951
+ "",
27952
+ "## Archive Readiness",
27953
+ "",
27954
+ `- Terminal unarchived tasks: ${formatNumber(report.archive.terminal_unarchived_tasks)}`,
27955
+ `- Older than ${report.archive.older_than_days} days: ${formatNumber(report.archive.older_terminal_unarchived_tasks)}`,
27956
+ `- Include-archived listing exposes archived tasks: ${report.archive.archived_tasks_visible_with_include_archived ? "yes" : "no"}`,
27957
+ "",
27958
+ "## Compaction",
27959
+ "",
27960
+ `- Pages: ${formatNumber(report.compaction.page_count)}`,
27961
+ `- Free pages: ${formatNumber(report.compaction.freelist_count)} (${Math.round(report.compaction.freelist_ratio * 100)}%)`,
27962
+ `- Maintenance recommended: ${report.compaction.recommended ? "yes" : "no"}`,
27963
+ `- Commands: ${report.compaction.commands.map((command) => `\`${command}\``).join(", ")}`,
27964
+ "",
27965
+ "## Integrity",
27966
+ "",
27967
+ `- Quick check: ${report.integrity.quick_check}`,
27968
+ `- Foreign key violations: ${formatNumber(report.integrity.foreign_key_violations)}`,
27969
+ `- Missing indexes: ${report.integrity.missing_indexes.length === 0 ? "none" : report.integrity.missing_indexes.join(", ")}`,
27970
+ `- Overall: ${report.integrity.ok ? "ok" : "needs attention"}`
27971
+ ];
27972
+ if (report.warnings.length > 0) {
27973
+ lines.push("", "## Warnings", "", ...report.warnings.map((warning) => `- ${warning}`));
27974
+ }
27975
+ return `${lines.join(`
27976
+ `)}
27977
+ `;
27978
+ }
25852
27979
  // src/lib/todos-md.ts
25853
27980
  init_database();
25854
27981
  var TODOS_MARKDOWN_SCHEMA = "hasna.todos.md/v1";
@@ -26048,9 +28175,9 @@ function importTodosMarkdown(markdown, options = {}, db) {
26048
28175
  inserted.projects = parsed.projectName ? 1 : 0;
26049
28176
  inserted.plans = parsed.plans.length;
26050
28177
  inserted.tasks = parsed.tasks.length;
26051
- inserted.comments = parsed.tasks.reduce((count, task2) => count + task2.comments.length, 0);
26052
- inserted.runs = parsed.tasks.reduce((count, task2) => count + task2.run_summaries.length, 0);
26053
- inserted.task_dependencies = parsed.tasks.reduce((count, task2) => count + task2.depends_on_titles.length, 0);
28178
+ inserted.comments = parsed.tasks.reduce((count3, task2) => count3 + task2.comments.length, 0);
28179
+ inserted.runs = parsed.tasks.reduce((count3, task2) => count3 + task2.run_summaries.length, 0);
28180
+ inserted.task_dependencies = parsed.tasks.reduce((count3, task2) => count3 + task2.depends_on_titles.length, 0);
26054
28181
  return { ok: issues.length === 0, dry_run: true, mode: "plain_markdown", inserted, merged: emptyCounts2(), skipped, conflicts: [], issues };
26055
28182
  }
26056
28183
  const project = parsed.projectName ? createProject({ name: parsed.projectName, path: `todos-md://${parsed.projectName}` }, d) : null;
@@ -26108,8 +28235,8 @@ function importTodosMarkdown(markdown, options = {}, db) {
26108
28235
  init_database();
26109
28236
  init_migrations();
26110
28237
  init_schema();
26111
- import { chmodSync, copyFileSync, existsSync as existsSync16, mkdirSync as mkdirSync9, statSync as statSync7 } from "fs";
26112
- import { basename as basename4, dirname as dirname7, join as join16 } from "path";
28238
+ import { chmodSync, copyFileSync, existsSync as existsSync16, mkdirSync as mkdirSync10, statSync as statSync7 } from "fs";
28239
+ import { basename as basename4, dirname as dirname8, join as join16 } from "path";
26113
28240
  var REQUIRED_TABLES2 = [
26114
28241
  "_migrations",
26115
28242
  "projects",
@@ -26137,7 +28264,7 @@ function getTableColumns(db, table) {
26137
28264
  return new Set;
26138
28265
  }
26139
28266
  }
26140
- function countQuery(db, sql) {
28267
+ function countQuery2(db, sql) {
26141
28268
  try {
26142
28269
  const row = db.query(sql).get();
26143
28270
  return row?.count ?? 0;
@@ -26227,7 +28354,7 @@ function addTaskStateChecks(db, checks) {
26227
28354
  const columns = getTableColumns(db, "tasks");
26228
28355
  const staleCutoff = new Date(Date.now() - 30 * 60 * 1000).toISOString();
26229
28356
  if (columns.has("updated_at") && columns.has("status")) {
26230
- const staleTasks = countQuery(db, `SELECT COUNT(*) as count FROM tasks WHERE status = 'in_progress' AND updated_at < '${staleCutoff}'`);
28357
+ const staleTasks = countQuery2(db, `SELECT COUNT(*) as count FROM tasks WHERE status = 'in_progress' AND updated_at < '${staleCutoff}'`);
26231
28358
  if (staleTasks > 0) {
26232
28359
  addCheck(checks, {
26233
28360
  severity: "warn",
@@ -26279,9 +28406,9 @@ function createBackup(dbPath) {
26279
28406
  if (!existsSync16(dbPath))
26280
28407
  return;
26281
28408
  const stamp = now().replace(/[:.]/g, "-");
26282
- const backupDir = join16(dirname7(dbPath), `${basename4(dbPath)}.backup-${stamp}`);
28409
+ const backupDir = join16(dirname8(dbPath), `${basename4(dbPath)}.backup-${stamp}`);
26283
28410
  const files = [];
26284
- mkdirSync9(backupDir, { recursive: true });
28411
+ mkdirSync10(backupDir, { recursive: true });
26285
28412
  for (const source9 of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
26286
28413
  if (!existsSync16(source9))
26287
28414
  continue;
@@ -26291,8 +28418,8 @@ function createBackup(dbPath) {
26291
28418
  }
26292
28419
  return files.length > 0 ? { path: backupDir, files } : undefined;
26293
28420
  }
26294
- function pushRepair(repairs, type, message, applied, count) {
26295
- repairs.push({ type, message, applied, count });
28421
+ function pushRepair(repairs, type, message, applied, count3) {
28422
+ repairs.push({ type, message, applied, count: count3 });
26296
28423
  }
26297
28424
  function summarize3(checks, repairs) {
26298
28425
  return {
@@ -26304,7 +28431,7 @@ function summarize3(checks, repairs) {
26304
28431
  };
26305
28432
  }
26306
28433
  function deleteOrphans(db, table, where) {
26307
- const before = countQuery(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
28434
+ const before = countQuery2(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
26308
28435
  if (before > 0)
26309
28436
  db.run(`DELETE FROM ${table} WHERE ${where}`);
26310
28437
  return before;
@@ -26341,7 +28468,7 @@ function runTodosDoctor(options = {}) {
26341
28468
  repairable: true
26342
28469
  });
26343
28470
  }
26344
- const orphanedParents = tableExists(db, "tasks") ? countQuery(db, "SELECT COUNT(*) as count FROM tasks t WHERE t.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM tasks p WHERE p.id = t.parent_id)") : 0;
28471
+ const orphanedParents = tableExists(db, "tasks") ? countQuery2(db, "SELECT COUNT(*) as count FROM tasks t WHERE t.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM tasks p WHERE p.id = t.parent_id)") : 0;
26345
28472
  if (orphanedParents > 0) {
26346
28473
  addCheck(checks, {
26347
28474
  severity: "error",
@@ -26351,7 +28478,7 @@ function runTodosDoctor(options = {}) {
26351
28478
  repairable: true
26352
28479
  });
26353
28480
  }
26354
- const orphanedDependencies = tableExists(db, "task_dependencies") && tableExists(db, "tasks") ? countQuery(db, "SELECT COUNT(*) as count FROM task_dependencies d WHERE NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = d.task_id) OR NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = d.depends_on)") : 0;
28481
+ const orphanedDependencies = tableExists(db, "task_dependencies") && tableExists(db, "tasks") ? countQuery2(db, "SELECT COUNT(*) as count FROM task_dependencies d WHERE NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = d.task_id) OR NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = d.depends_on)") : 0;
26355
28482
  if (orphanedDependencies > 0) {
26356
28483
  addCheck(checks, {
26357
28484
  severity: "error",
@@ -26368,18 +28495,18 @@ function runTodosDoctor(options = {}) {
26368
28495
  ["task_run_commands", "NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_run_commands.task_id) OR NOT EXISTS (SELECT 1 FROM task_runs r WHERE r.id = task_run_commands.run_id)"],
26369
28496
  ["task_run_artifacts", "NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_run_artifacts.task_id) OR NOT EXISTS (SELECT 1 FROM task_runs r WHERE r.id = task_run_artifacts.run_id)"]
26370
28497
  ];
26371
- let orphanedRows = 0;
28498
+ let orphanedRows2 = 0;
26372
28499
  for (const [table, where] of orphanTables) {
26373
28500
  if (!tableExists(db, table))
26374
28501
  continue;
26375
- orphanedRows += countQuery(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
28502
+ orphanedRows2 += countQuery2(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
26376
28503
  }
26377
- if (orphanedRows > 0) {
28504
+ if (orphanedRows2 > 0) {
26378
28505
  addCheck(checks, {
26379
28506
  severity: "error",
26380
28507
  type: "orphaned_child_rows",
26381
- message: `${orphanedRows} child rows reference missing tasks or runs`,
26382
- count: orphanedRows,
28508
+ message: `${orphanedRows2} child rows reference missing tasks or runs`,
28509
+ count: orphanedRows2,
26383
28510
  repairable: true
26384
28511
  });
26385
28512
  }
@@ -26443,15 +28570,15 @@ function runTodosDoctor(options = {}) {
26443
28570
  pushRepair(repairs, "orphaned_task_parents", "Cleared missing parent references", true, orphanedParents);
26444
28571
  }
26445
28572
  if (orphanedDependencies > 0 && tableExists(db, "task_dependencies")) {
26446
- const count = deleteOrphans(db, "task_dependencies", "NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_dependencies.task_id) OR NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_dependencies.depends_on)");
26447
- pushRepair(repairs, "orphaned_task_dependencies", "Deleted dependency rows referencing missing tasks", true, count);
28573
+ const count3 = deleteOrphans(db, "task_dependencies", "NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_dependencies.task_id) OR NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_dependencies.depends_on)");
28574
+ pushRepair(repairs, "orphaned_task_dependencies", "Deleted dependency rows referencing missing tasks", true, count3);
26448
28575
  }
26449
28576
  for (const [table, where] of orphanTables) {
26450
28577
  if (!tableExists(db, table))
26451
28578
  continue;
26452
- const count = deleteOrphans(db, table, where);
26453
- if (count > 0)
26454
- pushRepair(repairs, "orphaned_child_rows", `Deleted orphaned rows from ${table}`, true, count);
28579
+ const count3 = deleteOrphans(db, table, where);
28580
+ if (count3 > 0)
28581
+ pushRepair(repairs, "orphaned_child_rows", `Deleted orphaned rows from ${table}`, true, count3);
26455
28582
  }
26456
28583
  if (corruptJson.length > 0) {
26457
28584
  for (const cell of corruptJson) {
@@ -26490,10 +28617,10 @@ function runTodosDoctor(options = {}) {
26490
28617
  };
26491
28618
  }
26492
28619
  // src/lib/environment-snapshots.ts
26493
- import { createHash as createHash11 } from "crypto";
26494
- import { existsSync as existsSync17, readFileSync as readFileSync13, statSync as statSync8 } from "fs";
28620
+ import { createHash as createHash12 } from "crypto";
28621
+ import { existsSync as existsSync17, readFileSync as readFileSync14, statSync as statSync8 } from "fs";
26495
28622
  import { hostname, platform, arch } from "os";
26496
- import { dirname as dirname8, join as join17, resolve as resolve13 } from "path";
28623
+ import { dirname as dirname9, join as join17, resolve as resolve14 } from "path";
26497
28624
  import { tmpdir as tmpdir2 } from "os";
26498
28625
  init_database();
26499
28626
  var MANIFEST_FILES = ["package.json", "dashboard/package.json", "sdk/package.json"];
@@ -26512,8 +28639,8 @@ var CONFIG_FILES = [
26512
28639
  "vite.config.ts",
26513
28640
  "dashboard/vite.config.ts"
26514
28641
  ];
26515
- function sha2565(value) {
26516
- return createHash11("sha256").update(value).digest("hex");
28642
+ function sha2566(value) {
28643
+ return createHash12("sha256").update(value).digest("hex");
26517
28644
  }
26518
28645
  function fileRecord(root, relativePath) {
26519
28646
  const path = join17(root, relativePath);
@@ -26522,8 +28649,8 @@ function fileRecord(root, relativePath) {
26522
28649
  const stat = statSync8(path);
26523
28650
  if (!stat.isFile())
26524
28651
  return null;
26525
- const content = readFileSync13(path);
26526
- return { path: relativePath, sha256: sha2565(content), size_bytes: content.length };
28652
+ const content = readFileSync14(path);
28653
+ return { path: relativePath, sha256: sha2566(content), size_bytes: content.length };
26527
28654
  }
26528
28655
  function manifestRecord(root, relativePath) {
26529
28656
  const base = fileRecord(root, relativePath);
@@ -26625,14 +28752,14 @@ function defaultSnapshotDir() {
26625
28752
  const dbPath = getDatabasePath();
26626
28753
  if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
26627
28754
  return join17(tmpdir2(), "hasna-todos", "environment-snapshots");
26628
- return join17(dirname8(resolve13(dbPath)), "environment-snapshots");
28755
+ return join17(dirname9(resolve14(dbPath)), "environment-snapshots");
26629
28756
  }
26630
28757
  function snapshotWithId(snapshot) {
26631
- const digest = sha2565(JSON.stringify(snapshot)).slice(0, 24);
28758
+ const digest = sha2566(JSON.stringify(snapshot)).slice(0, 24);
26632
28759
  return { id: `env_${digest}`, ...snapshot };
26633
28760
  }
26634
28761
  function captureEnvironmentSnapshot(input = {}) {
26635
- const root = resolve13(input.root || process.cwd());
28762
+ const root = resolve14(input.root || process.cwd());
26636
28763
  const env = input.env || process.env;
26637
28764
  const warnings = [];
26638
28765
  const manifests = MANIFEST_FILES.map((file) => manifestRecord(root, file)).filter((file) => Boolean(file));
@@ -26672,13 +28799,13 @@ function captureEnvironmentSnapshot(input = {}) {
26672
28799
  });
26673
28800
  }
26674
28801
  function writeEnvironmentSnapshot(snapshot, outputPath) {
26675
- const path = outputPath ? resolve13(outputPath) : join17(defaultSnapshotDir(), `${snapshot.id}.json`);
26676
- ensureDir2(dirname8(path));
28802
+ const path = outputPath ? resolve14(outputPath) : join17(defaultSnapshotDir(), `${snapshot.id}.json`);
28803
+ ensureDir2(dirname9(path));
26677
28804
  writeJsonFile(path, snapshot);
26678
28805
  return path;
26679
28806
  }
26680
28807
  function readEnvironmentSnapshot(path) {
26681
- const snapshot = readJsonFile(resolve13(path));
28808
+ const snapshot = readJsonFile(resolve14(path));
26682
28809
  if (!snapshot || snapshot.schema_version !== 1 || typeof snapshot.id !== "string") {
26683
28810
  throw new Error(`Invalid environment snapshot: ${path}`);
26684
28811
  }
@@ -26971,14 +29098,391 @@ function renderPlanningForecastMarkdown(forecast) {
26971
29098
  return lines.join(`
26972
29099
  `);
26973
29100
  }
29101
+ // src/lib/cli-help.ts
29102
+ var COMPLETION_SHELLS = ["bash", "zsh", "fish"];
29103
+ var CORE_EXAMPLES = [
29104
+ "todos project-bootstrap . --json",
29105
+ 'todos add "Ship CLI help" --priority high --json',
29106
+ "todos ready --json",
29107
+ "todos usage report --max-tasks 1000 --max-projects 10 --json",
29108
+ 'todos runs command <run-id> "bun test" --status passed --summary "1836 pass, 0 fail"',
29109
+ "todos mcp"
29110
+ ];
29111
+ var JSON_CONTRACTS = [
29112
+ "local_task",
29113
+ "local_project",
29114
+ "task_run",
29115
+ "local_usage_ledger",
29116
+ "structured_error",
29117
+ "api_error"
29118
+ ];
29119
+ var ERROR_CODES = [
29120
+ { code: "0", meaning: "Command completed successfully." },
29121
+ { code: "1", meaning: 'Validation, lookup, database, or runtime failure. In JSON mode the CLI prints {"error":"message"}.' },
29122
+ { code: "structured_error", meaning: "Machine-readable error contract used by local MCP and SDK surfaces." },
29123
+ { code: "api_error", meaning: "HTTP API error envelope for the optional local server." }
29124
+ ];
29125
+ function optionEntry(option) {
29126
+ return {
29127
+ flags: option.flags,
29128
+ description: option.description || "",
29129
+ longFlag: option.long || null,
29130
+ shortFlag: option.short || null
29131
+ };
29132
+ }
29133
+ function collectCliCommandEntries(program, prefix = []) {
29134
+ const entries = [];
29135
+ for (const command of program.commands) {
29136
+ const path = [...prefix, command.name()];
29137
+ entries.push({
29138
+ path,
29139
+ command: path.join(" "),
29140
+ description: command.description() || "",
29141
+ aliases: command.aliases(),
29142
+ usage: command.usage(),
29143
+ options: command.options.map(optionEntry)
29144
+ });
29145
+ entries.push(...collectCliCommandEntries(command, path));
29146
+ }
29147
+ return entries;
29148
+ }
29149
+ function createCliManual(program) {
29150
+ return {
29151
+ title: "todos(1)",
29152
+ synopsis: "todos [global options] <command> [command options]",
29153
+ package_name: "@hasna/todos",
29154
+ local_only: true,
29155
+ install: ["bun install -g @hasna/todos"],
29156
+ update: ["bun install -g @hasna/todos", "todos upgrade"],
29157
+ completion_shells: COMPLETION_SHELLS,
29158
+ examples: CORE_EXAMPLES,
29159
+ json_contracts: JSON_CONTRACTS,
29160
+ error_codes: ERROR_CODES,
29161
+ commands: collectCliCommandEntries(program)
29162
+ };
29163
+ }
29164
+ function escapeMarkdownCell(value) {
29165
+ return value.replaceAll("|", "\\|").replaceAll(`
29166
+ `, " ");
29167
+ }
29168
+ function renderCliManualMarkdown(manual) {
29169
+ const lines = [
29170
+ `# ${manual.title}`,
29171
+ "",
29172
+ "## Name",
29173
+ "",
29174
+ "todos - local task, plan, run, and MCP workflows for AI coding agents",
29175
+ "",
29176
+ "## Synopsis",
29177
+ "",
29178
+ "```bash",
29179
+ manual.synopsis,
29180
+ "```",
29181
+ "",
29182
+ "## Install",
29183
+ "",
29184
+ "```bash",
29185
+ ...manual.install,
29186
+ "```",
29187
+ "",
29188
+ "## Update",
29189
+ "",
29190
+ "```bash",
29191
+ ...manual.update,
29192
+ "```",
29193
+ "",
29194
+ "## Shell Completions",
29195
+ "",
29196
+ "```bash",
29197
+ "todos completions bash > ~/.local/share/bash-completion/completions/todos",
29198
+ "todos completions zsh > ~/.zsh/completions/_todos",
29199
+ "todos completions fish > ~/.config/fish/completions/todos.fish",
29200
+ "```",
29201
+ "",
29202
+ "## Examples",
29203
+ "",
29204
+ "```bash",
29205
+ ...manual.examples,
29206
+ "```",
29207
+ "",
29208
+ "## JSON Output Contracts",
29209
+ "",
29210
+ "The CLI keeps JSON output stable for scripts and MCP adapters. Use `--json` or `-j` when a command supports machine output.",
29211
+ "",
29212
+ ...manual.json_contracts.map((contract2) => `- \`${contract2}\``),
29213
+ "",
29214
+ "## Error Codes",
29215
+ "",
29216
+ "| Code | Meaning |",
29217
+ "| --- | --- |",
29218
+ ...manual.error_codes.map((error) => `| \`${escapeMarkdownCell(error.code)}\` | ${escapeMarkdownCell(error.meaning)} |`),
29219
+ "",
29220
+ "## Command Catalog",
29221
+ "",
29222
+ "| Command | Description | Options |",
29223
+ "| --- | --- | --- |",
29224
+ ...manual.commands.map((command) => {
29225
+ const options = command.options.map((option) => `\`${option.flags}\``).join("<br>");
29226
+ return `| \`todos ${escapeMarkdownCell(command.command)}\` | ${escapeMarkdownCell(command.description)} | ${options || "-"} |`;
29227
+ }),
29228
+ ""
29229
+ ];
29230
+ return lines.join(`
29231
+ `);
29232
+ }
29233
+ function shellWords(values) {
29234
+ return values.map((value) => value.replaceAll("'", "'\\''")).join(" ");
29235
+ }
29236
+ function zshEntry(name, description) {
29237
+ return `'${name.replaceAll("'", "'\\''")}:${description.replaceAll("'", "'\\''")}'`;
29238
+ }
29239
+ function fishEscape(value) {
29240
+ return value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"");
29241
+ }
29242
+ function rootCommandNames(entries) {
29243
+ return entries.filter((entry) => entry.path.length === 1).map((entry) => entry.path[0]).filter((value) => Boolean(value));
29244
+ }
29245
+ function optionFlags(options) {
29246
+ const flags = new Set;
29247
+ for (const option of options) {
29248
+ if (option.longFlag)
29249
+ flags.add(option.longFlag);
29250
+ if (option.shortFlag)
29251
+ flags.add(option.shortFlag);
29252
+ }
29253
+ return [...flags].sort();
29254
+ }
29255
+ function commandOptions(program, entry) {
29256
+ return optionFlags([...program.options.map(optionEntry), ...entry.options]);
29257
+ }
29258
+ function generateCompletionScript(program, shell) {
29259
+ const entries = collectCliCommandEntries(program);
29260
+ const roots = rootCommandNames(entries);
29261
+ if (shell === "bash") {
29262
+ const rootCases = entries.filter((entry) => entry.path.length === 1).map((entry) => {
29263
+ const children = entries.filter((child) => child.path.length === 2 && child.path[0] === entry.path[0]).map((child) => child.path[1]).filter((value) => Boolean(value));
29264
+ const options = commandOptions(program, entry);
29265
+ return ` ${entry.path[0]})
29266
+ COMPREPLY=($(compgen -W '${shellWords([...children, ...options])}' -- "$cur"))
29267
+ return
29268
+ ;;`;
29269
+ }).join(`
29270
+ `);
29271
+ return [
29272
+ "# todos bash completion. Generated by `todos completions bash`.",
29273
+ "_todos_completion() {",
29274
+ " local cur",
29275
+ " COMPREPLY=()",
29276
+ ' cur="${COMP_WORDS[COMP_CWORD]}"',
29277
+ ' if [[ "$cur" == -* ]]; then',
29278
+ ` COMPREPLY=($(compgen -W '${shellWords(optionFlags(program.options.map(optionEntry)))}' -- "$cur"))`,
29279
+ " return",
29280
+ " fi",
29281
+ ' case "${COMP_WORDS[1]}" in',
29282
+ rootCases,
29283
+ " esac",
29284
+ ` COMPREPLY=($(compgen -W '${shellWords(roots)}' -- "$cur"))`,
29285
+ "}",
29286
+ "complete -F _todos_completion todos",
29287
+ ""
29288
+ ].join(`
29289
+ `);
29290
+ }
29291
+ if (shell === "zsh") {
29292
+ const commandEntries = entries.filter((entry) => entry.path.length === 1).map((entry) => zshEntry(entry.path[0] || "", entry.description));
29293
+ const optionEntries = optionFlags(program.options.map(optionEntry)).map((flag) => `'${flag}'`);
29294
+ const childCases = entries.filter((entry) => entry.path.length === 1).map((entry) => {
29295
+ const children = entries.filter((child) => child.path.length === 2 && child.path[0] === entry.path[0]);
29296
+ if (children.length === 0)
29297
+ return "";
29298
+ const childEntries = children.map((child) => zshEntry(child.path[1] || "", child.description)).join(" ");
29299
+ return ` ${entry.path[0]}) local -a subcommands; subcommands=(${childEntries}); _describe 'subcommand' subcommands ;;`;
29300
+ }).filter(Boolean).join(`
29301
+ `);
29302
+ return [
29303
+ "#compdef todos",
29304
+ "# todos zsh completion. Generated by `todos completions zsh`.",
29305
+ "_todos() {",
29306
+ ` local -a commands; commands=(${commandEntries.join(" ")})`,
29307
+ ` local -a global_options; global_options=(${optionEntries.join(" ")})`,
29308
+ " if (( CURRENT == 2 )); then",
29309
+ " _describe 'command' commands",
29310
+ " return",
29311
+ " fi",
29312
+ " case $words[2] in",
29313
+ childCases,
29314
+ " esac",
29315
+ " _arguments $global_options '*::arg:->args'",
29316
+ "}",
29317
+ '_todos "$@"',
29318
+ ""
29319
+ ].join(`
29320
+ `);
29321
+ }
29322
+ const fishLines = [
29323
+ "# todos fish completion. Generated by `todos completions fish`.",
29324
+ "complete -c todos -f"
29325
+ ];
29326
+ for (const option of program.options.map(optionEntry)) {
29327
+ const parts = ["complete -c todos"];
29328
+ if (option.longFlag)
29329
+ parts.push(`-l ${option.longFlag.replace(/^--/, "")}`);
29330
+ if (option.shortFlag)
29331
+ parts.push(`-s ${option.shortFlag.replace(/^-/, "")}`);
29332
+ parts.push(`-d "${fishEscape(option.description)}"`);
29333
+ fishLines.push(parts.join(" "));
29334
+ }
29335
+ for (const entry of entries) {
29336
+ if (entry.path.length === 1) {
29337
+ fishLines.push(`complete -c todos -n "__fish_use_subcommand" -a "${entry.path[0]}" -d "${fishEscape(entry.description)}"`);
29338
+ } else if (entry.path.length === 2) {
29339
+ fishLines.push(`complete -c todos -n "__fish_seen_subcommand_from ${entry.path[0]}" -a "${entry.path[1]}" -d "${fishEscape(entry.description)}"`);
29340
+ }
29341
+ }
29342
+ fishLines.push("");
29343
+ return fishLines.join(`
29344
+ `);
29345
+ }
29346
+ // src/lib/tui-dashboard.ts
29347
+ init_database();
29348
+ var TUI_DASHBOARD_VIEWS = ["overview", "projects", "tasks", "plans", "runs", "dependencies", "inbox", "search"];
29349
+ var TASK_STATUSES2 = ["pending", "in_progress", "completed", "failed", "cancelled"];
29350
+ var DEFAULT_LIMIT = 8;
29351
+ function limited(value) {
29352
+ const parsed = Number.isFinite(value) ? value : DEFAULT_LIMIT;
29353
+ return Math.max(1, Math.min(parsed || DEFAULT_LIMIT, 25));
29354
+ }
29355
+ function projectFilter(projectId) {
29356
+ return projectId ? { project_id: projectId } : {};
29357
+ }
29358
+ function dependencyRows(projectId, limit, db) {
29359
+ const where = ["t.archived_at IS NULL", "dep.archived_at IS NULL"];
29360
+ const params = [];
29361
+ if (projectId) {
29362
+ where.push("t.project_id = ?");
29363
+ params.push(projectId);
29364
+ }
29365
+ params.push(limit);
29366
+ return db.query(`SELECT
29367
+ td.task_id,
29368
+ t.title AS task_title,
29369
+ t.status AS task_status,
29370
+ td.depends_on,
29371
+ dep.title AS depends_on_title,
29372
+ dep.status AS depends_on_status
29373
+ FROM task_dependencies td
29374
+ JOIN tasks t ON t.id = td.task_id
29375
+ JOIN tasks dep ON dep.id = td.depends_on
29376
+ WHERE ${where.join(" AND ")}
29377
+ ORDER BY CASE WHEN dep.status = 'completed' THEN 1 ELSE 0 END, t.priority, t.created_at DESC
29378
+ LIMIT ?`).all(...params).map((row) => {
29379
+ const item = row;
29380
+ return { ...item, blocking: item.depends_on_status !== "completed" };
29381
+ });
29382
+ }
29383
+ function createTuiDashboardSnapshot(options = {}, db) {
29384
+ const d = db || getDatabase();
29385
+ const limit = limited(options.limit);
29386
+ const filter = projectFilter(options.project_id);
29387
+ const counts = TASK_STATUSES2.reduce((acc, status) => {
29388
+ acc[status] = countTasks({ ...filter, status }, d);
29389
+ return acc;
29390
+ }, { total: 0 });
29391
+ counts.total = TASK_STATUSES2.reduce((sum, status) => sum + counts[status], 0);
29392
+ const projectRows = listProjects(d).filter((project) => !options.project_id || project.id === options.project_id).slice(0, limit).map((project) => ({
29393
+ id: project.id,
29394
+ name: project.name,
29395
+ path: project.path,
29396
+ open_tasks: countTasks({ project_id: project.id, status: ["pending", "in_progress"] }, d)
29397
+ }));
29398
+ const plans = listPlans(options.project_id, d).slice(0, limit).map((plan) => ({
29399
+ id: plan.id,
29400
+ name: plan.name,
29401
+ status: plan.status,
29402
+ project_id: plan.project_id,
29403
+ open_tasks: countTasks({ plan_id: plan.id, status: ["pending", "in_progress"] }, d)
29404
+ }));
29405
+ const taskRows = listTasks({ ...filter, status: ["pending", "in_progress"], limit }, d);
29406
+ const projectTaskIds = new Set(options.project_id ? listTasks({ project_id: options.project_id, include_archived: true }, d).map((task2) => task2.id) : []);
29407
+ const runs = listTaskRuns(undefined, d).filter((run) => !options.project_id || projectTaskIds.has(run.task_id)).slice(0, limit);
29408
+ const inbox = listInboxItems({ limit }, d);
29409
+ const searchResults = options.search ? searchTasks({ query: options.search, project_id: options.project_id }, undefined, undefined, d).slice(0, limit) : [];
29410
+ return {
29411
+ generated_at: new Date().toISOString(),
29412
+ local_only: true,
29413
+ project_id: options.project_id ?? null,
29414
+ active_view: options.active_view || "overview",
29415
+ keymap: [
29416
+ "q quit",
29417
+ "r refresh",
29418
+ "h/left previous tab",
29419
+ "l/right next tab",
29420
+ "1-8 jump tabs",
29421
+ "/ search"
29422
+ ],
29423
+ counts,
29424
+ projects: projectRows,
29425
+ tasks: taskRows,
29426
+ plans,
29427
+ runs,
29428
+ dependencies: dependencyRows(options.project_id, limit, d),
29429
+ inbox,
29430
+ search: {
29431
+ query: options.search || "",
29432
+ total: searchResults.length,
29433
+ results: searchResults
29434
+ }
29435
+ };
29436
+ }
29437
+ function lineId(id) {
29438
+ return id.slice(0, 8);
29439
+ }
29440
+ function renderTuiDashboardSnapshot(snapshot) {
29441
+ const lines = [
29442
+ "# todos terminal dashboard",
29443
+ "",
29444
+ `View: ${snapshot.active_view}`,
29445
+ `Local only: ${snapshot.local_only}`,
29446
+ `Keys: ${snapshot.keymap.join(" | ")}`,
29447
+ "",
29448
+ `Tasks: ${snapshot.counts.pending} pending | ${snapshot.counts.in_progress} active | ${snapshot.counts.completed} done | ${snapshot.counts.failed} failed | ${snapshot.counts.total} total`,
29449
+ "",
29450
+ "## Projects",
29451
+ ...snapshot.projects.map((project) => `- ${lineId(project.id)} ${project.name} (${project.open_tasks} open) ${project.path}`),
29452
+ "",
29453
+ "## Tasks",
29454
+ ...snapshot.tasks.map((task2) => `- ${lineId(task2.id)} [${task2.status}] ${task2.priority} ${task2.title}`),
29455
+ "",
29456
+ "## Plans",
29457
+ ...snapshot.plans.map((plan) => `- ${lineId(plan.id)} [${plan.status}] ${plan.name} (${plan.open_tasks} open)`),
29458
+ "",
29459
+ "## Runs",
29460
+ ...snapshot.runs.map((run) => `- ${lineId(run.id)} [${run.status}] ${run.title || run.task_id}`),
29461
+ "",
29462
+ "## Dependencies",
29463
+ ...snapshot.dependencies.map((dep) => `- ${lineId(dep.task_id)} waits on ${lineId(dep.depends_on)} [${dep.depends_on_status}]${dep.blocking ? " blocking" : ""}`),
29464
+ "",
29465
+ "## Inbox",
29466
+ ...snapshot.inbox.map((item) => `- ${lineId(item.id)} [${item.status}] ${item.title}`),
29467
+ "",
29468
+ "## Search",
29469
+ snapshot.search.query ? `Query: ${snapshot.search.query}` : "Query: (none)",
29470
+ ...snapshot.search.results.map((task2) => `- ${lineId(task2.id)} [${task2.status}] ${task2.title}`),
29471
+ ""
29472
+ ];
29473
+ return lines.join(`
29474
+ `);
29475
+ }
26974
29476
  export {
26975
29477
  writeSdkIntegrationFixtures,
26976
29478
  writeOnboardingFixtureFiles,
29479
+ writeLocalBackupFile,
26977
29480
  writeEnvironmentSnapshot,
26978
29481
  writeBuiltinTemplateFiles,
26979
29482
  watchSourceTodos,
26980
29483
  verifyTaskRunArtifacts,
26981
29484
  verifyStoredArtifact,
29485
+ verifyLocalBackup,
26982
29486
  verifyLocalAuditLedger,
26983
29487
  verifyExtensionSignature,
26984
29488
  verifyApiKey,
@@ -27043,6 +29547,7 @@ export {
27043
29547
  simulateAgentReplayFile,
27044
29548
  simulateAgentReplay,
27045
29549
  shouldRegisterToolForProfile,
29550
+ setTaskWorkflowState,
27046
29551
  setTaskStatus,
27047
29552
  setTaskPriority,
27048
29553
  setTaskLocalFields,
@@ -27069,6 +29574,8 @@ export {
27069
29574
  returnReviewItem,
27070
29575
  retryAgentRunDispatch,
27071
29576
  resumeFocusSession,
29577
+ restoreLocalBackup,
29578
+ resolveWorkflowState,
27072
29579
  resolveVariables,
27073
29580
  resolveTaskRunId,
27074
29581
  resolvePartialId,
@@ -27079,20 +29586,26 @@ export {
27079
29586
  requestReviewQueue,
27080
29587
  requestApprovalGate,
27081
29588
  reopenReviewItem,
29589
+ renderWorkflowStatesMarkdown,
27082
29590
  renderWorkflowPromptMarkdown,
27083
29591
  renderWorkflowPrompt,
29592
+ renderTuiDashboardSnapshot,
27084
29593
  renderTerminalNotification,
27085
29594
  renderTaskBoard,
29595
+ renderScalePerformanceReportMarkdown,
27086
29596
  renderRoadmapMarkdown,
27087
29597
  renderRiskRegisterMarkdown,
27088
29598
  renderRetrospectiveMarkdown,
27089
29599
  renderReleaseNotesMarkdown,
27090
29600
  renderReleaseCompatibilityMarkdown,
27091
29601
  renderPlanningForecastMarkdown,
29602
+ renderLocalUsageLedgerMarkdown,
27092
29603
  renderLocalSnapshotMarkdown,
29604
+ renderLocalReportMarkdown,
27093
29605
  renderLocalAuditLedgerMarkdown,
27094
29606
  renderKnowledgeExportMarkdown,
27095
29607
  renderExtensionSummary,
29608
+ renderCliManualMarkdown,
27096
29609
  renderAgentReplaySimulationMarkdown,
27097
29610
  renderAgentReliabilityMarkdown,
27098
29611
  renderAgentContextPackMarkdown,
@@ -27128,8 +29641,10 @@ export {
27128
29641
  redactEvidenceText,
27129
29642
  recordTaskReview,
27130
29643
  recordEnvironmentSnapshot,
29644
+ readLocalBackupFile,
27131
29645
  readEnvironmentSnapshot,
27132
29646
  queueAgentRun,
29647
+ queryTasksByWorkflowState,
27133
29648
  queryTasksByLocalFields,
27134
29649
  previewTemplate,
27135
29650
  previewRetentionCleanup,
@@ -27148,6 +29663,7 @@ export {
27148
29663
  nextOccurrence,
27149
29664
  moveTask,
27150
29665
  moveBoardCard,
29666
+ migrateWorkflowStates,
27151
29667
  mergeDuplicateTask,
27152
29668
  matchCapabilities,
27153
29669
  logTrace,
@@ -27158,6 +29674,7 @@ export {
27158
29674
  lockTask,
27159
29675
  loadConfig,
27160
29676
  listWorkspaceTrustProfiles,
29677
+ listWorkflowStates,
27161
29678
  listWorkflowPrompts,
27162
29679
  listWebhooks,
27163
29680
  listVerificationProviders,
@@ -27191,6 +29708,7 @@ export {
27191
29708
  listMachines,
27192
29709
  listMachineLocalPaths,
27193
29710
  listLocalSnapshotResources,
29711
+ listLocalReportTypes,
27194
29712
  listLocalExtensions,
27195
29713
  listLocalEventHooks,
27196
29714
  listLocalAuditLedgerCheckpoints,
@@ -27247,6 +29765,7 @@ export {
27247
29765
  getTemplateTasks,
27248
29766
  getTemplate,
27249
29767
  getTasksChangedSince,
29768
+ getTaskWorkflowState,
27250
29769
  getTaskWithRelations,
27251
29770
  getTaskTraces,
27252
29771
  getTaskStats,
@@ -27345,6 +29864,7 @@ export {
27345
29864
  getActiveModel,
27346
29865
  generateReleaseNotes,
27347
29866
  generateCycles,
29867
+ generateCompletionScript,
27348
29868
  gatherTrainingData,
27349
29869
  formatTmuxTarget,
27350
29870
  formatSingleTask,
@@ -27386,6 +29906,7 @@ export {
27386
29906
  dispatchToMultiple,
27387
29907
  discoverVerificationProviderCapabilities,
27388
29908
  discoverProjectWorkspace,
29909
+ discoverLocalExtensions,
27389
29910
  detectInboxSourceType,
27390
29911
  describeTerminalNotificationRule,
27391
29912
  deriveInboxTitle,
@@ -27411,6 +29932,7 @@ export {
27411
29932
  decryptBridgeBundle,
27412
29933
  decomposeTasks,
27413
29934
  createWebhook,
29935
+ createTuiDashboardSnapshot,
27414
29936
  createTodosRegistry,
27415
29937
  createTemplate,
27416
29938
  createTaskList,
@@ -27419,6 +29941,7 @@ export {
27419
29941
  createSessionRecoveryHandoff,
27420
29942
  createSession,
27421
29943
  createSdkIntegrationFixturePack,
29944
+ createScalePerformanceReport,
27422
29945
  createRoadmap,
27423
29946
  createRiskRegisterExport,
27424
29947
  createRisk,
@@ -27430,8 +29953,11 @@ export {
27430
29953
  createOrg,
27431
29954
  createMilestone,
27432
29955
  createMcpManifest,
29956
+ createLocalUsageLedger,
27433
29957
  createLocalSqliteTodosStorageAdapter,
29958
+ createLocalReport,
27434
29959
  createLocalBridgeBundle,
29960
+ createLocalBackup,
27435
29961
  createKnowledgeSnapshot,
27436
29962
  createKnowledgeRecord,
27437
29963
  createKnowledgeExportReport,
@@ -27445,6 +29971,7 @@ export {
27445
29971
  createContractsManifest,
27446
29972
  createClient,
27447
29973
  createCliMcpParityManifest,
29974
+ createCliManual,
27448
29975
  createCapabilityManifest,
27449
29976
  createCalendarItem,
27450
29977
  createBranchWorkPlan,
@@ -27455,6 +29982,8 @@ export {
27455
29982
  completeTask,
27456
29983
  compareEnvironmentSnapshots,
27457
29984
  compareEnvironmentSnapshotFiles,
29985
+ compactScaleStorage,
29986
+ collectCliCommandEntries,
27458
29987
  closeRisk,
27459
29988
  closeDatabase,
27460
29989
  cloneTask,
@@ -27469,6 +29998,7 @@ export {
27469
29998
  checkRunnerSandbox,
27470
29999
  checkLock,
27471
30000
  checkLocalNotifications,
30001
+ checkLocalIntegrity,
27472
30002
  checkCompletionGuard,
27473
30003
  checkChecklistItem,
27474
30004
  checkBudget,
@@ -27512,6 +30042,7 @@ export {
27512
30042
  TodosClient,
27513
30043
  TaskNotFoundError,
27514
30044
  TaskListNotFoundError,
30045
+ TUI_DASHBOARD_VIEWS,
27515
30046
  TODOS_SDK_INTEGRATION_FIXTURE_SCHEMA_VERSION,
27516
30047
  TODOS_SDK_INTEGRATION_FIXTURE_GENERATED_AT,
27517
30048
  TODOS_REGISTRY,
@@ -27522,8 +30053,12 @@ export {
27522
30053
  TODOS_MARKDOWN_SCHEMA,
27523
30054
  TODOS_MARKDOWN_BRIDGE_MARKER,
27524
30055
  TODOS_LOCAL_SNAPSHOT_SCHEMA_VERSION,
30056
+ TODOS_LOCAL_INTEGRITY_SCHEMA_VERSION,
30057
+ TODOS_LOCAL_INTEGRITY_KIND,
27525
30058
  TODOS_LOCAL_BRIDGE_SCHEMA_VERSION,
27526
30059
  TODOS_LOCAL_BRIDGE_KIND,
30060
+ TODOS_LOCAL_BACKUP_SCHEMA_VERSION,
30061
+ TODOS_LOCAL_BACKUP_KIND,
27527
30062
  TODOS_JSON_CONTRACTS_MANIFEST,
27528
30063
  TODOS_JSON_CONTRACTS,
27529
30064
  TODOS_ERROR_CODES,
@@ -27545,11 +30080,15 @@ export {
27545
30080
  MCP_TOOL_GROUPS,
27546
30081
  MCP_PROFILE_GROUPS,
27547
30082
  LockError,
30083
+ LOCAL_USAGE_LEDGER_SCHEMA_VERSION,
27548
30084
  LOCAL_ROADMAP_SCHEMA_VERSION,
30085
+ LOCAL_REPORT_TYPES,
30086
+ LOCAL_REPORT_SCHEMA_VERSION,
27549
30087
  LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION,
27550
30088
  LOCAL_NOTIFICATION_SCHEMA_VERSION,
27551
30089
  LOCAL_EVENT_TYPES,
27552
30090
  LOCAL_CAPACITY_SCHEMA_VERSION,
30091
+ LOCAL_BACKUP_CHECKSUM_ALGORITHM,
27553
30092
  LOCAL_AUDIT_LEDGER_SCHEMA_VERSION,
27554
30093
  LOCAL_AUDIT_LEDGER_HASH_ALGORITHM,
27555
30094
  EXTRACT_TAGS,
@@ -27564,6 +30103,7 @@ export {
27564
30103
  DEFAULT_ENCRYPTION_KEY_ENV,
27565
30104
  CompletionGuardError,
27566
30105
  CORE_MCP_TOOLS,
30106
+ COMPLETION_SHELLS,
27567
30107
  BUILTIN_TEMPLATE_LIBRARY_VERSION,
27568
30108
  BUILTIN_TEMPLATE_LIBRARY_SOURCE,
27569
30109
  BUILTIN_TEMPLATES,