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