@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/registry.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");
@@ -2700,7 +2704,9 @@ var MCP_TOOL_GROUPS = {
2700
2704
  "create_risk",
2701
2705
  "create_handoff",
2702
2706
  "capture_environment_snapshot",
2707
+ "check_local_integrity",
2703
2708
  "compare_environment_snapshots",
2709
+ "create_local_backup",
2704
2710
  "create_inbox_item",
2705
2711
  "delete_comment",
2706
2712
  "detect_file_relationships",
@@ -2720,6 +2726,7 @@ var MCP_TOOL_GROUPS = {
2720
2726
  "add_task_run_event",
2721
2727
  "add_task_run_file",
2722
2728
  "acknowledge_handoff",
2729
+ "build_local_report",
2723
2730
  "cancel_agent_run_dispatch",
2724
2731
  "finish_task_run",
2725
2732
  "find_duplicate_tasks",
@@ -2739,9 +2746,11 @@ var MCP_TOOL_GROUPS = {
2739
2746
  "close_risk",
2740
2747
  "get_task_git_refs",
2741
2748
  "get_task_run_ledger",
2749
+ "get_usage_ledger",
2742
2750
  "list_agent_run_adapters",
2743
2751
  "list_agent_run_queue",
2744
2752
  "verify_task_run_artifacts",
2753
+ "verify_local_backup",
2745
2754
  "get_task_traceability",
2746
2755
  "get_task_commits",
2747
2756
  "get_task_dependencies",
@@ -2758,6 +2767,7 @@ var MCP_TOOL_GROUPS = {
2758
2767
  "list_handoffs",
2759
2768
  "list_inbox_items",
2760
2769
  "list_knowledge_records",
2770
+ "list_local_report_types",
2761
2771
  "list_onboarding_fixtures",
2762
2772
  "list_review_routing_rules",
2763
2773
  "list_local_snapshots",
@@ -2769,6 +2779,7 @@ var MCP_TOOL_GROUPS = {
2769
2779
  "queue_agent_run",
2770
2780
  "remove_agent_run_adapter",
2771
2781
  "remove_review_routing_rule",
2782
+ "restore_local_backup",
2772
2783
  "retry_agent_run_dispatch",
2773
2784
  "resolve_mentions",
2774
2785
  "run_next_agent_dispatch",
@@ -2779,6 +2790,7 @@ var MCP_TOOL_GROUPS = {
2779
2790
  "set_review_routing_rule",
2780
2791
  "set_verification_provider",
2781
2792
  "simulate_agent_replay",
2793
+ "discover_local_extensions",
2782
2794
  "inspect_local_extension",
2783
2795
  "install_local_extension",
2784
2796
  "list_local_extensions",
@@ -2846,11 +2858,15 @@ var MCP_TOOL_GROUPS = {
2846
2858
  "get_task_graph",
2847
2859
  "get_task_history",
2848
2860
  "get_task_stats",
2861
+ "list_workflow_states",
2849
2862
  "list_labels",
2850
2863
  "list_tags",
2864
+ "migrate_workflow_states",
2865
+ "query_tasks_by_workflow_state",
2851
2866
  "query_tasks_by_fields",
2852
2867
  "search_tools",
2853
2868
  "describe_tools",
2869
+ "set_task_workflow_state",
2854
2870
  "set_task_fields",
2855
2871
  "update_label",
2856
2872
  "update_tag"
@@ -3340,6 +3356,161 @@ var TODOS_JSON_CONTRACTS = [
3340
3356
  },
3341
3357
  optional: {}
3342
3358
  }),
3359
+ contract({
3360
+ id: "local_usage_ledger",
3361
+ name: "Local Usage Ledger",
3362
+ description: "Aggregate local report for task, project, run, command, cost, duration, storage, and simulated quota usage.",
3363
+ surfaces: ["cli", "mcp", "sdk"],
3364
+ stability: "stable",
3365
+ required: {
3366
+ schema_version: field("integer", "Report schema version."),
3367
+ local_only: field("boolean", "Always true; report reads only local todos state."),
3368
+ no_network: field("boolean", "Always true; report does not call hosted services."),
3369
+ generated_at: isoDateField,
3370
+ scope: field("object", "Project, agent, and time filters used for the aggregate."),
3371
+ counts: field("object", "Counts for tasks, projects, runs, commands, artifacts, traces, and usage metadata records."),
3372
+ durations: field("object", "Completed run, open run, trace, and total observed duration in milliseconds."),
3373
+ usage: field("object", "Token and USD aggregates from task cost fields, traces, and agent-provided metadata."),
3374
+ storage: field("object", "Evidence storage byte totals from local run artifacts."),
3375
+ quota: field("object", "Optional local quota simulation with exceeded limits."),
3376
+ redaction: field("object", "Aggregate-only guarantees for command and artifact path omission."),
3377
+ sources: field("array", "Local SQLite tables included in the report.")
3378
+ },
3379
+ optional: {}
3380
+ }),
3381
+ contract({
3382
+ id: "local_report",
3383
+ name: "Local Agent Report",
3384
+ description: "Local-only report composing ready, blocked, overdue, plan, run, verification, and agent summaries.",
3385
+ surfaces: ["cli", "mcp", "sdk"],
3386
+ stability: "stable",
3387
+ required: {
3388
+ schema_version: field("integer", "Report schema version."),
3389
+ local_only: field("boolean", "Always true; report reads only local todos state."),
3390
+ no_network: field("boolean", "Always true; report does not call hosted analytics or usage collection."),
3391
+ generated_at: isoDateField,
3392
+ scope: field("object", "Project, plan, agent, and time filters used to build the report."),
3393
+ report_types: field("array", "Stable local report sections included by this package."),
3394
+ views: field("object", "Ready, blocked, and overdue task views."),
3395
+ plans: field("array", "Plan progress summaries with blocked and overdue counts."),
3396
+ runs: field("object", "Run outcome counts and recent run evidence summaries."),
3397
+ verification: field("object", "Verification outcome counts and recent verification evidence summaries."),
3398
+ agents: field("array", "Per-agent task, run, and verification summaries."),
3399
+ exports: field("object", "JSON contract and Markdown support metadata.")
3400
+ },
3401
+ optional: {}
3402
+ }),
3403
+ contract({
3404
+ id: "local_backup_bundle",
3405
+ name: "Local Backup Bundle",
3406
+ description: "Local backup wrapper around a bridge bundle with manifest counts, section checksums, SQLite integrity metadata, and artifact-content coverage.",
3407
+ surfaces: ["cli", "mcp", "sdk"],
3408
+ stability: "stable",
3409
+ required: {
3410
+ schema_version: field("integer", "Backup schema version."),
3411
+ kind: field("string", "Backup bundle kind identifier."),
3412
+ local_only: field("boolean", "Always true; backup creation reads only local state."),
3413
+ no_network: field("boolean", "Always true; backup creation performs no network requests."),
3414
+ created_at: isoDateField,
3415
+ package: field("object", "Package source metadata."),
3416
+ manifest: field("object", "Backup manifest with checksums, counts, source scope, and SQLite integrity."),
3417
+ bridge: field("object", "Embedded local bridge bundle containing tasks, projects, plans, runs, comments, evidence, and stored artifact content."),
3418
+ checksum_algorithm: field("string", "Digest algorithm used for backup and bridge checksums."),
3419
+ checksum: field("string", "SHA-256 checksum of the backup payload excluding this field.")
3420
+ },
3421
+ optional: {}
3422
+ }),
3423
+ contract({
3424
+ id: "local_backup_verification",
3425
+ name: "Local Backup Verification",
3426
+ description: "Verification report for a local backup bundle covering backup checksum, bridge checksum, manifest counts, schema compatibility, and current SQLite integrity.",
3427
+ surfaces: ["cli", "mcp", "sdk"],
3428
+ stability: "stable",
3429
+ required: {
3430
+ schema_version: field("integer", "Verification schema version."),
3431
+ kind: field("string", "Verification report kind identifier."),
3432
+ local_only: field("boolean", "Always true; verification reads local files and SQLite only."),
3433
+ no_network: field("boolean", "Always true; verification performs no network requests."),
3434
+ verified_at: isoDateField,
3435
+ ok: field("boolean", "True when all checks pass."),
3436
+ checksum_algorithm: field("string", "Digest algorithm used for backup and bridge checksums."),
3437
+ checksum: field("object", "Expected and actual backup checksum status."),
3438
+ bridge_checksum: field("object", "Expected and actual bridge checksum status."),
3439
+ bridge_validation: field("object", "Embedded bridge schema validation result."),
3440
+ sqlite: field(["object", "null"], "Current SQLite integrity check, or null when skipped.", true),
3441
+ counts: field("object", "Manifest expected counts, actual bridge counts, and status."),
3442
+ compatible: field("boolean", "True when embedded bridge schema is compatible with this package."),
3443
+ issues: field("array", "Blocking verification issues."),
3444
+ warnings: field("array", "Non-blocking local warnings.")
3445
+ },
3446
+ optional: {}
3447
+ }),
3448
+ contract({
3449
+ id: "local_backup_restore_result",
3450
+ name: "Local Backup Restore Result",
3451
+ description: "Dry-run or applied local backup restore result with verification and bridge import details.",
3452
+ surfaces: ["cli", "mcp", "sdk"],
3453
+ stability: "stable",
3454
+ required: {
3455
+ schema_version: field("integer", "Restore result schema version."),
3456
+ kind: field("string", "Restore result kind identifier."),
3457
+ local_only: field("boolean", "Always true; restore targets only local SQLite state."),
3458
+ no_network: field("boolean", "Always true; restore performs no network requests."),
3459
+ restored_at: isoDateField,
3460
+ dry_run: field("boolean", "True when no local records were written."),
3461
+ ok: field("boolean", "True when verification passes and the import has no blocking issues."),
3462
+ verification: field("object", "Backup verification result run before importing."),
3463
+ import_result: field(["object", "null"], "Bridge import result, or null when verification failed.", true),
3464
+ issues: field("array", "Blocking verification or import issues.")
3465
+ },
3466
+ optional: {}
3467
+ }),
3468
+ contract({
3469
+ id: "local_integrity_report",
3470
+ name: "Local Integrity Report",
3471
+ description: "Local integrity report for SQLite quick_check, foreign keys, bridge validation, backup-relevant counts, and orphan rows.",
3472
+ surfaces: ["cli", "mcp", "sdk"],
3473
+ stability: "stable",
3474
+ required: {
3475
+ schema_version: field("integer", "Integrity report schema version."),
3476
+ kind: field("string", "Integrity report kind identifier."),
3477
+ local_only: field("boolean", "Always true; report reads only local SQLite state."),
3478
+ no_network: field("boolean", "Always true; report performs no network requests."),
3479
+ generated_at: isoDateField,
3480
+ database_path: field("string", "Resolved local SQLite database path."),
3481
+ sqlite: field("object", "SQLite quick_check and foreign key integrity summary."),
3482
+ bridge_validation: field("object", "Bridge validation result for a freshly-created local bridge bundle."),
3483
+ counts: field("object", "Backup-relevant record counts by bridge section."),
3484
+ orphaned_rows: field("object", "Detected orphaned local rows by relationship."),
3485
+ ok: field("boolean", "True when no blocking integrity issues are found."),
3486
+ issues: field("array", "Blocking integrity issues."),
3487
+ warnings: field("array", "Non-blocking local warnings.")
3488
+ },
3489
+ optional: {}
3490
+ }),
3491
+ contract({
3492
+ id: "terminal_dashboard_snapshot",
3493
+ name: "Terminal Dashboard Snapshot",
3494
+ description: "Deterministic local snapshot for the keyboard-first terminal dashboard.",
3495
+ surfaces: ["cli", "sdk"],
3496
+ stability: "stable",
3497
+ required: {
3498
+ generated_at: isoDateField,
3499
+ local_only: field("boolean", "Always true; dashboard snapshots read only local todos state."),
3500
+ project_id: field(["string", "null"], "Optional project scope.", true),
3501
+ active_view: field("string", "Active dashboard tab rendered by the TUI or snapshot command."),
3502
+ keymap: field("array", "Keyboard shortcuts exposed by the TUI."),
3503
+ counts: field("object", "Task counts by status and total."),
3504
+ projects: field("array", "Visible projects with open task counts."),
3505
+ tasks: field("array", "Visible pending and in-progress tasks."),
3506
+ plans: field("array", "Visible plans with open task counts."),
3507
+ runs: field("array", "Recent local task runs."),
3508
+ dependencies: field("array", "Local dependency edges and blocking state."),
3509
+ inbox: field("array", "Recent local inbox items."),
3510
+ search: field("object", "Local search query and matching task results.")
3511
+ },
3512
+ optional: {}
3513
+ }),
3343
3514
  contract({
3344
3515
  id: "mention_resolution_report",
3345
3516
  name: "Mention Resolution Report",
@@ -3587,6 +3758,56 @@ var TODOS_JSON_CONTRACTS = [
3587
3758
  },
3588
3759
  optional: {}
3589
3760
  }),
3761
+ contract({
3762
+ id: "workflow_state_config",
3763
+ name: "Workflow State Config",
3764
+ description: "Local workflow state definition mapped onto a canonical task status.",
3765
+ surfaces: ["cli", "mcp", "sdk"],
3766
+ stability: "stable",
3767
+ required: {
3768
+ name: field("string", "Local workflow state name."),
3769
+ canonical_status: field("string", "Canonical task status used for storage and compatibility."),
3770
+ aliases: field("array", "Alternate input names for this workflow state."),
3771
+ terminal: field("boolean", "Whether this state represents terminal work.")
3772
+ },
3773
+ optional: {
3774
+ description: field("string", "Optional human description."),
3775
+ transitions: field(["array", "null"], "Allowed destination state names, or null for unrestricted.", true),
3776
+ color: field("string", "Optional display color token.")
3777
+ }
3778
+ }),
3779
+ contract({
3780
+ id: "workflow_state_result",
3781
+ name: "Workflow State Result",
3782
+ description: "Result of setting a task's local workflow state.",
3783
+ surfaces: ["cli", "mcp", "sdk"],
3784
+ stability: "stable",
3785
+ required: {
3786
+ task: field("object", "Updated canonical task object."),
3787
+ workflow_state: field("object", "Resolved target workflow state."),
3788
+ previous_workflow_state: field("object", "Resolved prior workflow state."),
3789
+ changed: field("boolean", "Whether the local workflow state changed."),
3790
+ canonical_status_changed: field("boolean", "Whether the task row status changed."),
3791
+ local_only: field("boolean", "Always true; operation uses local state only.")
3792
+ },
3793
+ optional: {}
3794
+ }),
3795
+ contract({
3796
+ id: "workflow_state_migration",
3797
+ name: "Workflow State Migration",
3798
+ description: "Dry-run or applied local migration from canonical statuses to workflow state metadata.",
3799
+ surfaces: ["cli", "mcp", "sdk"],
3800
+ stability: "stable",
3801
+ required: {
3802
+ applied: field("boolean", "Whether metadata writes were applied."),
3803
+ migrated_count: field("integer", "Number of tasks written during an applied migration."),
3804
+ pending_count: field("integer", "Number of tasks that would be migrated in dry-run mode."),
3805
+ skipped_count: field("integer", "Number of tasks already carrying matching workflow metadata."),
3806
+ items: field("array", "Per-task migration preview records."),
3807
+ local_only: field("boolean", "Always true; migration uses local state only.")
3808
+ },
3809
+ optional: {}
3810
+ }),
3590
3811
  contract({
3591
3812
  id: "retention_cleanup_report",
3592
3813
  name: "Retention Cleanup Report",
@@ -3608,6 +3829,45 @@ var TODOS_JSON_CONTRACTS = [
3608
3829
  },
3609
3830
  optional: {}
3610
3831
  }),
3832
+ contract({
3833
+ id: "scale_performance_report",
3834
+ name: "Scale Performance Report",
3835
+ description: "Local scale hardening report for query performance, archive readiness, compaction, SQLite integrity, and expected indexes. It performs no network calls.",
3836
+ surfaces: ["cli", "sdk"],
3837
+ stability: "stable",
3838
+ required: {
3839
+ schema_version: field("integer", "Report schema version."),
3840
+ local_only: field("boolean", "Always true; the report reads only local state."),
3841
+ no_network: field("boolean", "Always true; the report performs no network requests."),
3842
+ generated_at: isoDateField,
3843
+ database_path: field("string", "Resolved local SQLite database path."),
3844
+ counts: field("object", "Local task, project, agent, plan, run, event, comment, and dependency counts."),
3845
+ benchmarks: field("array", "Measured local query timings with thresholds."),
3846
+ archive: field("object", "Archive-readiness counts for old terminal tasks and include-archived visibility."),
3847
+ compaction: field("object", "SQLite page and freelist state with recommended maintenance commands."),
3848
+ integrity: field("object", "SQLite quick_check, foreign key, and required-index results."),
3849
+ warnings: field("array", "Non-fatal warnings for slow queries, missing indexes, old archive candidates, or integrity issues.")
3850
+ },
3851
+ optional: {}
3852
+ }),
3853
+ contract({
3854
+ id: "scale_compaction_result",
3855
+ name: "Scale Compaction Result",
3856
+ description: "Dry-run or applied local SQLite optimization and VACUUM compaction result.",
3857
+ surfaces: ["cli", "sdk"],
3858
+ stability: "stable",
3859
+ required: {
3860
+ schema_version: field("integer", "Result schema version."),
3861
+ local_only: field("boolean", "Always true; compaction targets only the local SQLite database."),
3862
+ no_network: field("boolean", "Always true; compaction performs no network requests."),
3863
+ dry_run: field("boolean", "True when commands were only previewed."),
3864
+ database_path: field("string", "Resolved local SQLite database path."),
3865
+ before: field("object", "Page and freelist counts before compaction."),
3866
+ after: field("object", "Page and freelist counts after compaction or dry-run preview."),
3867
+ actions: field("array", "SQLite maintenance actions planned or applied.")
3868
+ },
3869
+ optional: {}
3870
+ }),
3611
3871
  contract({
3612
3872
  id: "duplicate_task_candidate",
3613
3873
  name: "Duplicate Task Candidate",
@@ -3712,12 +3972,30 @@ var TODOS_JSON_CONTRACTS = [
3712
3972
  manifest: field("object", "Normalized extension manifest."),
3713
3973
  validation: field("object", "Schema, compatibility, permission, and sandbox validation details."),
3714
3974
  ok: field("boolean", "Whether the extension passed hard compatibility checks."),
3715
- summary: field("object", "Counts for commands, MCP tools, hooks, permissions, sandbox checks, and failed dry-runs."),
3975
+ summary: field("object", "Counts for commands, MCP tools, templates, renderers, hooks, permissions, sandbox checks, and failed dry-runs."),
3716
3976
  errors: field("array", "Hard validation or compatibility errors."),
3717
3977
  warnings: field("array", "Non-blocking diagnostics such as sandbox approval requirements.")
3718
3978
  },
3719
3979
  optional: {}
3720
3980
  }),
3981
+ contract({
3982
+ id: "local_extension_discovery",
3983
+ name: "Local Extension Discovery",
3984
+ description: "Local-only discovery report for extension manifests from config, project roots, .todos folders, and installed registry records.",
3985
+ surfaces: ["cli", "mcp", "sdk"],
3986
+ stability: "stable",
3987
+ required: {
3988
+ schema_version: field("integer", "Report schema version."),
3989
+ local_only: field("boolean", "Always true; discovery reads only local files and config."),
3990
+ no_network: field("boolean", "Always true; discovery performs no network requests."),
3991
+ project_path: field(["string", "null"], "Project root used for discovery, or null when omitted.", true),
3992
+ config_sources: field("array", "Resolved extension source paths from config and project discovery."),
3993
+ discovered: field("array", "Validated extension source inspections."),
3994
+ installed: field("array", "Installed local extension registry records when included."),
3995
+ warnings: field("array", "Non-fatal source read or validation warnings.")
3996
+ },
3997
+ optional: {}
3998
+ }),
3721
3999
  contract({
3722
4000
  id: "agent",
3723
4001
  name: "Agent",
@@ -5125,6 +5403,15 @@ var DEFAULT_SECRET_PATTERNS = [
5125
5403
  { name: "bearer-token", regex: /\b(bearer)\s+[A-Za-z0-9._~+/=-]{12,}/gi, replacement: "$1 [REDACTED]" }
5126
5404
  ];
5127
5405
  var DEFAULT_SECRET_KEY_PATTERN = /api[_-]?key|token|secret|password/i;
5406
+ var NON_SECRET_USAGE_KEYS = new Set([
5407
+ "tokens",
5408
+ "total_tokens",
5409
+ "token_count",
5410
+ "input_tokens",
5411
+ "output_tokens",
5412
+ "prompt_tokens",
5413
+ "completion_tokens"
5414
+ ]);
5128
5415
  function unique(values) {
5129
5416
  return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
5130
5417
  }
@@ -5144,6 +5431,8 @@ function secretPatterns() {
5144
5431
  return [...customPatterns(), ...DEFAULT_SECRET_PATTERNS];
5145
5432
  }
5146
5433
  function isSecretKey(key) {
5434
+ if (NON_SECRET_USAGE_KEYS.has(key.toLowerCase()))
5435
+ return false;
5147
5436
  if (DEFAULT_SECRET_KEY_PATTERN.test(key))
5148
5437
  return true;
5149
5438
  return unique(loadConfig().secret_safety?.redaction_keys).some((pattern) => key.toLowerCase().includes(pattern.toLowerCase()));
@@ -9248,7 +9537,18 @@ function addTaskRunCommand(input, db) {
9248
9537
  run_id: run.id,
9249
9538
  event_type: "command",
9250
9539
  message: `${status}: ${command}`,
9251
- data: { command, status, exit_code: input.exit_code ?? null, output_summary: outputSummary, artifact_path: artifactPath },
9540
+ data: {
9541
+ command,
9542
+ status,
9543
+ exit_code: input.exit_code ?? null,
9544
+ output_summary: outputSummary,
9545
+ artifact_path: artifactPath,
9546
+ usage: {
9547
+ tokens: input.tokens ?? null,
9548
+ cost_usd: input.cost_usd ?? null,
9549
+ duration_ms: input.duration_ms ?? null
9550
+ }
9551
+ },
9252
9552
  agent_id: input.agent_id ?? run.agent_id ?? undefined,
9253
9553
  created_at: timestamp
9254
9554
  }, d);
@@ -10618,9 +10918,305 @@ function importOnboardingFixture(options = {}) {
10618
10918
  conflictStrategy: options.conflictStrategy
10619
10919
  });
10620
10920
  }
10621
- // src/lib/local-snapshots.ts
10921
+ // src/lib/local-backups.ts
10622
10922
  init_database();
10623
10923
  import { createHash as createHash3 } from "crypto";
10924
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
10925
+ import { dirname as dirname6, resolve as resolve7 } from "path";
10926
+ import { mkdirSync as mkdirSync6 } from "fs";
10927
+ var TODOS_LOCAL_BACKUP_KIND = "hasna.todos.local-backup";
10928
+ var TODOS_LOCAL_BACKUP_SCHEMA_VERSION = 1;
10929
+ var TODOS_LOCAL_INTEGRITY_KIND = "hasna.todos.local-integrity";
10930
+ var TODOS_LOCAL_INTEGRITY_SCHEMA_VERSION = 1;
10931
+ var LOCAL_BACKUP_CHECKSUM_ALGORITHM = "sha256";
10932
+ function stableJson(value) {
10933
+ if (value === null || typeof value !== "object")
10934
+ return JSON.stringify(value);
10935
+ if (Array.isArray(value))
10936
+ return `[${value.map(stableJson).join(",")}]`;
10937
+ const record = value;
10938
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
10939
+ }
10940
+ function sha2562(value) {
10941
+ return createHash3("sha256").update(stableJson(value)).digest("hex");
10942
+ }
10943
+ function sqliteIntegrity(db) {
10944
+ let quick = "unknown";
10945
+ try {
10946
+ const row = db.query("PRAGMA quick_check").get();
10947
+ quick = row?.quick_check ?? "unknown";
10948
+ } catch (error) {
10949
+ quick = error instanceof Error ? error.message : String(error);
10950
+ }
10951
+ let foreignKeyViolations = 0;
10952
+ try {
10953
+ foreignKeyViolations = db.query("PRAGMA foreign_key_check").all().length;
10954
+ } catch {
10955
+ foreignKeyViolations = 0;
10956
+ }
10957
+ return {
10958
+ quick_check: quick,
10959
+ foreign_key_violations: foreignKeyViolations,
10960
+ ok: quick === "ok" && foreignKeyViolations === 0
10961
+ };
10962
+ }
10963
+ function bridgeStats2(data) {
10964
+ return Object.fromEntries(Object.keys(data).map((key) => [key, data[key].length]));
10965
+ }
10966
+ function sectionChecksums(data) {
10967
+ return Object.fromEntries(Object.keys(data).map((key) => [key, sha2562(data[key])]));
10968
+ }
10969
+ function checksumPayload(bundle) {
10970
+ return sha2562(bundle);
10971
+ }
10972
+ function asRecord(value) {
10973
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
10974
+ }
10975
+ function createLocalBackup(options = {}, db) {
10976
+ const d = db || getDatabase();
10977
+ const createdAt2 = options.generated_at ?? now();
10978
+ const bridge = createLocalBridgeBundle({
10979
+ project_id: options.project_id,
10980
+ generatedAt: createdAt2,
10981
+ version: options.version
10982
+ }, d);
10983
+ const integrity = sqliteIntegrity(d);
10984
+ const bridgeChecksum = sha2562(bridge);
10985
+ const warnings = [];
10986
+ if (!integrity.ok)
10987
+ warnings.push("current SQLite integrity check did not pass");
10988
+ const manifest = {
10989
+ schema_version: TODOS_LOCAL_BACKUP_SCHEMA_VERSION,
10990
+ kind: TODOS_LOCAL_BACKUP_KIND,
10991
+ local_only: true,
10992
+ no_network: true,
10993
+ created_at: createdAt2,
10994
+ package: bridge.package,
10995
+ source: bridge.source,
10996
+ bridge: {
10997
+ kind: TODOS_LOCAL_BRIDGE_KIND,
10998
+ schema_version: TODOS_LOCAL_BRIDGE_SCHEMA_VERSION,
10999
+ exported_at: bridge.exportedAt,
11000
+ checksum: bridgeChecksum,
11001
+ stats: bridge.stats,
11002
+ artifact_contents: bridge.artifact_contents?.length ?? 0
11003
+ },
11004
+ database: {
11005
+ path: getDatabasePath(),
11006
+ integrity
11007
+ },
11008
+ checksum_algorithm: LOCAL_BACKUP_CHECKSUM_ALGORITHM,
11009
+ section_checksums: sectionChecksums(bridge.data),
11010
+ warnings
11011
+ };
11012
+ const withoutChecksum = {
11013
+ schema_version: TODOS_LOCAL_BACKUP_SCHEMA_VERSION,
11014
+ kind: TODOS_LOCAL_BACKUP_KIND,
11015
+ local_only: true,
11016
+ no_network: true,
11017
+ created_at: createdAt2,
11018
+ package: bridge.package,
11019
+ manifest,
11020
+ bridge,
11021
+ checksum_algorithm: LOCAL_BACKUP_CHECKSUM_ALGORITHM
11022
+ };
11023
+ const backup = {
11024
+ ...withoutChecksum,
11025
+ checksum: checksumPayload(withoutChecksum)
11026
+ };
11027
+ if (options.output_path)
11028
+ writeLocalBackupFile(backup, options.output_path);
11029
+ return backup;
11030
+ }
11031
+ function writeLocalBackupFile(backup, outputPath) {
11032
+ const path = resolve7(outputPath);
11033
+ mkdirSync6(dirname6(path), { recursive: true });
11034
+ writeFileSync4(path, `${JSON.stringify(backup, null, 2)}
11035
+ `);
11036
+ return path;
11037
+ }
11038
+ function readLocalBackupFile(path) {
11039
+ return JSON.parse(readFileSync4(resolve7(path), "utf-8"));
11040
+ }
11041
+ function verifyLocalBackup(value, options = {}, db) {
11042
+ const verifiedAt = options.verified_at ?? now();
11043
+ const record = asRecord(value);
11044
+ const issues = [];
11045
+ const warnings = [];
11046
+ const bridge = record?.bridge;
11047
+ const manifest = asRecord(record?.manifest);
11048
+ const expectedChecksum = typeof record?.checksum === "string" ? record.checksum : null;
11049
+ const expectedBridgeChecksum = typeof manifest?.bridge === "object" && manifest.bridge && "checksum" in manifest.bridge ? String(manifest.bridge.checksum) : null;
11050
+ if (!record)
11051
+ issues.push("backup must be an object");
11052
+ if (record?.kind !== TODOS_LOCAL_BACKUP_KIND)
11053
+ issues.push(`kind must be ${TODOS_LOCAL_BACKUP_KIND}`);
11054
+ if (record?.schema_version !== TODOS_LOCAL_BACKUP_SCHEMA_VERSION) {
11055
+ issues.push(`schema_version must be ${TODOS_LOCAL_BACKUP_SCHEMA_VERSION}`);
11056
+ }
11057
+ if (record?.local_only !== true)
11058
+ issues.push("local_only must be true");
11059
+ if (record?.no_network !== true)
11060
+ issues.push("no_network must be true");
11061
+ if (!manifest)
11062
+ issues.push("manifest must be an object");
11063
+ if (!bridge)
11064
+ issues.push("bridge must be an object");
11065
+ const bridgeValidation = validateLocalBridgeBundle(bridge);
11066
+ if (!bridgeValidation.ok)
11067
+ issues.push(...bridgeValidation.issues.map((issue) => `bridge: ${issue}`));
11068
+ const actualBridgeChecksum = bridge ? sha2562(bridge) : null;
11069
+ if (expectedBridgeChecksum && actualBridgeChecksum && expectedBridgeChecksum !== actualBridgeChecksum) {
11070
+ issues.push("bridge checksum mismatch");
11071
+ }
11072
+ const withoutChecksum = record ? { ...record } : null;
11073
+ if (withoutChecksum)
11074
+ delete withoutChecksum.checksum;
11075
+ const actualChecksum = withoutChecksum ? checksumPayload(withoutChecksum) : null;
11076
+ if (expectedChecksum && actualChecksum && expectedChecksum !== actualChecksum) {
11077
+ issues.push("backup checksum mismatch");
11078
+ }
11079
+ const expectedCounts = manifest?.bridge && typeof manifest.bridge === "object" ? manifest.bridge.stats ?? {} : {};
11080
+ const actualCounts = bridge?.data ? bridgeStats2(bridge.data) : {};
11081
+ const countMismatches = Object.entries(expectedCounts).filter(([key, count]) => {
11082
+ const actual = actualCounts[key];
11083
+ return typeof count === "number" && actual !== count;
11084
+ });
11085
+ if (countMismatches.length > 0) {
11086
+ issues.push(`manifest count mismatch: ${countMismatches.map(([key]) => key).join(", ")}`);
11087
+ }
11088
+ if (manifest?.section_checksums && typeof manifest.section_checksums === "object" && bridge?.data) {
11089
+ const actualSections = sectionChecksums(bridge.data);
11090
+ const mismatches = Object.entries(manifest.section_checksums).filter(([key, expected]) => actualSections[key] !== expected);
11091
+ if (mismatches.length > 0)
11092
+ issues.push(`section checksum mismatch: ${mismatches.map(([key]) => key).join(", ")}`);
11093
+ }
11094
+ if (bridge?.schemaVersion !== TODOS_LOCAL_BRIDGE_SCHEMA_VERSION) {
11095
+ issues.push(`bridge schemaVersion must be ${TODOS_LOCAL_BRIDGE_SCHEMA_VERSION}`);
11096
+ }
11097
+ const sqlite = options.check_sqlite === false ? null : sqliteIntegrity(db || getDatabase());
11098
+ if (sqlite && !sqlite.ok)
11099
+ warnings.push("current SQLite integrity check did not pass");
11100
+ return {
11101
+ schema_version: TODOS_LOCAL_BACKUP_SCHEMA_VERSION,
11102
+ kind: "hasna.todos.local-backup-verification",
11103
+ local_only: true,
11104
+ no_network: true,
11105
+ verified_at: verifiedAt,
11106
+ ok: issues.length === 0,
11107
+ checksum_algorithm: LOCAL_BACKUP_CHECKSUM_ALGORITHM,
11108
+ checksum: {
11109
+ expected: expectedChecksum,
11110
+ actual: actualChecksum,
11111
+ ok: Boolean(expectedChecksum && actualChecksum && expectedChecksum === actualChecksum)
11112
+ },
11113
+ bridge_checksum: {
11114
+ expected: expectedBridgeChecksum,
11115
+ actual: actualBridgeChecksum,
11116
+ ok: Boolean(expectedBridgeChecksum && actualBridgeChecksum && expectedBridgeChecksum === actualBridgeChecksum)
11117
+ },
11118
+ bridge_validation: bridgeValidation,
11119
+ sqlite,
11120
+ counts: {
11121
+ expected: expectedCounts,
11122
+ actual: actualCounts,
11123
+ ok: countMismatches.length === 0
11124
+ },
11125
+ compatible: bridge?.schemaVersion === TODOS_LOCAL_BRIDGE_SCHEMA_VERSION,
11126
+ issues,
11127
+ warnings
11128
+ };
11129
+ }
11130
+ function restoreLocalBackup(backup, options = {}, db) {
11131
+ const d = db || getDatabase();
11132
+ const verification = verifyLocalBackup(backup, { verified_at: options.verified_at, check_sqlite: true }, d);
11133
+ const issues = [...verification.issues];
11134
+ let importResult = null;
11135
+ if (verification.ok) {
11136
+ importResult = importLocalBridgeBundle(backup.bridge, {
11137
+ dryRun: !options.apply,
11138
+ conflictStrategy: options.conflict_strategy ?? "skip"
11139
+ }, d);
11140
+ if (!importResult.ok)
11141
+ issues.push(...importResult.issues);
11142
+ }
11143
+ return {
11144
+ schema_version: TODOS_LOCAL_BACKUP_SCHEMA_VERSION,
11145
+ kind: "hasna.todos.local-backup-restore",
11146
+ local_only: true,
11147
+ no_network: true,
11148
+ restored_at: options.verified_at ?? now(),
11149
+ dry_run: !options.apply,
11150
+ ok: verification.ok && Boolean(importResult?.ok),
11151
+ verification,
11152
+ import_result: importResult,
11153
+ issues
11154
+ };
11155
+ }
11156
+ function count(db, table) {
11157
+ const row = db.query(`SELECT COUNT(*) AS count FROM ${table}`).get();
11158
+ return row?.count ?? 0;
11159
+ }
11160
+ function orphanedRows(db) {
11161
+ return {
11162
+ 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)"),
11163
+ 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)"),
11164
+ 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)"),
11165
+ 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)"),
11166
+ 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)"),
11167
+ 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)"),
11168
+ 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)"),
11169
+ 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)")
11170
+ };
11171
+ }
11172
+ function countQuery(db, sql) {
11173
+ try {
11174
+ const row = db.query(sql).get();
11175
+ return row?.count ?? 0;
11176
+ } catch {
11177
+ return 0;
11178
+ }
11179
+ }
11180
+ function checkLocalIntegrity(options = {}, db) {
11181
+ const d = db || getDatabase();
11182
+ const bridge = createLocalBridgeBundle({
11183
+ project_id: options.project_id,
11184
+ generatedAt: options.generated_at,
11185
+ version: options.version ?? getPackageVersion(import.meta.url)
11186
+ }, d);
11187
+ const bridgeValidation = validateLocalBridgeBundle(bridge);
11188
+ const sqlite = sqliteIntegrity(d);
11189
+ const orphans = orphanedRows(d);
11190
+ const issues = [];
11191
+ const warnings = [];
11192
+ if (!sqlite.ok)
11193
+ issues.push("SQLite integrity check failed");
11194
+ if (!bridgeValidation.ok)
11195
+ issues.push(...bridgeValidation.issues.map((issue) => `bridge: ${issue}`));
11196
+ const orphanTotal = Object.values(orphans).reduce((sum, value) => sum + value, 0);
11197
+ if (orphanTotal > 0)
11198
+ issues.push(`${orphanTotal} orphaned local row(s) detected`);
11199
+ if (count(d, "tasks") === 0)
11200
+ warnings.push("no tasks found in local store");
11201
+ return {
11202
+ schema_version: TODOS_LOCAL_INTEGRITY_SCHEMA_VERSION,
11203
+ kind: TODOS_LOCAL_INTEGRITY_KIND,
11204
+ local_only: true,
11205
+ no_network: true,
11206
+ generated_at: options.generated_at ?? now(),
11207
+ database_path: getDatabasePath(),
11208
+ sqlite,
11209
+ bridge_validation: bridgeValidation,
11210
+ counts: bridge.stats,
11211
+ orphaned_rows: orphans,
11212
+ ok: issues.length === 0,
11213
+ issues,
11214
+ warnings
11215
+ };
11216
+ }
11217
+ // src/lib/local-snapshots.ts
11218
+ init_database();
11219
+ import { createHash as createHash4 } from "crypto";
10624
11220
 
