@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.
- package/README.md +125 -3
- package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
- package/dist/cli/commands/help-commands.d.ts +3 -0
- package/dist/cli/commands/help-commands.d.ts.map +1 -0
- package/dist/cli/commands/local-backup-commands.d.ts +3 -0
- package/dist/cli/commands/local-backup-commands.d.ts.map +1 -0
- package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/commands/scale-hardening-commands.d.ts +3 -0
- package/dist/cli/commands/scale-hardening-commands.d.ts.map +1 -0
- package/dist/cli/commands/usage-ledger-commands.d.ts +3 -0
- package/dist/cli/commands/usage-ledger-commands.d.ts.map +1 -0
- package/dist/cli/components/Dashboard.d.ts.map +1 -1
- package/dist/cli/index.js +3822 -547
- package/dist/cli-mcp-parity.d.ts +1 -1
- package/dist/cli-mcp-parity.d.ts.map +1 -1
- package/dist/contracts.d.ts +6 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +1506 -24
- package/dist/db/agent-names.d.ts +2 -1
- package/dist/db/agent-names.d.ts.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/task-runs.d.ts +3 -0
- package/dist/db/task-runs.d.ts.map +1 -1
- package/dist/index.d.ts +16 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2822 -282
- package/dist/json-contracts.d.ts.map +1 -1
- package/dist/lib/cli-help.d.ts +38 -0
- package/dist/lib/cli-help.d.ts.map +1 -0
- package/dist/lib/config.d.ts +38 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/local-backups.d.ts +129 -0
- package/dist/lib/local-backups.d.ts.map +1 -0
- package/dist/lib/local-extensions.d.ts +18 -1
- package/dist/lib/local-extensions.d.ts.map +1 -1
- package/dist/lib/local-reports.d.ts +149 -0
- package/dist/lib/local-reports.d.ts.map +1 -0
- package/dist/lib/redaction.d.ts.map +1 -1
- package/dist/lib/scale-hardening.d.ts +74 -0
- package/dist/lib/scale-hardening.d.ts.map +1 -0
- package/dist/lib/tui-dashboard.d.ts +49 -0
- package/dist/lib/tui-dashboard.d.ts.map +1 -0
- package/dist/lib/usage-ledger.d.ts +82 -0
- package/dist/lib/usage-ledger.d.ts.map +1 -0
- package/dist/lib/workflow-states.d.ts +70 -0
- package/dist/lib/workflow-states.d.ts.map +1 -0
- package/dist/mcp/index.js +8245 -6445
- package/dist/mcp/token-utils.d.ts.map +1 -1
- package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-resources.d.ts.map +1 -1
- package/dist/mcp.js +12 -0
- package/dist/registry.js +1487 -24
- package/dist/server/index.js +152 -20
- package/dist/storage.js +164 -21
- package/package.json +1 -1
- 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: {
|
|
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-
|
|
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
|
|
10898
|
-
return
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
14231
|
-
import { join as join8, resolve as
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
16124
|
-
return
|
|
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:
|
|
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(
|
|
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
|
}
|