10625
11221
  // src/lib/activity-timeline.ts
10626
11222
  init_database();
@@ -10894,8 +11490,8 @@ function stable(value) {
10894
11490
  return value;
10895
11491
  return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, stable(item)]));
10896
11492
  }
10897
- function sha2562(value) {
10898
- return createHash3("sha256").update(JSON.stringify(stable(value))).digest("hex");
11493
+ function sha2563(value) {
11494
+ return createHash4("sha256").update(JSON.stringify(stable(value))).digest("hex");
10899
11495
  }
10900
11496
  function latestTimestamp(items, fallback) {
10901
11497
  const timestamps = [];
@@ -11075,7 +11671,7 @@ function getLocalSnapshot(options, db) {
11075
11671
  package: source3(getPackageVersion(import.meta.url)),
11076
11672
  filters: body.filters,
11077
11673
  cursor,
11078
- fingerprint: sha2562(body),
11674
+ fingerprint: sha2563(body),
11079
11675
  count: items.length,
11080
11676
  items,
11081
11677
  resources: {
@@ -11132,7 +11728,7 @@ function renderLocalSnapshotMarkdown(snapshot) {
11132
11728
  `;
11133
11729
  }
11134
11730
  // src/lib/sdk-integration-fixtures.ts
11135
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync4 } from "fs";
11731
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync5 } from "fs";
11136
11732
  import { join as join7 } from "path";
11137
11733
 
11138
11734
  // src/cli-mcp-parity.ts
@@ -11313,6 +11909,56 @@ var TODOS_CLI_MCP_PARITY = [
11313
11909
  mcpTool: "get_agent_reliability_scorecard"
11314
11910
  }
11315
11911
  },
11912
+ {
11913
+ domain: "local-reports",
11914
+ cliCommands: [
11915
+ "todos reports local"
11916
+ ],
11917
+ mcpTools: [
11918
+ "list_local_report_types",
11919
+ "build_local_report"
11920
+ ],
11921
+ jsonContracts: ["local_report", "task", "structured_error", "api_error"],
11922
+ errorContracts: ["structured_error", "api_error"],
11923
+ status: "matched",
11924
+ intentionalGaps: [],
11925
+ example: {
11926
+ cli: "todos reports local --agent codex --format markdown",
11927
+ mcpTool: "build_local_report"
11928
+ }
11929
+ },
11930
+ {
11931
+ domain: "local-backups",
11932
+ cliCommands: [
11933
+ "todos backup create",
11934
+ "todos backup verify",
11935
+ "todos backup restore",
11936
+ "todos backup integrity"
11937
+ ],
11938
+ mcpTools: [
11939
+ "create_local_backup",
11940
+ "verify_local_backup",
11941
+ "restore_local_backup",
11942
+ "check_local_integrity"
11943
+ ],
11944
+ jsonContracts: [
11945
+ "local_backup_bundle",
11946
+ "local_backup_verification",
11947
+ "local_backup_restore_result",
11948
+ "local_integrity_report",
11949
+ "local_bridge_bundle",
11950
+ "local_bridge_import_result",
11951
+ "structured_error",
11952
+ "api_error"
11953
+ ],
11954
+ errorContracts: ["structured_error", "api_error"],
11955
+ status: "matched",
11956
+ intentionalGaps: [],
11957
+ example: {
11958
+ cli: "todos backup create --output todos-backup.json --json",
11959
+ mcpTool: "create_local_backup"
11960
+ }
11961
+ },
11316
11962
  {
11317
11963
  domain: "local-fields",
11318
11964
  cliCommands: [
@@ -11334,6 +11980,29 @@ var TODOS_CLI_MCP_PARITY = [
11334
11980
  mcpTool: "set_task_fields"
11335
11981
  }
11336
11982
  },
11983
+ {
11984
+ domain: "workflow-states",
11985
+ cliCommands: [
11986
+ "todos workflow states",
11987
+ "todos workflow set",
11988
+ "todos workflow tasks",
11989
+ "todos workflow migrate"
11990
+ ],
11991
+ mcpTools: [
11992
+ "list_workflow_states",
11993
+ "set_task_workflow_state",
11994
+ "query_tasks_by_workflow_state",
11995
+ "migrate_workflow_states"
11996
+ ],
11997
+ jsonContracts: ["workflow_state_config", "workflow_state_result", "workflow_state_migration", "task", "structured_error", "api_error"],
11998
+ errorContracts: ["structured_error", "api_error"],
11999
+ status: "matched",
12000
+ intentionalGaps: [],
12001
+ example: {
12002
+ cli: "todos workflow set 1234abcd review --json",
12003
+ mcpTool: "set_task_workflow_state"
12004
+ }
12005
+ },
11337
12006
  {
11338
12007
  domain: "dedupe",
11339
12008
  cliCommands: [
@@ -11528,6 +12197,51 @@ var TODOS_CLI_MCP_PARITY = [
11528
12197
  mcpTool: "check_release_compatibility"
11529
12198
  }
11530
12199
  },
12200
+ {
12201
+ domain: "usage-ledger",
12202
+ cliCommands: ["todos usage report"],
12203
+ mcpTools: ["get_usage_ledger"],
12204
+ jsonContracts: ["local_usage_ledger", "structured_error", "api_error"],
12205
+ errorContracts: ["structured_error", "api_error"],
12206
+ status: "matched",
12207
+ intentionalGaps: [],
12208
+ example: {
12209
+ cli: "todos usage report --agent codex --max-tasks 1000 --json",
12210
+ mcpTool: "get_usage_ledger"
12211
+ }
12212
+ },
12213
+ {
12214
+ domain: "terminal-dashboard",
12215
+ cliCommands: ["todos dashboard", "todos dashboard --snapshot"],
12216
+ mcpTools: [],
12217
+ jsonContracts: ["terminal_dashboard_snapshot", "structured_error", "api_error"],
12218
+ errorContracts: ["structured_error", "api_error"],
12219
+ status: "intentional-gap",
12220
+ intentionalGaps: [{
12221
+ cliCommand: "todos dashboard",
12222
+ 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."
12223
+ }],
12224
+ gapReason: "Terminal UI keyboard navigation is not an MCP interaction model.",
12225
+ example: {
12226
+ cli: "todos dashboard --snapshot --view tasks --search release --json"
12227
+ }
12228
+ },
12229
+ {
12230
+ domain: "scale-hardening",
12231
+ cliCommands: ["todos scale report", "todos scale compact"],
12232
+ mcpTools: [],
12233
+ jsonContracts: ["scale_performance_report", "scale_compaction_result", "structured_error", "api_error"],
12234
+ errorContracts: ["structured_error", "api_error"],
12235
+ status: "intentional-gap",
12236
+ intentionalGaps: [{
12237
+ cliCommand: "todos scale compact",
12238
+ 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."
12239
+ }],
12240
+ gapReason: "Scale diagnostics and compaction are local operator maintenance commands, while MCP tools should use domain-specific task, run, and doctor APIs.",
12241
+ example: {
12242
+ cli: "todos scale report --older-than-days 30 --json"
12243
+ }
12244
+ },
11531
12245
  {
11532
12246
  domain: "templates",
11533
12247
  cliCommands: [
@@ -11651,6 +12365,7 @@ var TODOS_CLI_MCP_PARITY = [
11651
12365
  domain: "extensions",
11652
12366
  cliCommands: [
11653
12367
  "todos extensions list",
12368
+ "todos extensions discover",
11654
12369
  "todos extensions inspect",
11655
12370
  "todos extensions compat",
11656
12371
  "todos extensions install",
@@ -11659,12 +12374,13 @@ var TODOS_CLI_MCP_PARITY = [
11659
12374
  ],
11660
12375
  mcpTools: [
11661
12376
  "list_local_extensions",
12377
+ "discover_local_extensions",
11662
12378
  "inspect_local_extension",
11663
12379
  "test_local_extension_compatibility",
11664
12380
  "install_local_extension",
11665
12381
  "remove_local_extension"
11666
12382
  ],
11667
- jsonContracts: ["local_extension_compatibility", "structured_error", "api_error"],
12383
+ jsonContracts: ["local_extension_compatibility", "local_extension_discovery", "structured_error", "api_error"],
11668
12384
  errorContracts: ["structured_error", "api_error"],
11669
12385
  status: "matched",
11670
12386
  example: {
@@ -12950,7 +13666,7 @@ function createSdkIntegrationFixturePack(options = {}) {
12950
13666
  };
12951
13667
  }
12952
13668
  function writeSdkIntegrationFixtures(directory, options = {}) {
12953
- mkdirSync6(directory, { recursive: true });
13669
+ mkdirSync7(directory, { recursive: true });
12954
13670
  const pack = createSdkIntegrationFixturePack(options);
12955
13671
  const bundle = getOnboardingFixtureBundle("agent-project-demo");
12956
13672
  const files = [
@@ -12962,7 +13678,7 @@ function writeSdkIntegrationFixtures(directory, options = {}) {
12962
13678
  const written = [];
12963
13679
  for (const [name, payload] of files) {
12964
13680
  const file = join7(directory, name);
12965
- writeFileSync4(file, `${JSON.stringify(payload, null, 2)}
13681
+ writeFileSync5(file, `${JSON.stringify(payload, null, 2)}
12966
13682
  `, "utf-8");
12967
13683
  written.push(file);
12968
13684
  }
@@ -13948,7 +14664,7 @@ function renderRoadmapMarkdown(idOrName, db) {
13948
14664
  }
13949
14665
  // src/lib/audit-ledger.ts
13950
14666
  init_database();
13951
- import { createHash as createHash4 } from "crypto";
14667
+ import { createHash as createHash5 } from "crypto";
13952
14668
  var LOCAL_AUDIT_LEDGER_SCHEMA_VERSION = 1;
13953
14669
  var LOCAL_AUDIT_LEDGER_HASH_ALGORITHM = "sha256";
13954
14670
  var LOCAL_AUDIT_LEDGER_INITIAL_HASH = "0".repeat(64);
@@ -13961,7 +14677,7 @@ function canonicalize(value) {
13961
14677
  return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalize(object[key])}`).join(",")}}`;
13962
14678
  }
13963
14679
  function hash(value) {
13964
- return createHash4("sha256").update(value).digest("hex");
14680
+ return createHash5("sha256").update(value).digest("hex");
13965
14681
  }
13966
14682
  function parsePayload(value) {
13967
14683
  if (!value)
@@ -14220,15 +14936,15 @@ function renderLocalAuditLedgerMarkdown(ledger) {
14220
14936
  `Scope: ${ledger.run_id ?? ledger.task_id ?? ledger.project_id ?? "all local evidence"}`,
14221
14937
  `Root hash: ${ledger.root_hash}`,
14222
14938
  `Entries: ${ledger.entry_count}`,
14223
- `Sources: ${Object.entries(ledger.source_counts).map(([source6, count]) => `${source6}=${count}`).join(", ") || "none"}`
14939
+ `Sources: ${Object.entries(ledger.source_counts).map(([source6, count2]) => `${source6}=${count2}`).join(", ") || "none"}`
14224
14940
  ].join(`
14225
14941
  `);
14226
14942
  }
14227
14943
  // src/lib/release-compatibility.ts
14228
14944
  init_migrations();
14229
14945
  init_schema();
14230
- import { readFileSync as readFileSync4 } from "fs";
14231
- import { join as join8, resolve as resolve7 } from "path";
14946
+ import { readFileSync as readFileSync5 } from "fs";
14947
+ import { join as join8, resolve as resolve8 } from "path";
14232
14948
  import { Database as Database2 } from "bun:sqlite";
14233
14949
  var LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION = 1;
14234
14950
  var EXPECTED_PACKAGE_NAME = "@hasna/todos";
@@ -14274,7 +14990,7 @@ function warn(id, message, details) {
14274
14990
  return { id, status: "warning", message, details };
14275
14991
  }
14276
14992
  function readPackageJson(root) {
14277
- return JSON.parse(readFileSync4(join8(root, "package.json"), "utf8"));
14993
+ return JSON.parse(readFileSync5(join8(root, "package.json"), "utf8"));
14278
14994
  }
14279
14995
  function sortedKeys(value) {
14280
14996
  return Object.keys(value ?? {}).sort((left, right) => left.localeCompare(right));
@@ -14370,7 +15086,7 @@ function checkChangelog() {
14370
15086
  ];
14371
15087
  }
14372
15088
  function createReleaseCompatibilityReport(options = {}) {
14373
- const root = resolve7(options.root ?? process.cwd());
15089
+ const root = resolve8(options.root ?? process.cwd());
14374
15090
  const packageJson = readPackageJson(root);
14375
15091
  const simulatedLevels = options.simulated_levels ?? defaultSimulationLevels();
14376
15092
  const checks = [
@@ -14481,7 +15197,7 @@ function renderReleaseCompatibilityMarkdown(report) {
14481
15197
  `);
14482
15198
  }
14483
15199
  // src/db/inbox.ts
14484
- import { createHash as createHash5 } from "crypto";
15200
+ import { createHash as createHash6 } from "crypto";
14485
15201
 
14486
15202
  // src/lib/github.ts
14487
15203
  import { execFileSync } from "child_process";
@@ -14564,7 +15280,7 @@ function compactWhitespace(value) {
14564
15280
  function fingerprintInboxInput(input) {
14565
15281
  const sourceType = input.source_type || detectInboxSourceType(input.body, input.source_url);
14566
15282
  const normalized = compactWhitespace(redactEvidenceText(input.body)).slice(0, 8000);
14567
- return createHash5("sha256").update(`${sourceType}
15283
+ return createHash6("sha256").update(`${sourceType}
14568
15284
  ${input.source_url || ""}
14569
15285
  ${normalized}`).digest("hex");
14570
15286
  }
@@ -16094,8 +16810,755 @@ async function checkLocalNotifications(input = {}, db) {
16094
16810
  warnings
16095
16811
  };
16096
16812
  }
16813
+ // src/lib/usage-ledger.ts
16814
+ init_database();
16815
+ var LOCAL_USAGE_LEDGER_SCHEMA_VERSION = 1;
16816
+ function numberValue(value) {
16817
+ if (typeof value === "number" && Number.isFinite(value))
16818
+ return value;
16819
+ if (typeof value === "string" && value.trim() !== "") {
16820
+ const parsed = Number(value);
16821
+ if (Number.isFinite(parsed))
16822
+ return parsed;
16823
+ }
16824
+ return 0;
16825
+ }
16826
+ function sumDirectNumber(record, keys) {
16827
+ let value = 0;
16828
+ for (const [rawKey, rawValue] of Object.entries(record)) {
16829
+ const key = rawKey.toLowerCase();
16830
+ if (keys.includes(key))
16831
+ value += Math.max(0, numberValue(rawValue));
16832
+ }
16833
+ return value;
16834
+ }
16835
+ function maxDirectNumber(record, keys) {
16836
+ let value = 0;
16837
+ for (const [rawKey, rawValue] of Object.entries(record)) {
16838
+ const key = rawKey.toLowerCase();
16839
+ if (keys.includes(key))
16840
+ value = Math.max(value, numberValue(rawValue));
16841
+ }
16842
+ return Math.max(0, value);
16843
+ }
16844
+ function extractUsage(value) {
16845
+ if (!value || typeof value !== "object")
16846
+ return { tokens: 0, cost_usd: 0, duration_ms: 0, records: 0 };
16847
+ if (Array.isArray(value)) {
16848
+ return value.reduce((acc, item) => {
16849
+ const usage = extractUsage(item);
16850
+ acc.tokens += usage.tokens;
16851
+ acc.cost_usd += usage.cost_usd;
16852
+ acc.duration_ms += usage.duration_ms;
16853
+ acc.records += usage.records;
16854
+ return acc;
16855
+ }, { tokens: 0, cost_usd: 0, duration_ms: 0, records: 0 });
16856
+ }
16857
+ const record = value;
16858
+ const explicitTokens = maxDirectNumber(record, ["tokens", "total_tokens", "token_count"]);
16859
+ const splitTokens2 = sumDirectNumber(record, ["input_tokens", "output_tokens", "prompt_tokens", "completion_tokens"]);
16860
+ const cost = maxDirectNumber(record, ["cost_usd", "usd", "price_usd", "amount_usd", "cost"]);
16861
+ const duration = maxDirectNumber(record, ["duration_ms", "elapsed_ms", "latency_ms"]);
16862
+ const own = {
16863
+ tokens: explicitTokens || splitTokens2,
16864
+ cost_usd: cost,
16865
+ duration_ms: duration,
16866
+ records: explicitTokens || splitTokens2 || cost || duration ? 1 : 0
16867
+ };
16868
+ for (const [key, child] of Object.entries(record)) {
16869
+ 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())) {
16870
+ continue;
16871
+ }
16872
+ const nested = extractUsage(child);
16873
+ own.tokens += nested.tokens;
16874
+ own.cost_usd += nested.cost_usd;
16875
+ own.duration_ms += nested.duration_ms;
16876
+ own.records += nested.records;
16877
+ }
16878
+ return own;
16879
+ }
16880
+ function parseJsonObject4(value) {
16881
+ if (!value)
16882
+ return {};
16883
+ try {
16884
+ const parsed = JSON.parse(value);
16885
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
16886
+ } catch {
16887
+ return {};
16888
+ }
16889
+ }
16890
+ function millisBetween(start, end) {
16891
+ if (!start || !end)
16892
+ return 0;
16893
+ const startMs = Date.parse(start);
16894
+ const endMs = Date.parse(end);
16895
+ if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs < startMs)
16896
+ return 0;
16897
+ return endMs - startMs;
16898
+ }
16899
+ function addTaskScope(where, params, options, alias = "t") {
16900
+ if (options.project_id) {
16901
+ where.push(`${alias}.project_id = ?`);
16902
+ params.push(options.project_id);
16903
+ }
16904
+ if (options.agent_id) {
16905
+ where.push(`(${alias}.agent_id = ? OR ${alias}.assigned_to = ?)`);
16906
+ params.push(options.agent_id, options.agent_id);
16907
+ }
16908
+ if (options.since) {
16909
+ where.push(`${alias}.created_at >= ?`);
16910
+ params.push(options.since);
16911
+ }
16912
+ if (options.until) {
16913
+ where.push(`${alias}.created_at <= ?`);
16914
+ params.push(options.until);
16915
+ }
16916
+ }
16917
+ function addRunScope(where, params, options, runAlias = "r", taskAlias = "t") {
16918
+ if (options.project_id) {
16919
+ where.push(`${taskAlias}.project_id = ?`);
16920
+ params.push(options.project_id);
16921
+ }
16922
+ if (options.agent_id) {
16923
+ where.push(`(${runAlias}.agent_id = ? OR ${taskAlias}.agent_id = ? OR ${taskAlias}.assigned_to = ?)`);
16924
+ params.push(options.agent_id, options.agent_id, options.agent_id);
16925
+ }
16926
+ if (options.since) {
16927
+ where.push(`${runAlias}.started_at >= ?`);
16928
+ params.push(options.since);
16929
+ }
16930
+ if (options.until) {
16931
+ where.push(`${runAlias}.started_at <= ?`);
16932
+ params.push(options.until);
16933
+ }
16934
+ }
16935
+ function addTraceScope(where, params, options, traceAlias = "tr", taskAlias = "t") {
16936
+ if (options.project_id) {
16937
+ where.push(`${taskAlias}.project_id = ?`);
16938
+ params.push(options.project_id);
16939
+ }
16940
+ if (options.agent_id) {
16941
+ where.push(`(${traceAlias}.agent_id = ? OR ${taskAlias}.agent_id = ? OR ${taskAlias}.assigned_to = ?)`);
16942
+ params.push(options.agent_id, options.agent_id, options.agent_id);
16943
+ }
16944
+ if (options.since) {
16945
+ where.push(`${traceAlias}.created_at >= ?`);
16946
+ params.push(options.since);
16947
+ }
16948
+ if (options.until) {
16949
+ where.push(`${traceAlias}.created_at <= ?`);
16950
+ params.push(options.until);
16951
+ }
16952
+ }
16953
+ function queryOne(db, sql, params) {
16954
+ return db.query(sql).get(...params);
16955
+ }
16956
+ function queryAll(db, sql, params) {
16957
+ return db.query(sql).all(...params);
16958
+ }
16959
+ function rounded(value, places = 6) {
16960
+ if (!Number.isFinite(value))
16961
+ return 0;
16962
+ const factor = 10 ** places;
16963
+ return Math.round(value * factor) / factor;
16964
+ }
16965
+ function quotaLimit(name, limit, used) {
16966
+ if (limit === undefined || !Number.isFinite(limit) || limit < 0)
16967
+ return null;
16968
+ const normalized = name === "max_cost_usd" ? rounded(limit) : Math.floor(limit);
16969
+ const roundedUsed = name === "max_cost_usd" ? rounded(used) : Math.floor(used);
16970
+ return {
16971
+ name,
16972
+ limit: normalized,
16973
+ used: roundedUsed,
16974
+ remaining: rounded(normalized - roundedUsed),
16975
+ exceeded: roundedUsed > normalized
16976
+ };
16977
+ }
16978
+ function buildQuota(options, report) {
16979
+ const quotas = options.quotas || {};
16980
+ const limits2 = [
16981
+ quotaLimit("max_tasks", quotas.max_tasks, report.counts.tasks),
16982
+ quotaLimit("max_projects", quotas.max_projects, report.counts.projects),
16983
+ quotaLimit("max_runs", quotas.max_runs, report.counts.runs),
16984
+ quotaLimit("max_commands", quotas.max_commands, report.counts.commands),
16985
+ quotaLimit("max_tokens", quotas.max_tokens, report.usage.total_tokens),
16986
+ quotaLimit("max_cost_usd", quotas.max_cost_usd, report.usage.total_cost_usd),
16987
+ quotaLimit("max_storage_bytes", quotas.max_storage_bytes, report.storage.evidence_bytes)
16988
+ ].filter((item) => Boolean(item));
16989
+ const exceeded = limits2.filter((item) => item.exceeded).map((item) => item.name);
16990
+ return {
16991
+ simulated: limits2.length > 0,
16992
+ limits: limits2,
16993
+ exceeded,
16994
+ allowed: exceeded.length === 0
16995
+ };
16996
+ }
16997
+ function createLocalUsageLedger(options = {}, db) {
16998
+ const d = db || getDatabase();
16999
+ const generatedAt = options.generated_at || new Date().toISOString();
17000
+ const taskWhere = [];
17001
+ const taskParams = [];
17002
+ addTaskScope(taskWhere, taskParams, options);
17003
+ const taskClause = taskWhere.length ? `WHERE ${taskWhere.join(" AND ")}` : "";
17004
+ 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);
17005
+ let projectCount = 0;
17006
+ if (options.project_id) {
17007
+ projectCount = queryOne(d, "SELECT COUNT(*) as count FROM projects WHERE id = ?", [options.project_id]).count;
17008
+ } else if (options.agent_id) {
17009
+ const projectWhere = ["t.project_id IS NOT NULL"];
17010
+ const projectParams = [];
17011
+ addTaskScope(projectWhere, projectParams, options);
17012
+ projectCount = queryOne(d, `SELECT COUNT(DISTINCT t.project_id) as count FROM tasks t WHERE ${projectWhere.join(" AND ")}`, projectParams).count;
17013
+ } else {
17014
+ projectCount = queryOne(d, "SELECT COUNT(*) as count FROM projects", []).count;
17015
+ }
17016
+ const runWhere = [];
17017
+ const runParams = [];
17018
+ addRunScope(runWhere, runParams, options);
17019
+ const runClause = runWhere.length ? `WHERE ${runWhere.join(" AND ")}` : "";
17020
+ const runs = queryAll(d, `SELECT r.id, r.started_at, r.completed_at, r.metadata
17021
+ FROM task_runs r JOIN tasks t ON t.id = r.task_id
17022
+ ${runClause}`, runParams);
17023
+ const commandTotals = queryOne(d, `SELECT COUNT(*) as commands
17024
+ FROM task_run_commands c
17025
+ JOIN task_runs r ON r.id = c.run_id
17026
+ JOIN tasks t ON t.id = c.task_id
17027
+ ${runClause}`, runParams);
17028
+ const artifactTotals = queryOne(d, `SELECT COUNT(*) as artifacts, COALESCE(SUM(a.size_bytes), 0) as bytes
17029
+ FROM task_run_artifacts a
17030
+ JOIN task_runs r ON r.id = a.run_id
17031
+ JOIN tasks t ON t.id = a.task_id
17032
+ ${runClause}`, runParams);
17033
+ const traceWhere = [];
17034
+ const traceParams = [];
17035
+ addTraceScope(traceWhere, traceParams, options);
17036
+ const traceClause = traceWhere.length ? `WHERE ${traceWhere.join(" AND ")}` : "";
17037
+ const traceTotals = queryOne(d, `SELECT COUNT(*) as traces,
17038
+ COALESCE(SUM(tr.tokens), 0) as tokens,
17039
+ COALESCE(SUM(tr.cost_usd), 0) as cost_usd,
17040
+ COALESCE(SUM(tr.duration_ms), 0) as duration_ms
17041
+ FROM task_traces tr
17042
+ JOIN tasks t ON t.id = tr.task_id
17043
+ ${traceClause}`, traceParams);
17044
+ let completedRunMs = 0;
17045
+ let openRunMs = 0;
17046
+ let metadataUsage = { tokens: 0, cost_usd: 0, duration_ms: 0, records: 0 };
17047
+ for (const run of runs) {
17048
+ if (run.completed_at)
17049
+ completedRunMs += millisBetween(run.started_at, run.completed_at);
17050
+ else
17051
+ openRunMs += millisBetween(run.started_at, generatedAt);
17052
+ const usage = extractUsage(parseJsonObject4(run.metadata));
17053
+ metadataUsage.tokens += usage.tokens;
17054
+ metadataUsage.cost_usd += usage.cost_usd;
17055
+ metadataUsage.duration_ms += usage.duration_ms;
17056
+ metadataUsage.records += usage.records;
17057
+ }
17058
+ const eventRows = queryAll(d, `SELECT e.data
17059
+ FROM task_run_events e
17060
+ JOIN task_runs r ON r.id = e.run_id
17061
+ JOIN tasks t ON t.id = e.task_id
17062
+ ${runClause}`, runParams);
17063
+ for (const event of eventRows) {
17064
+ const usage = extractUsage(parseJsonObject4(event.data));
17065
+ metadataUsage.tokens += usage.tokens;
17066
+ metadataUsage.cost_usd += usage.cost_usd;
17067
+ metadataUsage.duration_ms += usage.duration_ms;
17068
+ metadataUsage.records += usage.records;
17069
+ }
17070
+ const taskTokens = Number(taskTotals.task_tokens || 0);
17071
+ const traceTokens = Number(traceTotals.tokens || 0);
17072
+ const metadataTokens = metadataUsage.tokens;
17073
+ const taskCost = Number(taskTotals.task_cost_usd || 0);
17074
+ const traceCost = Number(traceTotals.cost_usd || 0);
17075
+ const metadataCost = metadataUsage.cost_usd;
17076
+ const traceMs = Number(traceTotals.duration_ms || 0);
17077
+ const baseReport = {
17078
+ schema_version: LOCAL_USAGE_LEDGER_SCHEMA_VERSION,
17079
+ local_only: true,
17080
+ no_network: true,
17081
+ generated_at: generatedAt,
17082
+ scope: {
17083
+ project_id: options.project_id || null,
17084
+ agent_id: options.agent_id || null,
17085
+ since: options.since || null,
17086
+ until: options.until || null
17087
+ },
17088
+ counts: {
17089
+ tasks: Number(taskTotals.tasks || 0),
17090
+ projects: Number(projectCount || 0),
17091
+ runs: runs.length,
17092
+ commands: Number(commandTotals.commands || 0),
17093
+ artifacts: Number(artifactTotals.artifacts || 0),
17094
+ traces: Number(traceTotals.traces || 0),
17095
+ metadata_records: metadataUsage.records
17096
+ },
17097
+ durations: {
17098
+ completed_run_ms: completedRunMs,
17099
+ open_run_ms: openRunMs,
17100
+ trace_ms: traceMs,
17101
+ total_observed_ms: completedRunMs + openRunMs + traceMs + metadataUsage.duration_ms
17102
+ },
17103
+ usage: {
17104
+ task_tokens: taskTokens,
17105
+ trace_tokens: traceTokens,
17106
+ metadata_tokens: metadataTokens,
17107
+ total_tokens: taskTokens + traceTokens + metadataTokens,
17108
+ task_cost_usd: rounded(taskCost),
17109
+ trace_cost_usd: rounded(traceCost),
17110
+ metadata_cost_usd: rounded(metadataCost),
17111
+ total_cost_usd: rounded(taskCost + traceCost + metadataCost)
17112
+ },
17113
+ storage: {
17114
+ artifact_bytes: Number(artifactTotals.bytes || 0),
17115
+ evidence_bytes: Number(artifactTotals.bytes || 0)
17116
+ },
17117
+ redaction: {
17118
+ raw_commands_included: false,
17119
+ raw_artifact_paths_included: false,
17120
+ aggregate_only: true
17121
+ },
17122
+ sources: ["tasks", "projects", "task_runs", "task_run_commands", "task_run_artifacts", "task_run_events", "task_traces"]
17123
+ };
17124
+ return {
17125
+ ...baseReport,
17126
+ quota: buildQuota(options, baseReport)
17127
+ };
17128
+ }
17129
+ function renderLocalUsageLedgerMarkdown(report) {
17130
+ const minutes2 = (report.durations.total_observed_ms / 60000).toFixed(1);
17131
+ const lines = [
17132
+ "# Local Usage Ledger",
17133
+ "",
17134
+ `Generated: ${report.generated_at}`,
17135
+ `Scope: project=${report.scope.project_id || "all"} agent=${report.scope.agent_id || "all"}`,
17136
+ "",
17137
+ "## Counts",
17138
+ `- Tasks: ${report.counts.tasks}`,
17139
+ `- Projects: ${report.counts.projects}`,
17140
+ `- Runs: ${report.counts.runs}`,
17141
+ `- Commands: ${report.counts.commands}`,
17142
+ `- Artifacts: ${report.counts.artifacts}`,
17143
+ `- Traces: ${report.counts.traces}`,
17144
+ "",
17145
+ "## Usage",
17146
+ `- Tokens: ${report.usage.total_tokens}`,
17147
+ `- Cost USD: ${report.usage.total_cost_usd}`,
17148
+ `- Observed duration minutes: ${minutes2}`,
17149
+ `- Evidence bytes: ${report.storage.evidence_bytes}`,
17150
+ "",
17151
+ "## Quota"
17152
+ ];
17153
+ if (!report.quota.simulated) {
17154
+ lines.push("- No local quota limits supplied.");
17155
+ } else {
17156
+ for (const limit of report.quota.limits) {
17157
+ lines.push(`- ${limit.name}: ${limit.used}/${limit.limit}${limit.exceeded ? " exceeded" : ""}`);
17158
+ }
17159
+ lines.push(`- Allowed: ${report.quota.allowed ? "yes" : "no"}`);
17160
+ }
17161
+ lines.push("", "Raw commands and artifact paths are not included in this aggregate report.");
17162
+ return lines.join(`
17163
+ `);
17164
+ }
17165
+ // src/lib/local-reports.ts
17166
+ init_database();
17167
+ init_database();
17168
+ var LOCAL_REPORT_SCHEMA_VERSION = 1;
17169
+ var LOCAL_REPORT_TYPES = [
17170
+ "ready",
17171
+ "blocked",
17172
+ "overdue",
17173
+ "standup",
17174
+ "sprint",
17175
+ "progress",
17176
+ "run_outcomes",
17177
+ "verification_evidence",
17178
+ "agent_summary"
17179
+ ];
17180
+ function limitValue(value) {
17181
+ if (value === undefined || !Number.isFinite(value))
17182
+ return 20;
17183
+ return Math.max(1, Math.min(500, Math.trunc(value)));
17184
+ }
17185
+ function isTerminal(task2) {
17186
+ return task2.status === "completed" || task2.status === "failed" || task2.status === "cancelled";
17187
+ }
17188
+ function sameAgent(task2, agentId) {
17189
+ if (!agentId)
17190
+ return true;
17191
+ return task2.assigned_to === agentId || task2.agent_id === agentId;
17192
+ }
17193
+ function withinTaskWindow(task2, options) {
17194
+ const time = Date.parse(task2.updated_at);
17195
+ if (!Number.isFinite(time))
17196
+ return true;
17197
+ if (options.since && time < Date.parse(options.since))
17198
+ return false;
17199
+ if (options.until && time > Date.parse(options.until))
17200
+ return false;
17201
+ return true;
17202
+ }
17203
+ function scopedTasks(options, db) {
17204
+ return listTasks({
17205
+ project_id: options.project_id,
17206
+ plan_id: options.plan_id,
17207
+ include_archived: false
17208
+ }, db).filter((task2) => sameAgent(task2, options.agent_id) && withinTaskWindow(task2, options));
17209
+ }
17210
+ function summarizeTask(task2) {
17211
+ return {
17212
+ id: task2.id,
17213
+ short_id: task2.short_id,
17214
+ title: task2.title,
17215
+ status: task2.status,
17216
+ priority: task2.priority,
17217
+ project_id: task2.project_id,
17218
+ plan_id: task2.plan_id,
17219
+ assigned_to: task2.assigned_to,
17220
+ due_at: task2.due_at,
17221
+ updated_at: task2.updated_at
17222
+ };
17223
+ }
17224
+ function overdueTasks(tasks, nowIso) {
17225
+ const now2 = Date.parse(nowIso);
17226
+ return tasks.filter((task2) => {
17227
+ if (isTerminal(task2) || !task2.due_at)
17228
+ return false;
17229
+ const due = Date.parse(task2.due_at);
17230
+ return Number.isFinite(due) && due < now2;
17231
+ });
17232
+ }
17233
+ function isReady(task2, db) {
17234
+ if (task2.status !== "pending")
17235
+ return false;
17236
+ if (task2.locked_by && !isLockExpired(task2.locked_at))
17237
+ return false;
17238
+ return getBlockingDeps(task2.id, db).length === 0;
17239
+ }
17240
+ function pushTaskCounts(summary, task2, blockedIds, overdueIds) {
17241
+ summary.task_counts.total += 1;
17242
+ summary.task_counts[task2.status] += 1;
17243
+ if (blockedIds.has(task2.id))
17244
+ summary.task_counts.blocked += 1;
17245
+ if (overdueIds.has(task2.id))
17246
+ summary.task_counts.overdue += 1;
17247
+ }
17248
+ function initialAgentSummary(agentId) {
17249
+ return {
17250
+ agent_id: agentId,
17251
+ task_counts: {
17252
+ total: 0,
17253
+ pending: 0,
17254
+ in_progress: 0,
17255
+ completed: 0,
17256
+ failed: 0,
17257
+ cancelled: 0,
17258
+ blocked: 0,
17259
+ overdue: 0
17260
+ },
17261
+ run_outcomes: {
17262
+ running: 0,
17263
+ completed: 0,
17264
+ failed: 0,
17265
+ cancelled: 0
17266
+ },
17267
+ verification_outcomes: {
17268
+ passed: 0,
17269
+ failed: 0,
17270
+ unknown: 0
17271
+ }
17272
+ };
17273
+ }
17274
+ function addScopeClauses(where, params, options, timeColumn) {
17275
+ if (options.project_id) {
17276
+ where.push("t.project_id = ?");
17277
+ params.push(options.project_id);
17278
+ }
17279
+ if (options.plan_id) {
17280
+ where.push("t.plan_id = ?");
17281
+ params.push(options.plan_id);
17282
+ }
17283
+ if (options.agent_id) {
17284
+ where.push("(r.agent_id = ? OR t.agent_id = ? OR t.assigned_to = ?)");
17285
+ params.push(options.agent_id, options.agent_id, options.agent_id);
17286
+ }
17287
+ if (options.since) {
17288
+ where.push(`${timeColumn} >= ?`);
17289
+ params.push(options.since);
17290
+ }
17291
+ if (options.until) {
17292
+ where.push(`${timeColumn} <= ?`);
17293
+ params.push(options.until);
17294
+ }
17295
+ }
17296
+ function loadRuns(options, db) {
17297
+ const where = ["t.archived_at IS NULL"];
17298
+ const params = [];
17299
+ addScopeClauses(where, params, options, "r.started_at");
17300
+ return db.query(`
17301
+ SELECT
17302
+ r.id,
17303
+ r.task_id,
17304
+ t.title AS task_title,
17305
+ t.project_id,
17306
+ t.plan_id,
17307
+ t.agent_id AS task_agent_id,
17308
+ t.assigned_to,
17309
+ r.agent_id,
17310
+ r.status,
17311
+ r.summary,
17312
+ r.started_at,
17313
+ r.completed_at,
17314
+ SUM(CASE WHEN c.status = 'passed' THEN 1 ELSE 0 END) AS passed_commands,
17315
+ SUM(CASE WHEN c.status = 'failed' THEN 1 ELSE 0 END) AS failed_commands,
17316
+ SUM(CASE WHEN c.status = 'unknown' THEN 1 ELSE 0 END) AS unknown_commands,
17317
+ COUNT(DISTINCT a.id) AS artifacts
17318
+ FROM task_runs r
17319
+ JOIN tasks t ON t.id = r.task_id
17320
+ LEFT JOIN task_run_commands c ON c.run_id = r.id
17321
+ LEFT JOIN task_run_artifacts a ON a.run_id = r.id
17322
+ WHERE ${where.join(" AND ")}
17323
+ GROUP BY r.id
17324
+ ORDER BY r.started_at DESC, r.created_at DESC
17325
+ `).all(...params);
17326
+ }
17327
+ function loadVerifications(options, db) {
17328
+ const where = ["t.archived_at IS NULL"];
17329
+ const params = [];
17330
+ if (options.project_id) {
17331
+ where.push("t.project_id = ?");
17332
+ params.push(options.project_id);
17333
+ }
17334
+ if (options.plan_id) {
17335
+ where.push("t.plan_id = ?");
17336
+ params.push(options.plan_id);
17337
+ }
17338
+ if (options.agent_id) {
17339
+ where.push("(v.agent_id = ? OR t.agent_id = ? OR t.assigned_to = ?)");
17340
+ params.push(options.agent_id, options.agent_id, options.agent_id);
17341
+ }
17342
+ if (options.since) {
17343
+ where.push("v.run_at >= ?");
17344
+ params.push(options.since);
17345
+ }
17346
+ if (options.until) {
17347
+ where.push("v.run_at <= ?");
17348
+ params.push(options.until);
17349
+ }
17350
+ return db.query(`
17351
+ SELECT
17352
+ v.id,
17353
+ v.task_id,
17354
+ t.title AS task_title,
17355
+ t.project_id,
17356
+ t.plan_id,
17357
+ t.agent_id AS task_agent_id,
17358
+ t.assigned_to,
17359
+ v.agent_id,
17360
+ v.status,
17361
+ v.command,
17362
+ v.output_summary,
17363
+ v.run_at
17364
+ FROM task_verifications v
17365
+ JOIN tasks t ON t.id = v.task_id
17366
+ WHERE ${where.join(" AND ")}
17367
+ ORDER BY v.run_at DESC, v.created_at DESC
17368
+ `).all(...params);
17369
+ }
17370
+ function agentKey(value) {
17371
+ return value || "unassigned";
17372
+ }
17373
+ function listLocalReportTypes() {
17374
+ return [...LOCAL_REPORT_TYPES];
17375
+ }
17376
+ function createLocalReport(options = {}, db) {
17377
+ const d = db || getDatabase();
17378
+ const limit = limitValue(options.limit);
17379
+ const generatedAt = options.generated_at ?? new Date().toISOString();
17380
+ const nowIso = options.now ?? generatedAt;
17381
+ const tasks = scopedTasks(options, d);
17382
+ const overdue = overdueTasks(tasks, nowIso);
17383
+ const overdueIds = new Set(overdue.map((task2) => task2.id));
17384
+ const blocked = tasks.filter((task2) => task2.status === "pending").map((task2) => ({ task: task2, blockers: getBlockingDeps(task2.id, d) })).filter((item) => item.blockers.length > 0);
17385
+ const blockedIds = new Set(blocked.map((item) => item.task.id));
17386
+ const ready = tasks.filter((task2) => isReady(task2, d));
17387
+ const runs = loadRuns(options, d);
17388
+ const verifications = loadVerifications(options, d);
17389
+ const planSummaries = listPlans(options.project_id, d).filter((plan) => !options.plan_id || plan.id === options.plan_id).map((plan) => {
17390
+ const planTasks = tasks.filter((task2) => task2.plan_id === plan.id);
17391
+ const completed = planTasks.filter((task2) => task2.status === "completed").length;
17392
+ const total = planTasks.length;
17393
+ return {
17394
+ id: plan.id,
17395
+ name: plan.name,
17396
+ status: plan.status,
17397
+ project_id: plan.project_id,
17398
+ agent_id: plan.agent_id,
17399
+ counts: {
17400
+ total,
17401
+ pending: planTasks.filter((task2) => task2.status === "pending").length,
17402
+ in_progress: planTasks.filter((task2) => task2.status === "in_progress").length,
17403
+ completed,
17404
+ failed: planTasks.filter((task2) => task2.status === "failed").length,
17405
+ cancelled: planTasks.filter((task2) => task2.status === "cancelled").length,
17406
+ blocked: planTasks.filter((task2) => blockedIds.has(task2.id)).length,
17407
+ overdue: planTasks.filter((task2) => overdueIds.has(task2.id)).length
17408
+ },
17409
+ progress_percent: total === 0 ? 0 : Math.round(completed / total * 100)
17410
+ };
17411
+ }).filter((plan) => plan.counts.total > 0 || options.plan_id === plan.id);
17412
+ const runOutcomes = { running: 0, completed: 0, failed: 0, cancelled: 0 };
17413
+ for (const run of runs)
17414
+ runOutcomes[run.status] += 1;
17415
+ const verificationOutcomes = { passed: 0, failed: 0, unknown: 0 };
17416
+ for (const verification of verifications)
17417
+ verificationOutcomes[verification.status] += 1;
17418
+ const agents = new Map;
17419
+ function getAgent(id) {
17420
+ if (!agents.has(id))
17421
+ agents.set(id, initialAgentSummary(id));
17422
+ return agents.get(id);
17423
+ }
17424
+ for (const task2 of tasks) {
17425
+ const summary = getAgent(agentKey(task2.assigned_to || task2.agent_id));
17426
+ pushTaskCounts(summary, task2, blockedIds, overdueIds);
17427
+ }
17428
+ for (const run of runs) {
17429
+ const summary = getAgent(agentKey(run.agent_id || run.assigned_to || run.task_agent_id));
17430
+ summary.run_outcomes[run.status] += 1;
17431
+ }
17432
+ for (const verification of verifications) {
17433
+ const summary = getAgent(agentKey(verification.agent_id || verification.assigned_to || verification.task_agent_id));
17434
+ summary.verification_outcomes[verification.status] += 1;
17435
+ }
17436
+ return {
17437
+ schema_version: LOCAL_REPORT_SCHEMA_VERSION,
17438
+ local_only: true,
17439
+ no_network: true,
17440
+ generated_at: generatedAt,
17441
+ scope: {
17442
+ project_id: options.project_id ?? null,
17443
+ plan_id: options.plan_id ?? null,
17444
+ agent_id: options.agent_id ?? null,
17445
+ since: options.since ?? null,
17446
+ until: options.until ?? null
17447
+ },
17448
+ report_types: listLocalReportTypes(),
17449
+ views: {
17450
+ ready: {
17451
+ type: "ready",
17452
+ total: ready.length,
17453
+ items: ready.slice(0, limit).map(summarizeTask)
17454
+ },
17455
+ blocked: {
17456
+ type: "blocked",
17457
+ total: blocked.length,
17458
+ items: blocked.slice(0, limit).map(({ task: task2, blockers }) => ({
17459
+ ...summarizeTask(task2),
17460
+ blocked_by: blockers.map(summarizeTask)
17461
+ }))
17462
+ },
17463
+ overdue: {
17464
+ type: "overdue",
17465
+ total: overdue.length,
17466
+ items: overdue.slice(0, limit).map(summarizeTask)
17467
+ }
17468
+ },
17469
+ plans: planSummaries,
17470
+ runs: {
17471
+ outcomes: runOutcomes,
17472
+ recent: runs.slice(0, limit).map((run) => ({
17473
+ id: run.id,
17474
+ task_id: run.task_id,
17475
+ task_title: run.task_title,
17476
+ agent_id: run.agent_id,
17477
+ status: run.status,
17478
+ summary: run.summary,
17479
+ started_at: run.started_at,
17480
+ completed_at: run.completed_at,
17481
+ command_outcomes: {
17482
+ passed: Number(run.passed_commands || 0),
17483
+ failed: Number(run.failed_commands || 0),
17484
+ unknown: Number(run.unknown_commands || 0)
17485
+ },
17486
+ artifacts: Number(run.artifacts || 0)
17487
+ }))
17488
+ },
17489
+ verification: {
17490
+ outcomes: verificationOutcomes,
17491
+ recent: verifications.slice(0, limit).map((verification) => ({
17492
+ id: verification.id,
17493
+ task_id: verification.task_id,
17494
+ task_title: verification.task_title,
17495
+ agent_id: verification.agent_id,
17496
+ status: verification.status,
17497
+ command: verification.command,
17498
+ output_summary: verification.output_summary,
17499
+ run_at: verification.run_at
17500
+ }))
17501
+ },
17502
+ agents: [...agents.values()].sort((left, right) => left.agent_id.localeCompare(right.agent_id)),
17503
+ exports: {
17504
+ json_contract: "local_report",
17505
+ markdown_supported: true
17506
+ }
17507
+ };
17508
+ }
17509
+ function taskLine(task2) {
17510
+ const due = task2.due_at ? ` due ${task2.due_at.slice(0, 10)}` : "";
17511
+ const assignee = task2.assigned_to ? ` @${task2.assigned_to}` : "";
17512
+ return `- ${task2.short_id || task2.id.slice(0, 8)} ${task2.title} [${task2.status}/${task2.priority}]${assignee}${due}`;
17513
+ }
17514
+ function outcomeLine(values) {
17515
+ return Object.entries(values).map(([key, value]) => `${key}: ${value}`).join(", ");
17516
+ }
17517
+ function renderLocalReportMarkdown(report) {
17518
+ const lines = [
17519
+ "# Local Agent Report",
17520
+ "",
17521
+ `Generated: ${report.generated_at}`,
17522
+ `Scope: project ${report.scope.project_id ?? "all"}; plan ${report.scope.plan_id ?? "all"}; agent ${report.scope.agent_id ?? "all"}`,
17523
+ "",
17524
+ "## Task Views",
17525
+ "",
17526
+ `Ready (${report.views.ready.total})`,
17527
+ ...report.views.ready.items.length ? report.views.ready.items.map(taskLine) : ["- none"],
17528
+ "",
17529
+ `Blocked (${report.views.blocked.total})`,
17530
+ ...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"],
17531
+ "",
17532
+ `Overdue (${report.views.overdue.total})`,
17533
+ ...report.views.overdue.items.length ? report.views.overdue.items.map(taskLine) : ["- none"],
17534
+ "",
17535
+ "## Plans"
17536
+ ];
17537
+ if (report.plans.length === 0)
17538
+ lines.push("- none");
17539
+ for (const plan of report.plans) {
17540
+ lines.push(`- ${plan.name}: ${plan.progress_percent}% complete, ${plan.counts.blocked} blocked, ${plan.counts.overdue} overdue`);
17541
+ }
17542
+ lines.push("", "## Runs", outcomeLine(report.runs.outcomes));
17543
+ for (const run of report.runs.recent) {
17544
+ lines.push(`- ${run.id.slice(0, 8)} ${run.status} ${run.task_title}${run.summary ? `: ${run.summary}` : ""}`);
17545
+ }
17546
+ lines.push("", "## Verification", outcomeLine(report.verification.outcomes));
17547
+ for (const verification of report.verification.recent) {
17548
+ lines.push(`- ${verification.status} ${verification.task_title}: ${verification.output_summary || verification.command}`);
17549
+ }
17550
+ lines.push("", "## Agents");
17551
+ if (report.agents.length === 0)
17552
+ lines.push("- none");
17553
+ for (const agent of report.agents) {
17554
+ 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)}`);
17555
+ }
17556
+ return `${lines.join(`
17557
+ `)}
17558
+ `;
17559
+ }
16097
17560
  // src/lib/local-encryption.ts
16098
- import { createCipheriv, createDecipheriv, createHash as createHash6, randomBytes, scryptSync, timingSafeEqual } from "crypto";
17561
+ import { createCipheriv, createDecipheriv, createHash as createHash7, randomBytes, scryptSync, timingSafeEqual } from "crypto";
16099
17562
  var TODOS_ENCRYPTED_VALUE_KIND = "hasna.todos.encrypted-value";
16100
17563
  var TODOS_ENCRYPTED_BRIDGE_KIND = "hasna.todos.encrypted-bridge";
16101
17564
  var TODOS_ENCRYPTION_SCHEMA_VERSION = 1;
@@ -16120,8 +17583,8 @@ class EncryptedPayloadError extends Error {
16120
17583
  function now2() {
16121
17584
  return new Date().toISOString();
16122
17585
  }
16123
- function sha2563(value) {
16124
- return createHash6("sha256").update(value).digest("hex");
17586
+ function sha2564(value) {
17587
+ return createHash7("sha256").update(value).digest("hex");
16125
17588
  }
16126
17589
  function normalizeProfileName(value) {
16127
17590
  const name = (value || DEFAULT_ENCRYPTION_PROFILE).trim();
@@ -16220,7 +17683,7 @@ function encryptString(plaintext, options = {}) {
16220
17683
  iv: iv.toString("base64"),
16221
17684
  auth_tag: authTag.toString("base64"),
16222
17685
  ciphertext: ciphertext.toString("base64"),
16223
- plaintext_sha256: sha2563(plaintext)
17686
+ plaintext_sha256: sha2564(plaintext)
16224
17687
  };
16225
17688
  }
16226
17689
  function isEncryptedValue(value) {
@@ -16245,7 +17708,7 @@ function decryptString(envelope, env = process.env) {
16245
17708
  decipher.final()
16246
17709
  ]).toString("utf8");
16247
17710
  const expected = Buffer.from(envelope.plaintext_sha256, "hex");
16248
- const actual = Buffer.from(sha2563(plaintext), "hex");
17711
+ const actual = Buffer.from(sha2564(plaintext), "hex");
16249
17712
  if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
16250
17713
  throw new EncryptedPayloadError("decrypted payload checksum mismatch");
16251
17714
  }