@cleocode/core 2026.4.7 → 2026.4.11
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/dist/adapters/adapter-registry.js +64 -0
- package/dist/adapters/adapter-registry.js.map +1 -0
- package/dist/adapters/discovery.js +83 -0
- package/dist/adapters/discovery.js.map +1 -0
- package/dist/adapters/index.js +9 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/manager.js +260 -0
- package/dist/adapters/manager.js.map +1 -0
- package/dist/admin/export-tasks.js +171 -0
- package/dist/admin/export-tasks.js.map +1 -0
- package/dist/admin/export.js +103 -0
- package/dist/admin/export.js.map +1 -0
- package/dist/admin/help.js +106 -0
- package/dist/admin/help.js.map +1 -0
- package/dist/admin/import-tasks.js +182 -0
- package/dist/admin/import-tasks.js.map +1 -0
- package/dist/admin/import.js +129 -0
- package/dist/admin/import.js.map +1 -0
- package/dist/admin/index.js +13 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/adrs/find.js +134 -0
- package/dist/adrs/find.js.map +1 -0
- package/dist/adrs/index.js +15 -0
- package/dist/adrs/index.js.map +1 -0
- package/dist/adrs/link-pipeline.js +160 -0
- package/dist/adrs/link-pipeline.js.map +1 -0
- package/dist/adrs/list.js +43 -0
- package/dist/adrs/list.js.map +1 -0
- package/dist/adrs/parse.js +51 -0
- package/dist/adrs/parse.js.map +1 -0
- package/dist/adrs/show.js +22 -0
- package/dist/adrs/show.js.map +1 -0
- package/dist/adrs/sync.js +188 -0
- package/dist/adrs/sync.js.map +1 -0
- package/dist/adrs/types.js +9 -0
- package/dist/adrs/types.js.map +1 -0
- package/dist/adrs/validate.js +57 -0
- package/dist/adrs/validate.js.map +1 -0
- package/dist/agents/agent-registry.js +288 -0
- package/dist/agents/agent-registry.js.map +1 -0
- package/dist/agents/agent-schema.d.ts +2 -2
- package/dist/agents/agent-schema.js +82 -0
- package/dist/agents/agent-schema.js.map +1 -0
- package/dist/agents/capacity.js +116 -0
- package/dist/agents/capacity.js.map +1 -0
- package/dist/agents/execution-learning.js +474 -0
- package/dist/agents/execution-learning.js.map +1 -0
- package/dist/agents/health-monitor.js +217 -0
- package/dist/agents/health-monitor.js.map +1 -0
- package/dist/agents/index.js +29 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/registry.js +314 -0
- package/dist/agents/registry.js.map +1 -0
- package/dist/agents/retry.js +215 -0
- package/dist/agents/retry.js.map +1 -0
- package/dist/audit-prune.js +94 -0
- package/dist/audit-prune.js.map +1 -0
- package/dist/audit.js +68 -0
- package/dist/audit.js.map +1 -0
- package/dist/backfill/index.js +229 -0
- package/dist/backfill/index.js.map +1 -0
- package/dist/bootstrap.js +344 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/caamp/adapter.js +259 -0
- package/dist/caamp/adapter.js.map +1 -0
- package/dist/caamp/capability-check.js +38 -0
- package/dist/caamp/capability-check.js.map +1 -0
- package/dist/caamp/index.js +21 -0
- package/dist/caamp/index.js.map +1 -0
- package/dist/caamp-init.js +16 -0
- package/dist/caamp-init.js.map +1 -0
- package/dist/cleo.js +322 -0
- package/dist/cleo.js.map +1 -0
- package/dist/code/index.js +10 -0
- package/dist/code/index.js.map +1 -0
- package/dist/code/outline.js +165 -0
- package/dist/code/outline.js.map +1 -0
- package/dist/code/parser.js +295 -0
- package/dist/code/parser.js.map +1 -0
- package/dist/code/search.js +135 -0
- package/dist/code/search.js.map +1 -0
- package/dist/code/unfold.js +155 -0
- package/dist/code/unfold.js.map +1 -0
- package/dist/codebase-map/analyzers/architecture.js +130 -0
- package/dist/codebase-map/analyzers/architecture.js.map +1 -0
- package/dist/codebase-map/analyzers/concerns.js +122 -0
- package/dist/codebase-map/analyzers/concerns.js.map +1 -0
- package/dist/codebase-map/analyzers/conventions.js +149 -0
- package/dist/codebase-map/analyzers/conventions.js.map +1 -0
- package/dist/codebase-map/analyzers/integrations.js +108 -0
- package/dist/codebase-map/analyzers/integrations.js.map +1 -0
- package/dist/codebase-map/analyzers/stack.js +117 -0
- package/dist/codebase-map/analyzers/stack.js.map +1 -0
- package/dist/codebase-map/analyzers/structure.js +137 -0
- package/dist/codebase-map/analyzers/structure.js.map +1 -0
- package/dist/codebase-map/analyzers/testing.js +118 -0
- package/dist/codebase-map/analyzers/testing.js.map +1 -0
- package/dist/codebase-map/index.js +57 -0
- package/dist/codebase-map/index.js.map +1 -0
- package/dist/codebase-map/store.js +122 -0
- package/dist/codebase-map/store.js.map +1 -0
- package/dist/codebase-map/summary.js +152 -0
- package/dist/codebase-map/summary.js.map +1 -0
- package/dist/compliance/index.js +288 -0
- package/dist/compliance/index.js.map +1 -0
- package/dist/compliance/protocol-enforcement.js +332 -0
- package/dist/compliance/protocol-enforcement.js.map +1 -0
- package/dist/compliance/protocol-rules.js +786 -0
- package/dist/compliance/protocol-rules.js.map +1 -0
- package/dist/compliance/protocol-types.js +79 -0
- package/dist/compliance/protocol-types.js.map +1 -0
- package/dist/compliance/store.js +53 -0
- package/dist/compliance/store.js.map +1 -0
- package/dist/conduit/conduit-client.js +107 -0
- package/dist/conduit/conduit-client.js.map +1 -0
- package/dist/conduit/factory.js +52 -0
- package/dist/conduit/factory.js.map +1 -0
- package/dist/conduit/http-transport.js +155 -0
- package/dist/conduit/http-transport.js.map +1 -0
- package/dist/conduit/index.js +15 -0
- package/dist/conduit/index.js.map +1 -0
- package/dist/conduit/local-transport.js +245 -0
- package/dist/conduit/local-transport.js.map +1 -0
- package/dist/conduit/sse-transport.js +299 -0
- package/dist/conduit/sse-transport.js.map +1 -0
- package/dist/config/build-config.js +29 -0
- package/dist/config/build-config.js.map +1 -0
- package/dist/config.js +401 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.js +18 -0
- package/dist/constants.js.map +1 -0
- package/dist/context/index.js +137 -0
- package/dist/context/index.js.map +1 -0
- package/dist/crypto/credentials.js +191 -0
- package/dist/crypto/credentials.js.map +1 -0
- package/dist/discovery.js +182 -0
- package/dist/discovery.js.map +1 -0
- package/dist/engine-result.js +12 -0
- package/dist/engine-result.js.map +1 -0
- package/dist/error-catalog.js +404 -0
- package/dist/error-catalog.js.map +1 -0
- package/dist/error-registry.js +393 -0
- package/dist/error-registry.js.map +1 -0
- package/dist/errors.js +167 -0
- package/dist/errors.js.map +1 -0
- package/dist/hooks/handlers/agent-hooks.js +106 -0
- package/dist/hooks/handlers/agent-hooks.js.map +1 -0
- package/dist/hooks/handlers/context-hooks.js +111 -0
- package/dist/hooks/handlers/context-hooks.js.map +1 -0
- package/dist/hooks/handlers/error-hooks.js +52 -0
- package/dist/hooks/handlers/error-hooks.js.map +1 -0
- package/dist/hooks/handlers/file-hooks.js +104 -0
- package/dist/hooks/handlers/file-hooks.js.map +1 -0
- package/dist/hooks/handlers/handler-helpers.js +61 -0
- package/dist/hooks/handlers/handler-helpers.js.map +1 -0
- package/dist/hooks/handlers/index.js +28 -0
- package/dist/hooks/handlers/index.js.map +1 -0
- package/dist/hooks/handlers/memory-bridge-refresh.js +42 -0
- package/dist/hooks/handlers/memory-bridge-refresh.js.map +1 -0
- package/dist/hooks/handlers/notification-hooks.js +62 -0
- package/dist/hooks/handlers/notification-hooks.js.map +1 -0
- package/dist/hooks/handlers/session-hooks.d.ts +21 -0
- package/dist/hooks/handlers/session-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/session-hooks.js +142 -0
- package/dist/hooks/handlers/session-hooks.js.map +1 -0
- package/dist/hooks/handlers/task-hooks.js +65 -0
- package/dist/hooks/handlers/task-hooks.js.map +1 -0
- package/dist/hooks/handlers/work-capture-hooks.js +165 -0
- package/dist/hooks/handlers/work-capture-hooks.js.map +1 -0
- package/dist/hooks/index.js +13 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/payload-schemas.d.ts +2 -2
- package/dist/hooks/payload-schemas.js +220 -0
- package/dist/hooks/payload-schemas.js.map +1 -0
- package/dist/hooks/provider-hooks.js +66 -0
- package/dist/hooks/provider-hooks.js.map +1 -0
- package/dist/hooks/registry.js +229 -0
- package/dist/hooks/registry.js.map +1 -0
- package/dist/hooks/types.js +66 -0
- package/dist/hooks/types.js.map +1 -0
- package/dist/hooks.js +136 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.js +3361 -3095
- package/dist/index.js.map +4 -4
- package/dist/init.js +852 -0
- package/dist/init.js.map +1 -0
- package/dist/inject/index.js +82 -0
- package/dist/inject/index.js.map +1 -0
- package/dist/injection.js +343 -0
- package/dist/injection.js.map +1 -0
- package/dist/intelligence/adaptive-validation.js +497 -0
- package/dist/intelligence/adaptive-validation.js.map +1 -0
- package/dist/intelligence/impact.js +675 -0
- package/dist/intelligence/impact.js.map +1 -0
- package/dist/intelligence/index.js +22 -0
- package/dist/intelligence/index.js.map +1 -0
- package/dist/intelligence/patterns.js +492 -0
- package/dist/intelligence/patterns.js.map +1 -0
- package/dist/intelligence/prediction.js +499 -0
- package/dist/intelligence/prediction.js.map +1 -0
- package/dist/intelligence/types.js +13 -0
- package/dist/intelligence/types.js.map +1 -0
- package/dist/internal.d.ts +7 -2
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +299 -0
- package/dist/internal.js.map +1 -0
- package/dist/issue/create.js +121 -0
- package/dist/issue/create.js.map +1 -0
- package/dist/issue/diagnostics.js +59 -0
- package/dist/issue/diagnostics.js.map +1 -0
- package/dist/issue/index.js +10 -0
- package/dist/issue/index.js.map +1 -0
- package/dist/issue/template-parser.js +267 -0
- package/dist/issue/template-parser.js.map +1 -0
- package/dist/json-schema-validator.js +76 -0
- package/dist/json-schema-validator.js.map +1 -0
- package/dist/lib/index.js +11 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/retry.js +152 -0
- package/dist/lib/retry.js.map +1 -0
- package/dist/lib/tree-sitter-languages.js +71 -0
- package/dist/lib/tree-sitter-languages.js.map +1 -0
- package/dist/lifecycle/chain-composition.js +152 -0
- package/dist/lifecycle/chain-composition.js.map +1 -0
- package/dist/lifecycle/chain-store.js +246 -0
- package/dist/lifecycle/chain-store.js.map +1 -0
- package/dist/lifecycle/consolidate-rcasd.js +352 -0
- package/dist/lifecycle/consolidate-rcasd.js.map +1 -0
- package/dist/lifecycle/default-chain.js +176 -0
- package/dist/lifecycle/default-chain.js.map +1 -0
- package/dist/lifecycle/evidence.js +180 -0
- package/dist/lifecycle/evidence.js.map +1 -0
- package/dist/lifecycle/frontmatter.js +363 -0
- package/dist/lifecycle/frontmatter.js.map +1 -0
- package/dist/lifecycle/index.js +756 -0
- package/dist/lifecycle/index.js.map +1 -0
- package/dist/lifecycle/pipeline.js +656 -0
- package/dist/lifecycle/pipeline.js.map +1 -0
- package/dist/lifecycle/rcasd-index.js +326 -0
- package/dist/lifecycle/rcasd-index.js.map +1 -0
- package/dist/lifecycle/rcasd-paths.js +220 -0
- package/dist/lifecycle/rcasd-paths.js.map +1 -0
- package/dist/lifecycle/resume.js +864 -0
- package/dist/lifecycle/resume.js.map +1 -0
- package/dist/lifecycle/stage-artifacts.js +94 -0
- package/dist/lifecycle/stage-artifacts.js.map +1 -0
- package/dist/lifecycle/stage-guidance.js +234 -0
- package/dist/lifecycle/stage-guidance.js.map +1 -0
- package/dist/lifecycle/stages.js +534 -0
- package/dist/lifecycle/stages.js.map +1 -0
- package/dist/lifecycle/state-machine.js +516 -0
- package/dist/lifecycle/state-machine.js.map +1 -0
- package/dist/lifecycle/tessera-engine.js +249 -0
- package/dist/lifecycle/tessera-engine.js.map +1 -0
- package/dist/logger.js +140 -0
- package/dist/logger.js.map +1 -0
- package/dist/memory/auto-extract.js +177 -0
- package/dist/memory/auto-extract.js.map +1 -0
- package/dist/memory/brain-embedding.js +66 -0
- package/dist/memory/brain-embedding.js.map +1 -0
- package/dist/memory/brain-lifecycle.js +298 -0
- package/dist/memory/brain-lifecycle.js.map +1 -0
- package/dist/memory/brain-links.js +161 -0
- package/dist/memory/brain-links.js.map +1 -0
- package/dist/memory/brain-maintenance.js +114 -0
- package/dist/memory/brain-maintenance.js.map +1 -0
- package/dist/memory/brain-migration.js +149 -0
- package/dist/memory/brain-migration.js.map +1 -0
- package/dist/memory/brain-reasoning.js +215 -0
- package/dist/memory/brain-reasoning.js.map +1 -0
- package/dist/memory/brain-retrieval.js +542 -0
- package/dist/memory/brain-retrieval.js.map +1 -0
- package/dist/memory/brain-row-types.js +10 -0
- package/dist/memory/brain-row-types.js.map +1 -0
- package/dist/memory/brain-search.js +519 -0
- package/dist/memory/brain-search.js.map +1 -0
- package/dist/memory/brain-similarity.js +145 -0
- package/dist/memory/brain-similarity.js.map +1 -0
- package/dist/memory/claude-mem-migration.js +277 -0
- package/dist/memory/claude-mem-migration.js.map +1 -0
- package/dist/memory/decisions.js +162 -0
- package/dist/memory/decisions.js.map +1 -0
- package/dist/memory/embedding-local.js +97 -0
- package/dist/memory/embedding-local.js.map +1 -0
- package/dist/memory/embedding-queue.js +271 -0
- package/dist/memory/embedding-queue.js.map +1 -0
- package/dist/memory/embedding-worker.js +58 -0
- package/dist/memory/embedding-worker.js.map +1 -0
- package/dist/memory/engine-compat.js +1397 -0
- package/dist/memory/engine-compat.js.map +1 -0
- package/dist/memory/index.js +1140 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/learnings.d.ts +4 -4
- package/dist/memory/learnings.js +121 -0
- package/dist/memory/learnings.js.map +1 -0
- package/dist/memory/memory-bridge.js +370 -0
- package/dist/memory/memory-bridge.js.map +1 -0
- package/dist/memory/patterns.d.ts +6 -6
- package/dist/memory/patterns.js +122 -0
- package/dist/memory/patterns.js.map +1 -0
- package/dist/memory/pipeline-manifest-sqlite.js +975 -0
- package/dist/memory/pipeline-manifest-sqlite.js.map +1 -0
- package/dist/memory/session-memory.js +331 -0
- package/dist/memory/session-memory.js.map +1 -0
- package/dist/metrics/ab-test.js +260 -0
- package/dist/metrics/ab-test.js.map +1 -0
- package/dist/metrics/aggregation.js +363 -0
- package/dist/metrics/aggregation.js.map +1 -0
- package/dist/metrics/common.js +64 -0
- package/dist/metrics/common.js.map +1 -0
- package/dist/metrics/enums.js +78 -0
- package/dist/metrics/enums.js.map +1 -0
- package/dist/metrics/index.js +19 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/model-provider-registry.js +88 -0
- package/dist/metrics/model-provider-registry.js.map +1 -0
- package/dist/metrics/otel-integration.js +263 -0
- package/dist/metrics/otel-integration.js.map +1 -0
- package/dist/metrics/provider-detection.js +103 -0
- package/dist/metrics/provider-detection.js.map +1 -0
- package/dist/metrics/token-estimation.js +253 -0
- package/dist/metrics/token-estimation.js.map +1 -0
- package/dist/metrics/token-service.js +450 -0
- package/dist/metrics/token-service.js.map +1 -0
- package/dist/migration/agent-outputs.js +316 -0
- package/dist/migration/agent-outputs.js.map +1 -0
- package/dist/migration/checksum.js +92 -0
- package/dist/migration/checksum.js.map +1 -0
- package/dist/migration/index.js +282 -0
- package/dist/migration/index.js.map +1 -0
- package/dist/migration/logger.js +360 -0
- package/dist/migration/logger.js.map +1 -0
- package/dist/migration/preflight.js +9 -0
- package/dist/migration/preflight.js.map +1 -0
- package/dist/migration/state.js +421 -0
- package/dist/migration/state.js.map +1 -0
- package/dist/migration/validate.js +241 -0
- package/dist/migration/validate.js.map +1 -0
- package/dist/mvi-helpers.js +74 -0
- package/dist/mvi-helpers.js.map +1 -0
- package/dist/nexus/deps.js +375 -0
- package/dist/nexus/deps.js.map +1 -0
- package/dist/nexus/discover.js +288 -0
- package/dist/nexus/discover.js.map +1 -0
- package/dist/nexus/hash.js +10 -0
- package/dist/nexus/hash.js.map +1 -0
- package/dist/nexus/index.js +40 -0
- package/dist/nexus/index.js.map +1 -0
- package/dist/nexus/migrate-json-to-sqlite.js +115 -0
- package/dist/nexus/migrate-json-to-sqlite.js.map +1 -0
- package/dist/nexus/permissions.js +105 -0
- package/dist/nexus/permissions.js.map +1 -0
- package/dist/nexus/query.js +175 -0
- package/dist/nexus/query.js.map +1 -0
- package/dist/nexus/registry.js +584 -0
- package/dist/nexus/registry.js.map +1 -0
- package/dist/nexus/sharing/index.js +288 -0
- package/dist/nexus/sharing/index.js.map +1 -0
- package/dist/nexus/transfer-types.js +8 -0
- package/dist/nexus/transfer-types.js.map +1 -0
- package/dist/nexus/transfer.js +263 -0
- package/dist/nexus/transfer.js.map +1 -0
- package/dist/nexus/workspace.js +355 -0
- package/dist/nexus/workspace.js.map +1 -0
- package/dist/observability/index.js +103 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/log-filter.js +63 -0
- package/dist/observability/log-filter.js.map +1 -0
- package/dist/observability/log-parser.js +99 -0
- package/dist/observability/log-parser.js.map +1 -0
- package/dist/observability/log-reader.js +139 -0
- package/dist/observability/log-reader.js.map +1 -0
- package/dist/observability/types.js +19 -0
- package/dist/observability/types.js.map +1 -0
- package/dist/orchestration/analyze.js +107 -0
- package/dist/orchestration/analyze.js.map +1 -0
- package/dist/orchestration/bootstrap.js +132 -0
- package/dist/orchestration/bootstrap.js.map +1 -0
- package/dist/orchestration/context.js +56 -0
- package/dist/orchestration/context.js.map +1 -0
- package/dist/orchestration/critical-path.js +100 -0
- package/dist/orchestration/critical-path.js.map +1 -0
- package/dist/orchestration/hierarchy.js +183 -0
- package/dist/orchestration/hierarchy.js.map +1 -0
- package/dist/orchestration/index.js +287 -0
- package/dist/orchestration/index.js.map +1 -0
- package/dist/orchestration/parallel.js +89 -0
- package/dist/orchestration/parallel.js.map +1 -0
- package/dist/orchestration/protocol-validators.js +815 -0
- package/dist/orchestration/protocol-validators.js.map +1 -0
- package/dist/orchestration/skill-ops.js +98 -0
- package/dist/orchestration/skill-ops.js.map +1 -0
- package/dist/orchestration/status.js +107 -0
- package/dist/orchestration/status.js.map +1 -0
- package/dist/orchestration/unblock.js +103 -0
- package/dist/orchestration/unblock.js.map +1 -0
- package/dist/orchestration/validate-spawn.js +67 -0
- package/dist/orchestration/validate-spawn.js.map +1 -0
- package/dist/orchestration/waves.js +86 -0
- package/dist/orchestration/waves.js.map +1 -0
- package/dist/otel/index.js +163 -0
- package/dist/otel/index.js.map +1 -0
- package/dist/output.js +164 -0
- package/dist/output.js.map +1 -0
- package/dist/pagination.js +64 -0
- package/dist/pagination.js.map +1 -0
- package/dist/paths.d.ts +39 -9
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +776 -0
- package/dist/paths.js.map +1 -0
- package/dist/phases/deps.js +372 -0
- package/dist/phases/deps.js.map +1 -0
- package/dist/phases/index.js +349 -0
- package/dist/phases/index.js.map +1 -0
- package/dist/pipeline/index.js +10 -0
- package/dist/pipeline/index.js.map +1 -0
- package/dist/pipeline/phase.js +45 -0
- package/dist/pipeline/phase.js.map +1 -0
- package/dist/platform.js +211 -0
- package/dist/platform.js.map +1 -0
- package/dist/project-info.js +84 -0
- package/dist/project-info.js.map +1 -0
- package/dist/reconciliation/index.js +10 -0
- package/dist/reconciliation/index.js.map +1 -0
- package/dist/reconciliation/link-store.js +129 -0
- package/dist/reconciliation/link-store.js.map +1 -0
- package/dist/reconciliation/reconciliation-engine.js +298 -0
- package/dist/reconciliation/reconciliation-engine.js.map +1 -0
- package/dist/release/artifacts.js +427 -0
- package/dist/release/artifacts.js.map +1 -0
- package/dist/release/changelog-writer.js +151 -0
- package/dist/release/changelog-writer.js.map +1 -0
- package/dist/release/channel.js +144 -0
- package/dist/release/channel.js.map +1 -0
- package/dist/release/ci.js +166 -0
- package/dist/release/ci.js.map +1 -0
- package/dist/release/github-pr.js +225 -0
- package/dist/release/github-pr.js.map +1 -0
- package/dist/release/guards.js +116 -0
- package/dist/release/guards.js.map +1 -0
- package/dist/release/index.js +22 -0
- package/dist/release/index.js.map +1 -0
- package/dist/release/release-config.js +158 -0
- package/dist/release/release-config.js.map +1 -0
- package/dist/release/release-manifest.js +1019 -0
- package/dist/release/release-manifest.js.map +1 -0
- package/dist/release/version-bump.js +255 -0
- package/dist/release/version-bump.js.map +1 -0
- package/dist/remote/index.js +257 -0
- package/dist/remote/index.js.map +1 -0
- package/dist/repair.js +130 -0
- package/dist/repair.js.map +1 -0
- package/dist/research/index.js +2 -0
- package/dist/research/index.js.map +1 -0
- package/dist/roadmap/index.js +59 -0
- package/dist/roadmap/index.js.map +1 -0
- package/dist/routing/capability-matrix.js +1556 -0
- package/dist/routing/capability-matrix.js.map +1 -0
- package/dist/routing/index.js +9 -0
- package/dist/routing/index.js.map +1 -0
- package/dist/scaffold.d.ts +15 -2
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +1759 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/schema-management.js +295 -0
- package/dist/schema-management.js.map +1 -0
- package/dist/security/index.js +9 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/input-sanitization.js +321 -0
- package/dist/security/input-sanitization.js.map +1 -0
- package/dist/sequence/index.js +295 -0
- package/dist/sequence/index.js.map +1 -0
- package/dist/sessions/assumptions.js +54 -0
- package/dist/sessions/assumptions.js.map +1 -0
- package/dist/sessions/briefing.js +377 -0
- package/dist/sessions/briefing.js.map +1 -0
- package/dist/sessions/context-alert.js +222 -0
- package/dist/sessions/context-alert.js.map +1 -0
- package/dist/sessions/context-inject.js +61 -0
- package/dist/sessions/context-inject.js.map +1 -0
- package/dist/sessions/context-monitor.js +98 -0
- package/dist/sessions/context-monitor.js.map +1 -0
- package/dist/sessions/decisions.js +65 -0
- package/dist/sessions/decisions.js.map +1 -0
- package/dist/sessions/find.js +65 -0
- package/dist/sessions/find.js.map +1 -0
- package/dist/sessions/handoff.js +328 -0
- package/dist/sessions/handoff.js.map +1 -0
- package/dist/sessions/hitl-warnings.js +254 -0
- package/dist/sessions/hitl-warnings.js.map +1 -0
- package/dist/sessions/index.js +327 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/sessions/session-archive.js +40 -0
- package/dist/sessions/session-archive.js.map +1 -0
- package/dist/sessions/session-cleanup.js +59 -0
- package/dist/sessions/session-cleanup.js.map +1 -0
- package/dist/sessions/session-drift.js +134 -0
- package/dist/sessions/session-drift.js.map +1 -0
- package/dist/sessions/session-enforcement.js +144 -0
- package/dist/sessions/session-enforcement.js.map +1 -0
- package/dist/sessions/session-grade.js +253 -0
- package/dist/sessions/session-grade.js.map +1 -0
- package/dist/sessions/session-history.js +42 -0
- package/dist/sessions/session-history.js.map +1 -0
- package/dist/sessions/session-id.js +81 -0
- package/dist/sessions/session-id.js.map +1 -0
- package/dist/sessions/session-memory-bridge.js +52 -0
- package/dist/sessions/session-memory-bridge.js.map +1 -0
- package/dist/sessions/session-show.js +24 -0
- package/dist/sessions/session-show.js.map +1 -0
- package/dist/sessions/session-stats.js +69 -0
- package/dist/sessions/session-stats.js.map +1 -0
- package/dist/sessions/session-suspend.js +39 -0
- package/dist/sessions/session-suspend.js.map +1 -0
- package/dist/sessions/session-switch.js +51 -0
- package/dist/sessions/session-switch.js.map +1 -0
- package/dist/sessions/session-view.js +76 -0
- package/dist/sessions/session-view.js.map +1 -0
- package/dist/sessions/snapshot.js +213 -0
- package/dist/sessions/snapshot.js.map +1 -0
- package/dist/sessions/statusline-setup.js +85 -0
- package/dist/sessions/statusline-setup.js.map +1 -0
- package/dist/sessions/types.js +8 -0
- package/dist/sessions/types.js.map +1 -0
- package/dist/skills/agents/config.js +94 -0
- package/dist/skills/agents/config.js.map +1 -0
- package/dist/skills/agents/install.js +116 -0
- package/dist/skills/agents/install.js.map +1 -0
- package/dist/skills/agents/registry.js +161 -0
- package/dist/skills/agents/registry.js.map +1 -0
- package/dist/skills/discovery.js +333 -0
- package/dist/skills/discovery.js.map +1 -0
- package/dist/skills/dispatch.js +347 -0
- package/dist/skills/dispatch.js.map +1 -0
- package/dist/skills/dynamic-skill-generator.js +87 -0
- package/dist/skills/dynamic-skill-generator.js.map +1 -0
- package/dist/skills/index.js +44 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/injection/subagent.js +195 -0
- package/dist/skills/injection/subagent.js.map +1 -0
- package/dist/skills/injection/token.js +260 -0
- package/dist/skills/injection/token.js.map +1 -0
- package/dist/skills/install.js +40 -0
- package/dist/skills/install.js.map +1 -0
- package/dist/skills/manifests/contribution.js +175 -0
- package/dist/skills/manifests/contribution.js.map +1 -0
- package/dist/skills/manifests/research.js +281 -0
- package/dist/skills/manifests/research.js.map +1 -0
- package/dist/skills/manifests/resolver.js +146 -0
- package/dist/skills/manifests/resolver.js.map +1 -0
- package/dist/skills/marketplace.js +90 -0
- package/dist/skills/marketplace.js.map +1 -0
- package/dist/skills/orchestrator/spawn.js +178 -0
- package/dist/skills/orchestrator/spawn.js.map +1 -0
- package/dist/skills/orchestrator/startup.js +451 -0
- package/dist/skills/orchestrator/startup.js.map +1 -0
- package/dist/skills/orchestrator/validator.js +301 -0
- package/dist/skills/orchestrator/validator.js.map +1 -0
- package/dist/skills/precedence-integration.js +73 -0
- package/dist/skills/precedence-integration.js.map +1 -0
- package/dist/skills/precedence-types.js +16 -0
- package/dist/skills/precedence-types.js.map +1 -0
- package/dist/skills/routing-table.js +63 -0
- package/dist/skills/routing-table.js.map +1 -0
- package/dist/skills/skill-paths.js +217 -0
- package/dist/skills/skill-paths.js.map +1 -0
- package/dist/skills/test-utility.js +55 -0
- package/dist/skills/test-utility.js.map +1 -0
- package/dist/skills/types.js +118 -0
- package/dist/skills/types.js.map +1 -0
- package/dist/skills/validation.js +183 -0
- package/dist/skills/validation.js.map +1 -0
- package/dist/skills/version.js +57 -0
- package/dist/skills/version.js.map +1 -0
- package/dist/snapshot/index.js +188 -0
- package/dist/snapshot/index.js.map +1 -0
- package/dist/spawn/adapter-registry.js +246 -0
- package/dist/spawn/adapter-registry.js.map +1 -0
- package/dist/spawn/index.js +10 -0
- package/dist/spawn/index.js.map +1 -0
- package/dist/stats/index.js +343 -0
- package/dist/stats/index.js.map +1 -0
- package/dist/stats/workflow-telemetry.js +400 -0
- package/dist/stats/workflow-telemetry.js.map +1 -0
- package/dist/sticky/archive.js +47 -0
- package/dist/sticky/archive.js.map +1 -0
- package/dist/sticky/convert.js +235 -0
- package/dist/sticky/convert.js.map +1 -0
- package/dist/sticky/create.js +48 -0
- package/dist/sticky/create.js.map +1 -0
- package/dist/sticky/id.js +35 -0
- package/dist/sticky/id.js.map +1 -0
- package/dist/sticky/index.js +16 -0
- package/dist/sticky/index.js.map +1 -0
- package/dist/sticky/list.js +44 -0
- package/dist/sticky/list.js.map +1 -0
- package/dist/sticky/purge.js +45 -0
- package/dist/sticky/purge.js.map +1 -0
- package/dist/sticky/show.js +42 -0
- package/dist/sticky/show.js.map +1 -0
- package/dist/sticky/types.js +10 -0
- package/dist/sticky/types.js.map +1 -0
- package/dist/store/agent-registry-accessor.js +265 -0
- package/dist/store/agent-registry-accessor.js.map +1 -0
- package/dist/store/atomic.js +167 -0
- package/dist/store/atomic.js.map +1 -0
- package/dist/store/backup.js +94 -0
- package/dist/store/backup.js.map +1 -0
- package/dist/store/brain-accessor.js +397 -0
- package/dist/store/brain-accessor.js.map +1 -0
- package/dist/store/brain-schema.d.ts +8 -8
- package/dist/store/brain-schema.js +215 -0
- package/dist/store/brain-schema.js.map +1 -0
- package/dist/store/brain-sqlite.js +222 -0
- package/dist/store/brain-sqlite.js.map +1 -0
- package/dist/store/cache.js +168 -0
- package/dist/store/cache.js.map +1 -0
- package/dist/store/chain-schema.js +51 -0
- package/dist/store/chain-schema.js.map +1 -0
- package/dist/store/cleanup-legacy.d.ts +128 -0
- package/dist/store/cleanup-legacy.d.ts.map +1 -0
- package/dist/store/converters.js +124 -0
- package/dist/store/converters.js.map +1 -0
- package/dist/store/cross-db-cleanup.js +319 -0
- package/dist/store/cross-db-cleanup.js.map +1 -0
- package/dist/store/data-accessor.js +26 -0
- package/dist/store/data-accessor.js.map +1 -0
- package/dist/store/data-safety-central.js +269 -0
- package/dist/store/data-safety-central.js.map +1 -0
- package/dist/store/data-safety.js +274 -0
- package/dist/store/data-safety.js.map +1 -0
- package/dist/store/db-helpers.js +224 -0
- package/dist/store/db-helpers.js.map +1 -0
- package/dist/store/export.js +155 -0
- package/dist/store/export.js.map +1 -0
- package/dist/store/file-utils.js +270 -0
- package/dist/store/file-utils.js.map +1 -0
- package/dist/store/git-checkpoint.js +365 -0
- package/dist/store/git-checkpoint.js.map +1 -0
- package/dist/store/import-logging.js +139 -0
- package/dist/store/import-logging.js.map +1 -0
- package/dist/store/import-remap.js +145 -0
- package/dist/store/import-remap.js.map +1 -0
- package/dist/store/import-sort.js +121 -0
- package/dist/store/import-sort.js.map +1 -0
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +29 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/json.js +208 -0
- package/dist/store/json.js.map +1 -0
- package/dist/store/lifecycle-store.js +249 -0
- package/dist/store/lifecycle-store.js.map +1 -0
- package/dist/store/lock.js +70 -0
- package/dist/store/lock.js.map +1 -0
- package/dist/store/migration-manager.js +151 -0
- package/dist/store/migration-manager.js.map +1 -0
- package/dist/store/migration-sqlite.js +676 -0
- package/dist/store/migration-sqlite.js.map +1 -0
- package/dist/store/nexus-schema.js +62 -0
- package/dist/store/nexus-schema.js.map +1 -0
- package/dist/store/nexus-sqlite.d.ts +14 -2
- package/dist/store/nexus-sqlite.d.ts.map +1 -1
- package/dist/store/nexus-sqlite.js +217 -0
- package/dist/store/nexus-sqlite.js.map +1 -0
- package/dist/store/nexus-validation-schemas.js +40 -0
- package/dist/store/nexus-validation-schemas.js.map +1 -0
- package/dist/store/parsers.js +37 -0
- package/dist/store/parsers.js.map +1 -0
- package/dist/store/project-detect.js +457 -0
- package/dist/store/project-detect.js.map +1 -0
- package/dist/store/provider.js +101 -0
- package/dist/store/provider.js.map +1 -0
- package/dist/store/safety-data-accessor.js +257 -0
- package/dist/store/safety-data-accessor.js.map +1 -0
- package/dist/store/schema.js +7 -0
- package/dist/store/schema.js.map +1 -0
- package/dist/store/session-store.js +219 -0
- package/dist/store/session-store.js.map +1 -0
- package/dist/store/signaldock-sqlite.js +400 -0
- package/dist/store/signaldock-sqlite.js.map +1 -0
- package/dist/store/sqlite-backup.d.ts +121 -10
- package/dist/store/sqlite-backup.d.ts.map +1 -1
- package/dist/store/sqlite-backup.js +241 -0
- package/dist/store/sqlite-backup.js.map +1 -0
- package/dist/store/sqlite-data-accessor.js +787 -0
- package/dist/store/sqlite-data-accessor.js.map +1 -0
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +481 -0
- package/dist/store/sqlite.js.map +1 -0
- package/dist/store/status-registry.js +8 -0
- package/dist/store/status-registry.js.map +1 -0
- package/dist/store/task-store.js +358 -0
- package/dist/store/task-store.js.map +1 -0
- package/dist/store/tasks-schema.d.ts +8 -8
- package/dist/store/tasks-schema.js +610 -0
- package/dist/store/tasks-schema.js.map +1 -0
- package/dist/store/typed-query.js +15 -0
- package/dist/store/typed-query.js.map +1 -0
- package/dist/store/validation-schemas.d.ts +37 -37
- package/dist/store/validation-schemas.js +278 -0
- package/dist/store/validation-schemas.js.map +1 -0
- package/dist/system/archive-analytics.js +277 -0
- package/dist/system/archive-analytics.js.map +1 -0
- package/dist/system/archive-stats.js +64 -0
- package/dist/system/archive-stats.js.map +1 -0
- package/dist/system/audit.js +145 -0
- package/dist/system/audit.js.map +1 -0
- package/dist/system/backup.d.ts +91 -3
- package/dist/system/backup.d.ts.map +1 -1
- package/dist/system/backup.js +280 -0
- package/dist/system/backup.js.map +1 -0
- package/dist/system/cleanup.js +134 -0
- package/dist/system/cleanup.js.map +1 -0
- package/dist/system/health.js +1100 -0
- package/dist/system/health.js.map +1 -0
- package/dist/system/index.js +18 -0
- package/dist/system/index.js.map +1 -0
- package/dist/system/inject-generate.js +122 -0
- package/dist/system/inject-generate.js.map +1 -0
- package/dist/system/labels.js +38 -0
- package/dist/system/labels.js.map +1 -0
- package/dist/system/metrics.js +61 -0
- package/dist/system/metrics.js.map +1 -0
- package/dist/system/migrate.js +43 -0
- package/dist/system/migrate.js.map +1 -0
- package/dist/system/platform-paths.js +80 -0
- package/dist/system/platform-paths.js.map +1 -0
- package/dist/system/runtime.js +161 -0
- package/dist/system/runtime.js.map +1 -0
- package/dist/system/safestop.js +99 -0
- package/dist/system/safestop.js.map +1 -0
- package/dist/system/storage-preflight.js +123 -0
- package/dist/system/storage-preflight.js.map +1 -0
- package/dist/task-work/index.js +159 -0
- package/dist/task-work/index.js.map +1 -0
- package/dist/tasks/add.js +661 -0
- package/dist/tasks/add.js.map +1 -0
- package/dist/tasks/analyze.js +85 -0
- package/dist/tasks/analyze.js.map +1 -0
- package/dist/tasks/archive.js +90 -0
- package/dist/tasks/archive.js.map +1 -0
- package/dist/tasks/atomicity.js +83 -0
- package/dist/tasks/atomicity.js.map +1 -0
- package/dist/tasks/cancel-ops.js +83 -0
- package/dist/tasks/cancel-ops.js.map +1 -0
- package/dist/tasks/complete.js +211 -0
- package/dist/tasks/complete.js.map +1 -0
- package/dist/tasks/crossref-extract.js +73 -0
- package/dist/tasks/crossref-extract.js.map +1 -0
- package/dist/tasks/delete-preview.js +192 -0
- package/dist/tasks/delete-preview.js.map +1 -0
- package/dist/tasks/delete.js +120 -0
- package/dist/tasks/delete.js.map +1 -0
- package/dist/tasks/deletion-strategy.js +200 -0
- package/dist/tasks/deletion-strategy.js.map +1 -0
- package/dist/tasks/dependency-check.js +278 -0
- package/dist/tasks/dependency-check.js.map +1 -0
- package/dist/tasks/deps-ready.js +32 -0
- package/dist/tasks/deps-ready.js.map +1 -0
- package/dist/tasks/enforcement.js +86 -0
- package/dist/tasks/enforcement.js.map +1 -0
- package/dist/tasks/epic-enforcement.js +294 -0
- package/dist/tasks/epic-enforcement.js.map +1 -0
- package/dist/tasks/find.js +154 -0
- package/dist/tasks/find.js.map +1 -0
- package/dist/tasks/graph-cache.js +127 -0
- package/dist/tasks/graph-cache.js.map +1 -0
- package/dist/tasks/graph-ops.js +171 -0
- package/dist/tasks/graph-ops.js.map +1 -0
- package/dist/tasks/graph-rag.js +328 -0
- package/dist/tasks/graph-rag.js.map +1 -0
- package/dist/tasks/hierarchy-policy.js +149 -0
- package/dist/tasks/hierarchy-policy.js.map +1 -0
- package/dist/tasks/hierarchy.js +185 -0
- package/dist/tasks/hierarchy.js.map +1 -0
- package/dist/tasks/id-generator.js +65 -0
- package/dist/tasks/id-generator.js.map +1 -0
- package/dist/tasks/index.js +14 -0
- package/dist/tasks/index.js.map +1 -0
- package/dist/tasks/labels.js +52 -0
- package/dist/tasks/labels.js.map +1 -0
- package/dist/tasks/list.js +75 -0
- package/dist/tasks/list.js.map +1 -0
- package/dist/tasks/phase-tracking.js +133 -0
- package/dist/tasks/phase-tracking.js.map +1 -0
- package/dist/tasks/pipeline-stage.js +248 -0
- package/dist/tasks/pipeline-stage.js.map +1 -0
- package/dist/tasks/plan.js +268 -0
- package/dist/tasks/plan.js.map +1 -0
- package/dist/tasks/relates.js +89 -0
- package/dist/tasks/relates.js.map +1 -0
- package/dist/tasks/show.js +80 -0
- package/dist/tasks/show.js.map +1 -0
- package/dist/tasks/size-weighting.js +86 -0
- package/dist/tasks/size-weighting.js.map +1 -0
- package/dist/tasks/staleness.js +86 -0
- package/dist/tasks/staleness.js.map +1 -0
- package/dist/tasks/task-ops.js +1741 -0
- package/dist/tasks/task-ops.js.map +1 -0
- package/dist/tasks/update.js +277 -0
- package/dist/tasks/update.js.map +1 -0
- package/dist/templates/index.js +10 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/templates/parser.js +254 -0
- package/dist/templates/parser.js.map +1 -0
- package/dist/ui/aliases.js +153 -0
- package/dist/ui/aliases.js.map +1 -0
- package/dist/ui/changelog.js +184 -0
- package/dist/ui/changelog.js.map +1 -0
- package/dist/ui/command-registry.js +168 -0
- package/dist/ui/command-registry.js.map +1 -0
- package/dist/ui/flags.js +94 -0
- package/dist/ui/flags.js.map +1 -0
- package/dist/ui/index.js +24 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/upgrade.js +1148 -0
- package/dist/upgrade.js.map +1 -0
- package/dist/validation/chain-validation.js +146 -0
- package/dist/validation/chain-validation.js.map +1 -0
- package/dist/validation/compliance.js +155 -0
- package/dist/validation/compliance.js.map +1 -0
- package/dist/validation/docs-sync.js +212 -0
- package/dist/validation/docs-sync.js.map +1 -0
- package/dist/validation/doctor/checks.js +1069 -0
- package/dist/validation/doctor/checks.js.map +1 -0
- package/dist/validation/doctor/index.js +9 -0
- package/dist/validation/doctor/index.js.map +1 -0
- package/dist/validation/doctor/project-cache.js +160 -0
- package/dist/validation/doctor/project-cache.js.map +1 -0
- package/dist/validation/doctor/utils.js +155 -0
- package/dist/validation/doctor/utils.js.map +1 -0
- package/dist/validation/engine.js +902 -0
- package/dist/validation/engine.js.map +1 -0
- package/dist/validation/gap-check.js +175 -0
- package/dist/validation/gap-check.js.map +1 -0
- package/dist/validation/index.js +40 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/validation/manifest.js +237 -0
- package/dist/validation/manifest.js.map +1 -0
- package/dist/validation/operation-gate-validators.js +724 -0
- package/dist/validation/operation-gate-validators.js.map +1 -0
- package/dist/validation/operation-verification-gates.js +532 -0
- package/dist/validation/operation-verification-gates.js.map +1 -0
- package/dist/validation/param-utils.js +139 -0
- package/dist/validation/param-utils.js.map +1 -0
- package/dist/validation/protocol-common.js +300 -0
- package/dist/validation/protocol-common.js.map +1 -0
- package/dist/validation/protocols/_shared.js +75 -0
- package/dist/validation/protocols/_shared.js.map +1 -0
- package/dist/validation/protocols/architecture-decision.js +31 -0
- package/dist/validation/protocols/architecture-decision.js.map +1 -0
- package/dist/validation/protocols/artifact-publish.js +28 -0
- package/dist/validation/protocols/artifact-publish.js.map +1 -0
- package/dist/validation/protocols/consensus.js +41 -0
- package/dist/validation/protocols/consensus.js.map +1 -0
- package/dist/validation/protocols/contribution.js +27 -0
- package/dist/validation/protocols/contribution.js.map +1 -0
- package/dist/validation/protocols/decomposition.js +28 -0
- package/dist/validation/protocols/decomposition.js.map +1 -0
- package/dist/validation/protocols/implementation.js +24 -0
- package/dist/validation/protocols/implementation.js.map +1 -0
- package/dist/validation/protocols/provenance.js +29 -0
- package/dist/validation/protocols/provenance.js.map +1 -0
- package/dist/validation/protocols/release.js +29 -0
- package/dist/validation/protocols/release.js.map +1 -0
- package/dist/validation/protocols/research.js +24 -0
- package/dist/validation/protocols/research.js.map +1 -0
- package/dist/validation/protocols/specification.js +27 -0
- package/dist/validation/protocols/specification.js.map +1 -0
- package/dist/validation/protocols/testing.js +30 -0
- package/dist/validation/protocols/testing.js.map +1 -0
- package/dist/validation/protocols/validation.js +30 -0
- package/dist/validation/protocols/validation.js.map +1 -0
- package/dist/validation/schema-integrity.js +170 -0
- package/dist/validation/schema-integrity.js.map +1 -0
- package/dist/validation/schema-validator.js +176 -0
- package/dist/validation/schema-validator.js.map +1 -0
- package/dist/validation/validate-ops.js +937 -0
- package/dist/validation/validate-ops.js.map +1 -0
- package/dist/validation/validation-rules.js +226 -0
- package/dist/validation/validation-rules.js.map +1 -0
- package/dist/validation/verification.js +321 -0
- package/dist/validation/verification.js.map +1 -0
- package/package.json +10 -8
- package/src/__tests__/paths-walkup.test.ts +305 -0
- package/src/__tests__/paths.test.ts +61 -17
- package/src/hooks/handlers/session-hooks.ts +42 -0
- package/src/internal.ts +19 -2
- package/src/paths.ts +91 -14
- package/src/scaffold.ts +22 -3
- package/src/store/__tests__/cleanup-legacy.test.ts +268 -0
- package/src/store/__tests__/database-topology-integration.test.ts +504 -0
- package/src/store/__tests__/sqlite-backup-global.test.ts +281 -0
- package/src/store/__tests__/sqlite-backup.test.ts +118 -10
- package/src/store/cleanup-legacy.ts +208 -0
- package/src/store/index.ts +7 -0
- package/src/store/nexus-sqlite.ts +32 -3
- package/src/store/sqlite-backup.ts +368 -37
- package/src/store/sqlite.ts +19 -3
- package/src/system/__tests__/backup.test.ts +237 -0
- package/src/system/backup.ts +248 -28
- package/templates/cleo-gitignore +19 -3
|
@@ -0,0 +1,1741 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core task non-CRUD operations - business logic extracted from task-engine.ts.
|
|
3
|
+
*
|
|
4
|
+
* These are pure business logic functions that throw on failure and return
|
|
5
|
+
* data directly (no EngineResult wrapper). The engine layer wraps these
|
|
6
|
+
* in try/catch to produce EngineResult.
|
|
7
|
+
*
|
|
8
|
+
* Covers: next, blockers, tree, deps, relates, relatesAdd, analyze, restore,
|
|
9
|
+
* unarchive, reorder, reparent, promote, reopen, complexityEstimate, depends,
|
|
10
|
+
* stats, export, history, lint, batchValidate, import
|
|
11
|
+
*
|
|
12
|
+
* @task T4790
|
|
13
|
+
* @epic T4654
|
|
14
|
+
*/
|
|
15
|
+
import { TASK_STATUSES } from '@cleocode/contracts';
|
|
16
|
+
import { getAccessor } from '../store/data-accessor.js';
|
|
17
|
+
import { getDataPath, readJsonFile as storeReadJsonFile } from '../store/file-utils.js';
|
|
18
|
+
import { canCancel } from './cancel-ops.js';
|
|
19
|
+
import { detectCircularDeps, getBlockedTasks, getLeafBlockers, getReadyTasks, getTransitiveBlockers, validateDependencies, } from './dependency-check.js';
|
|
20
|
+
import { depsReady } from './deps-ready.js';
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Helpers
|
|
23
|
+
// ============================================================================
|
|
24
|
+
const PRIORITY_SCORE = {
|
|
25
|
+
critical: 100,
|
|
26
|
+
high: 75,
|
|
27
|
+
medium: 50,
|
|
28
|
+
low: 25,
|
|
29
|
+
};
|
|
30
|
+
async function loadAllTasks(projectRoot) {
|
|
31
|
+
const accessor = await getAccessor(projectRoot);
|
|
32
|
+
const { tasks } = await accessor.queryTasks({});
|
|
33
|
+
return tasks;
|
|
34
|
+
}
|
|
35
|
+
function buildTreeNode(task, childrenMap) {
|
|
36
|
+
const children = (childrenMap.get(task.id) ?? []).map((child) => buildTreeNode(child, childrenMap));
|
|
37
|
+
return {
|
|
38
|
+
id: task.id,
|
|
39
|
+
title: task.title,
|
|
40
|
+
status: task.status,
|
|
41
|
+
type: task.type,
|
|
42
|
+
children,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function buildUpstreamTree(taskId, taskMap, visited = new Set()) {
|
|
46
|
+
const task = taskMap.get(taskId);
|
|
47
|
+
if (!task?.depends?.length)
|
|
48
|
+
return [];
|
|
49
|
+
const nodes = [];
|
|
50
|
+
for (const depId of task.depends) {
|
|
51
|
+
if (visited.has(depId))
|
|
52
|
+
continue;
|
|
53
|
+
visited.add(depId);
|
|
54
|
+
const dep = taskMap.get(depId);
|
|
55
|
+
if (!dep)
|
|
56
|
+
continue;
|
|
57
|
+
nodes.push({
|
|
58
|
+
id: dep.id,
|
|
59
|
+
title: dep.title,
|
|
60
|
+
status: dep.status,
|
|
61
|
+
type: dep.type,
|
|
62
|
+
children: buildUpstreamTree(depId, taskMap, visited),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return nodes;
|
|
66
|
+
}
|
|
67
|
+
function countNodes(nodes) {
|
|
68
|
+
let count = nodes.length;
|
|
69
|
+
for (const node of nodes) {
|
|
70
|
+
count += countNodes(node.children);
|
|
71
|
+
}
|
|
72
|
+
return count;
|
|
73
|
+
}
|
|
74
|
+
function measureDependencyDepth(taskId, taskMap, visited = new Set()) {
|
|
75
|
+
if (visited.has(taskId))
|
|
76
|
+
return 0;
|
|
77
|
+
visited.add(taskId);
|
|
78
|
+
const task = taskMap.get(taskId);
|
|
79
|
+
if (!task || !task.depends || task.depends.length === 0)
|
|
80
|
+
return 0;
|
|
81
|
+
let maxDepth = 0;
|
|
82
|
+
for (const depId of task.depends) {
|
|
83
|
+
const depth = 1 + measureDependencyDepth(depId, taskMap, visited);
|
|
84
|
+
if (depth > maxDepth)
|
|
85
|
+
maxDepth = depth;
|
|
86
|
+
}
|
|
87
|
+
return maxDepth;
|
|
88
|
+
}
|
|
89
|
+
function getHierarchyLimits(projectRoot) {
|
|
90
|
+
const configPath = getDataPath(projectRoot, 'config.json');
|
|
91
|
+
const config = storeReadJsonFile(configPath);
|
|
92
|
+
let maxDepth = 3;
|
|
93
|
+
let maxSiblings = 0;
|
|
94
|
+
if (config) {
|
|
95
|
+
const hierarchy = config.hierarchy;
|
|
96
|
+
if (hierarchy) {
|
|
97
|
+
if (typeof hierarchy.maxDepth === 'number')
|
|
98
|
+
maxDepth = hierarchy.maxDepth;
|
|
99
|
+
if (typeof hierarchy.maxSiblings === 'number')
|
|
100
|
+
maxSiblings = hierarchy.maxSiblings;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { maxDepth, maxSiblings };
|
|
104
|
+
}
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// taskNext
|
|
107
|
+
// ============================================================================
|
|
108
|
+
/**
|
|
109
|
+
* Suggest next task to work on based on priority, phase, age, and deps.
|
|
110
|
+
*
|
|
111
|
+
* @param projectRoot - Absolute path to the CLEO project root directory
|
|
112
|
+
* @param params - Optional scoring configuration
|
|
113
|
+
* @param params.count - Number of suggestions to return (default: 1)
|
|
114
|
+
* @param params.explain - When true, include scoring reasons in each suggestion
|
|
115
|
+
* @returns Ranked suggestions with scores and the total number of eligible candidates
|
|
116
|
+
*
|
|
117
|
+
* @remarks
|
|
118
|
+
* Scoring considers priority weight, current phase alignment, dependency readiness,
|
|
119
|
+
* task age, and brain success/failure pattern matches. Results are sorted descending by score.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```typescript
|
|
123
|
+
* const { suggestions } = await coreTaskNext('/project', { count: 3, explain: true });
|
|
124
|
+
* console.log(suggestions[0].id, suggestions[0].score);
|
|
125
|
+
* ```
|
|
126
|
+
*
|
|
127
|
+
* @task T4790
|
|
128
|
+
*/
|
|
129
|
+
export async function coreTaskNext(projectRoot, params) {
|
|
130
|
+
const accessor = await getAccessor(projectRoot);
|
|
131
|
+
const allTasks = await loadAllTasks(projectRoot);
|
|
132
|
+
const taskMap = new Map(allTasks.map((t) => [t.id, t]));
|
|
133
|
+
const projectMeta = await accessor.getMetaValue('project_meta');
|
|
134
|
+
const currentPhase = projectMeta?.currentPhase ?? null;
|
|
135
|
+
const candidates = allTasks.filter((t) => t.status === 'pending' && depsReady(t.depends, taskMap));
|
|
136
|
+
if (candidates.length === 0) {
|
|
137
|
+
return { suggestions: [], totalCandidates: 0 };
|
|
138
|
+
}
|
|
139
|
+
const scored = candidates
|
|
140
|
+
.map((task) => {
|
|
141
|
+
const reasons = [];
|
|
142
|
+
let score = 0;
|
|
143
|
+
score += PRIORITY_SCORE[task.priority] ?? 50;
|
|
144
|
+
reasons.push(`priority: ${task.priority} (+${PRIORITY_SCORE[task.priority] ?? 50})`);
|
|
145
|
+
if (currentPhase && task.phase === currentPhase) {
|
|
146
|
+
score += 20;
|
|
147
|
+
reasons.push(`phase alignment: ${currentPhase} (+20)`);
|
|
148
|
+
}
|
|
149
|
+
if (depsReady(task.depends, taskMap)) {
|
|
150
|
+
score += 10;
|
|
151
|
+
reasons.push('all dependencies satisfied (+10)');
|
|
152
|
+
}
|
|
153
|
+
if (task.createdAt) {
|
|
154
|
+
const ageMs = Date.now() - new Date(task.createdAt).getTime();
|
|
155
|
+
const ageDays = ageMs / (1000 * 60 * 60 * 24);
|
|
156
|
+
if (ageDays > 7) {
|
|
157
|
+
const ageBonus = Math.min(15, Math.floor(ageDays / 7));
|
|
158
|
+
score += ageBonus;
|
|
159
|
+
reasons.push(`age: ${Math.floor(ageDays)} days (+${ageBonus})`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { task, score, reasons };
|
|
163
|
+
})
|
|
164
|
+
.sort((a, b) => b.score - a.score);
|
|
165
|
+
// Brain pattern scoring (best-effort)
|
|
166
|
+
try {
|
|
167
|
+
const { searchPatterns } = await import('../memory/patterns.js');
|
|
168
|
+
const [successPatterns, failurePatterns] = await Promise.all([
|
|
169
|
+
searchPatterns(projectRoot, { type: 'success', limit: 20 }),
|
|
170
|
+
searchPatterns(projectRoot, { type: 'failure', limit: 20 }),
|
|
171
|
+
]);
|
|
172
|
+
if (successPatterns.length > 0 || failurePatterns.length > 0) {
|
|
173
|
+
for (const item of scored) {
|
|
174
|
+
const titleLower = item.task.title.toLowerCase();
|
|
175
|
+
const labels = (item.task.labels ?? []).map((l) => l.toLowerCase());
|
|
176
|
+
const matchText = [titleLower, ...labels].join(' ');
|
|
177
|
+
for (const sp of successPatterns) {
|
|
178
|
+
if (matchText.includes(sp.pattern.toLowerCase())) {
|
|
179
|
+
item.score += 10;
|
|
180
|
+
item.reasons.push(`brain: success pattern match "${sp.pattern}" (+10)`);
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
for (const fp of failurePatterns) {
|
|
185
|
+
if (matchText.includes(fp.pattern.toLowerCase())) {
|
|
186
|
+
item.score -= 5;
|
|
187
|
+
item.reasons.push(`brain: failure pattern match "${fp.pattern}" (-5)`);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
scored.sort((a, b) => b.score - a.score);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// Brain pattern scoring is best-effort
|
|
197
|
+
}
|
|
198
|
+
const count = Math.min(params?.count || 1, scored.length);
|
|
199
|
+
const explain = params?.explain ?? false;
|
|
200
|
+
const suggestions = scored.slice(0, count).map(({ task, score, reasons }) => ({
|
|
201
|
+
id: task.id,
|
|
202
|
+
title: task.title,
|
|
203
|
+
priority: task.priority,
|
|
204
|
+
phase: task.phase ?? null,
|
|
205
|
+
score,
|
|
206
|
+
...(explain && { reasons }),
|
|
207
|
+
}));
|
|
208
|
+
return { suggestions, totalCandidates: candidates.length };
|
|
209
|
+
}
|
|
210
|
+
// ============================================================================
|
|
211
|
+
// taskBlockers
|
|
212
|
+
// ============================================================================
|
|
213
|
+
/**
|
|
214
|
+
* Show blocked tasks and analyze blocking chains.
|
|
215
|
+
*
|
|
216
|
+
* @param projectRoot - Absolute path to the CLEO project root directory
|
|
217
|
+
* @param params - Optional analysis configuration
|
|
218
|
+
* @param params.analyze - When true, compute transitive blocking chains
|
|
219
|
+
* @param params.limit - Maximum number of blocked tasks to return (default: 20)
|
|
220
|
+
* @returns Blocked tasks with optional blocking chains, critical bottleneck tasks, and a summary
|
|
221
|
+
*
|
|
222
|
+
* @remarks
|
|
223
|
+
* Collects both explicitly blocked tasks and dependency-blocked pending tasks.
|
|
224
|
+
* Critical blockers are the top 5 tasks that appear most frequently in blocking chains.
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* ```typescript
|
|
228
|
+
* const result = await coreTaskBlockers('/project', { analyze: true, limit: 10 });
|
|
229
|
+
* console.log(result.summary, result.criticalBlockers);
|
|
230
|
+
* ```
|
|
231
|
+
*
|
|
232
|
+
* @task T4790
|
|
233
|
+
*/
|
|
234
|
+
export async function coreTaskBlockers(projectRoot, params) {
|
|
235
|
+
const allTasks = await loadAllTasks(projectRoot);
|
|
236
|
+
const taskMap = new Map(allTasks.map((t) => [t.id, t]));
|
|
237
|
+
const analyze = params?.analyze ?? false;
|
|
238
|
+
const effectiveLimit = params?.limit ?? 20;
|
|
239
|
+
const blockedTasks = allTasks.filter((t) => t.status === 'blocked');
|
|
240
|
+
const depBlockedTasks = allTasks.filter((t) => t.status === 'pending' &&
|
|
241
|
+
t.depends &&
|
|
242
|
+
t.depends.length > 0 &&
|
|
243
|
+
t.depends.some((depId) => {
|
|
244
|
+
const dep = taskMap.get(depId);
|
|
245
|
+
return dep && dep.status !== 'done' && dep.status !== 'cancelled';
|
|
246
|
+
}));
|
|
247
|
+
const tasksAsTask = allTasks;
|
|
248
|
+
const blockerInfos = [
|
|
249
|
+
...blockedTasks.map((t) => ({
|
|
250
|
+
id: t.id,
|
|
251
|
+
title: t.title,
|
|
252
|
+
status: t.status,
|
|
253
|
+
depends: t.depends,
|
|
254
|
+
blockingChain: analyze ? getTransitiveBlockers(t.id, tasksAsTask) : [],
|
|
255
|
+
})),
|
|
256
|
+
...depBlockedTasks
|
|
257
|
+
.filter((t) => !blockedTasks.some((bt) => bt.id === t.id))
|
|
258
|
+
.map((t) => ({
|
|
259
|
+
id: t.id,
|
|
260
|
+
title: t.title,
|
|
261
|
+
status: t.status,
|
|
262
|
+
depends: t.depends,
|
|
263
|
+
blockingChain: analyze ? getTransitiveBlockers(t.id, tasksAsTask) : [],
|
|
264
|
+
})),
|
|
265
|
+
];
|
|
266
|
+
const total = blockerInfos.length;
|
|
267
|
+
const pagedBlockerInfos = blockerInfos.slice(0, effectiveLimit);
|
|
268
|
+
const blockerCounts = new Map();
|
|
269
|
+
for (const info of pagedBlockerInfos) {
|
|
270
|
+
for (const depId of info.blockingChain) {
|
|
271
|
+
blockerCounts.set(depId, (blockerCounts.get(depId) ?? 0) + 1);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const criticalBlockers = [...blockerCounts.entries()]
|
|
275
|
+
.sort((a, b) => b[1] - a[1])
|
|
276
|
+
.slice(0, 5)
|
|
277
|
+
.map(([id, count]) => {
|
|
278
|
+
const task = taskMap.get(id);
|
|
279
|
+
return { id, title: task?.title ?? 'Unknown', blocksCount: count };
|
|
280
|
+
});
|
|
281
|
+
return {
|
|
282
|
+
blockedTasks: pagedBlockerInfos,
|
|
283
|
+
criticalBlockers,
|
|
284
|
+
summary: total === 0 ? 'No blocked tasks found' : `${total} blocked task(s)`,
|
|
285
|
+
total,
|
|
286
|
+
limit: effectiveLimit,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
// ============================================================================
|
|
290
|
+
// taskTree
|
|
291
|
+
// ============================================================================
|
|
292
|
+
/**
|
|
293
|
+
* Build hierarchy tree for tasks.
|
|
294
|
+
*
|
|
295
|
+
* @param projectRoot - Absolute path to the CLEO project root directory
|
|
296
|
+
* @param taskId - Optional root task ID; when provided, builds the subtree rooted at this task
|
|
297
|
+
* @returns The tree nodes and total node count
|
|
298
|
+
*
|
|
299
|
+
* @remarks
|
|
300
|
+
* When no taskId is given, returns all root-level tasks with their full subtrees.
|
|
301
|
+
* When a taskId is given, returns that single task as the root with its descendants.
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* ```typescript
|
|
305
|
+
* const { tree, totalNodes } = await coreTaskTree('/project', 'T042');
|
|
306
|
+
* console.log(`${totalNodes} nodes in subtree`);
|
|
307
|
+
* ```
|
|
308
|
+
*
|
|
309
|
+
* @task T4790
|
|
310
|
+
*/
|
|
311
|
+
export async function coreTaskTree(projectRoot, taskId) {
|
|
312
|
+
const allTasks = await loadAllTasks(projectRoot);
|
|
313
|
+
if (taskId) {
|
|
314
|
+
const task = allTasks.find((t) => t.id === taskId);
|
|
315
|
+
if (!task) {
|
|
316
|
+
throw new Error(`Task '${taskId}' not found`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const childrenMap = new Map();
|
|
320
|
+
for (const task of allTasks) {
|
|
321
|
+
const parentKey = task.parentId ?? '__root__';
|
|
322
|
+
if (!childrenMap.has(parentKey)) {
|
|
323
|
+
childrenMap.set(parentKey, []);
|
|
324
|
+
}
|
|
325
|
+
childrenMap.get(parentKey).push(task);
|
|
326
|
+
}
|
|
327
|
+
let roots;
|
|
328
|
+
if (taskId) {
|
|
329
|
+
roots = [allTasks.find((t) => t.id === taskId)];
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
roots = childrenMap.get('__root__') ?? [];
|
|
333
|
+
}
|
|
334
|
+
const tree = roots.map((root) => buildTreeNode(root, childrenMap));
|
|
335
|
+
return { tree, totalNodes: countNodes(tree) };
|
|
336
|
+
}
|
|
337
|
+
// ============================================================================
|
|
338
|
+
// taskDeps
|
|
339
|
+
// ============================================================================
|
|
340
|
+
/**
|
|
341
|
+
* Show dependencies for a task.
|
|
342
|
+
*
|
|
343
|
+
* @param projectRoot - Absolute path to the CLEO project root directory
|
|
344
|
+
* @param taskId - The task ID to inspect dependencies for
|
|
345
|
+
* @returns Upstream and downstream dependencies, unresolved deps, and readiness flag
|
|
346
|
+
*
|
|
347
|
+
* @remarks
|
|
348
|
+
* Returns both the tasks this task depends on (upstream) and the tasks that depend
|
|
349
|
+
* on it (downstream). Unresolved deps are those not yet done or cancelled.
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* ```typescript
|
|
353
|
+
* const deps = await coreTaskDeps('/project', 'T100');
|
|
354
|
+
* if (!deps.allDepsReady) console.log('Blocked by:', deps.unresolvedDeps);
|
|
355
|
+
* ```
|
|
356
|
+
*
|
|
357
|
+
* @task T4790
|
|
358
|
+
*/
|
|
359
|
+
export async function coreTaskDeps(projectRoot, taskId) {
|
|
360
|
+
const allTasks = await loadAllTasks(projectRoot);
|
|
361
|
+
const task = allTasks.find((t) => t.id === taskId);
|
|
362
|
+
if (!task) {
|
|
363
|
+
throw new Error(`Task '${taskId}' not found`);
|
|
364
|
+
}
|
|
365
|
+
const taskMap = new Map(allTasks.map((t) => [t.id, t]));
|
|
366
|
+
const completedIds = new Set(allTasks.filter((t) => t.status === 'done' || t.status === 'cancelled').map((t) => t.id));
|
|
367
|
+
const dependsOn = (task.depends ?? [])
|
|
368
|
+
.map((depId) => {
|
|
369
|
+
const dep = taskMap.get(depId);
|
|
370
|
+
return dep ? { id: dep.id, title: dep.title, status: dep.status } : null;
|
|
371
|
+
})
|
|
372
|
+
.filter((d) => d !== null);
|
|
373
|
+
const dependedOnBy = allTasks
|
|
374
|
+
.filter((t) => t.depends?.includes(taskId))
|
|
375
|
+
.map((t) => ({ id: t.id, title: t.title, status: t.status }));
|
|
376
|
+
const unresolvedDeps = (task.depends ?? []).filter((depId) => !completedIds.has(depId));
|
|
377
|
+
return {
|
|
378
|
+
taskId,
|
|
379
|
+
dependsOn,
|
|
380
|
+
dependedOnBy,
|
|
381
|
+
unresolvedDeps,
|
|
382
|
+
allDepsReady: unresolvedDeps.length === 0,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
// ============================================================================
|
|
386
|
+
// taskRelates
|
|
387
|
+
// ============================================================================
|
|
388
|
+
/**
|
|
389
|
+
* Show task relations.
|
|
390
|
+
*
|
|
391
|
+
* @param projectRoot - Absolute path to the CLEO project root directory
|
|
392
|
+
* @param taskId - The task ID to retrieve relations for
|
|
393
|
+
* @returns The task's relations array and count
|
|
394
|
+
*
|
|
395
|
+
* @remarks
|
|
396
|
+
* Relations are non-dependency links between tasks (e.g. "related-to", "duplicates").
|
|
397
|
+
* Unlike dependencies, relations do not affect blocking or scheduling.
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* ```typescript
|
|
401
|
+
* const { relations, count } = await coreTaskRelates('/project', 'T050');
|
|
402
|
+
* console.log(`${count} relations found`);
|
|
403
|
+
* ```
|
|
404
|
+
*
|
|
405
|
+
* @task T4790
|
|
406
|
+
*/
|
|
407
|
+
export async function coreTaskRelates(projectRoot, taskId) {
|
|
408
|
+
const allTasks = await loadAllTasks(projectRoot);
|
|
409
|
+
const task = allTasks.find((t) => t.id === taskId);
|
|
410
|
+
if (!task) {
|
|
411
|
+
throw new Error(`Task '${taskId}' not found`);
|
|
412
|
+
}
|
|
413
|
+
const relations = task.relates ?? [];
|
|
414
|
+
return { taskId, relations, count: relations.length };
|
|
415
|
+
}
|
|
416
|
+
// ============================================================================
|
|
417
|
+
// taskRelatesAdd
|
|
418
|
+
// ============================================================================
|
|
419
|
+
/**
|
|
420
|
+
* Add a relation between two tasks.
|
|
421
|
+
*
|
|
422
|
+
* @param projectRoot - Absolute path to the CLEO project root directory
|
|
423
|
+
* @param taskId - The source task ID
|
|
424
|
+
* @param relatedId - The target task ID to relate to
|
|
425
|
+
* @param type - Relation type (e.g. "related-to", "duplicates", "blocks")
|
|
426
|
+
* @param reason - Optional human-readable reason for the relation
|
|
427
|
+
* @returns Confirmation of the added relation with source, target, and type
|
|
428
|
+
*
|
|
429
|
+
* @remarks
|
|
430
|
+
* Persists the relation both on the task's `relates` array and in the
|
|
431
|
+
* `task_relations` table for bidirectional querying.
|
|
432
|
+
*
|
|
433
|
+
* @example
|
|
434
|
+
* ```typescript
|
|
435
|
+
* const result = await coreTaskRelatesAdd('/project', 'T010', 'T020', 'related-to', 'Shared scope');
|
|
436
|
+
* console.log(result.added); // true
|
|
437
|
+
* ```
|
|
438
|
+
*
|
|
439
|
+
* @task T4790
|
|
440
|
+
*/
|
|
441
|
+
export async function coreTaskRelatesAdd(projectRoot, taskId, relatedId, type, reason) {
|
|
442
|
+
const accessor = await getAccessor(projectRoot);
|
|
443
|
+
const fromTask = await accessor.loadSingleTask(taskId);
|
|
444
|
+
if (!fromTask) {
|
|
445
|
+
throw new Error(`Task '${taskId}' not found`);
|
|
446
|
+
}
|
|
447
|
+
const toExists = await accessor.taskExists(relatedId);
|
|
448
|
+
if (!toExists) {
|
|
449
|
+
throw new Error(`Task '${relatedId}' not found`);
|
|
450
|
+
}
|
|
451
|
+
if (!fromTask.relates) {
|
|
452
|
+
fromTask.relates = [];
|
|
453
|
+
}
|
|
454
|
+
fromTask.relates.push({
|
|
455
|
+
taskId: relatedId,
|
|
456
|
+
type,
|
|
457
|
+
reason: reason || undefined,
|
|
458
|
+
});
|
|
459
|
+
fromTask.updatedAt = new Date().toISOString();
|
|
460
|
+
await accessor.upsertSingleTask(fromTask);
|
|
461
|
+
// Persist to task_relations table (T5168)
|
|
462
|
+
await accessor.addRelation(taskId, relatedId, type, reason);
|
|
463
|
+
return { from: taskId, to: relatedId, type, reason, added: true };
|
|
464
|
+
}
|
|
465
|
+
// ============================================================================
|
|
466
|
+
// taskAnalyze
|
|
467
|
+
// ============================================================================
|
|
468
|
+
/**
|
|
469
|
+
* Analyze tasks for priority and leverage.
|
|
470
|
+
*
|
|
471
|
+
* @param projectRoot - Absolute path to the CLEO project root directory
|
|
472
|
+
* @param taskId - Optional task or epic ID to scope the analysis; omit for project-wide
|
|
473
|
+
* @param params - Optional analysis configuration
|
|
474
|
+
* @param params.tierLimit - Maximum tasks per priority tier in the response (default: 10)
|
|
475
|
+
* @returns Analysis with recommended next task, bottlenecks, priority tiers, and aggregate metrics
|
|
476
|
+
*
|
|
477
|
+
* @remarks
|
|
478
|
+
* Computes a leverage score per task (how many other tasks it unblocks) and combines
|
|
479
|
+
* it with priority to produce a ranked recommendation. Bottlenecks are the top 5
|
|
480
|
+
* incomplete tasks that block the most others.
|
|
481
|
+
*
|
|
482
|
+
* @example
|
|
483
|
+
* ```typescript
|
|
484
|
+
* const analysis = await coreTaskAnalyze('/project', undefined, { tierLimit: 5 });
|
|
485
|
+
* if (analysis.recommended) console.log('Work on:', analysis.recommended.id);
|
|
486
|
+
* ```
|
|
487
|
+
*
|
|
488
|
+
* @task T4790
|
|
489
|
+
*/
|
|
490
|
+
export async function coreTaskAnalyze(projectRoot, taskId, params) {
|
|
491
|
+
const allTasks = await loadAllTasks(projectRoot);
|
|
492
|
+
const effectiveTierLimit = params?.tierLimit ?? 10;
|
|
493
|
+
const tasks = taskId
|
|
494
|
+
? allTasks.filter((t) => t.id === taskId || t.parentId === taskId)
|
|
495
|
+
: allTasks;
|
|
496
|
+
const blocksMap = {};
|
|
497
|
+
for (const task of tasks) {
|
|
498
|
+
if (task.depends) {
|
|
499
|
+
for (const dep of task.depends) {
|
|
500
|
+
if (!blocksMap[dep])
|
|
501
|
+
blocksMap[dep] = [];
|
|
502
|
+
blocksMap[dep].push(task.id);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const leverageMap = {};
|
|
507
|
+
for (const task of tasks) {
|
|
508
|
+
leverageMap[task.id] = (blocksMap[task.id] ?? []).length;
|
|
509
|
+
}
|
|
510
|
+
const actionable = tasks.filter((t) => t.status === 'pending' || t.status === 'active');
|
|
511
|
+
const blocked = tasks.filter((t) => t.status === 'blocked');
|
|
512
|
+
const bottlenecks = tasks
|
|
513
|
+
.filter((t) => (blocksMap[t.id]?.length ?? 0) > 0 && t.status !== 'done')
|
|
514
|
+
.map((t) => ({ id: t.id, title: t.title, blocksCount: blocksMap[t.id].length }))
|
|
515
|
+
.sort((a, b) => b.blocksCount - a.blocksCount)
|
|
516
|
+
.slice(0, 5);
|
|
517
|
+
const scored = actionable.map((t) => ({
|
|
518
|
+
id: t.id,
|
|
519
|
+
title: t.title,
|
|
520
|
+
leverage: leverageMap[t.id] ?? 0,
|
|
521
|
+
priority: t.priority,
|
|
522
|
+
}));
|
|
523
|
+
scored.sort((a, b) => {
|
|
524
|
+
const priorityWeight = { critical: 100, high: 50, medium: 20, low: 5 };
|
|
525
|
+
const aScore = (priorityWeight[a.priority ?? 'medium'] ?? 20) + a.leverage * 10;
|
|
526
|
+
const bScore = (priorityWeight[b.priority ?? 'medium'] ?? 20) + b.leverage * 10;
|
|
527
|
+
return bScore - aScore;
|
|
528
|
+
});
|
|
529
|
+
const critical = scored.filter((t) => t.priority === 'critical');
|
|
530
|
+
const high = scored.filter((t) => t.priority === 'high');
|
|
531
|
+
const normal = scored.filter((t) => t.priority !== 'critical' && t.priority !== 'high');
|
|
532
|
+
const recommended = scored.length > 0
|
|
533
|
+
? {
|
|
534
|
+
id: scored[0].id,
|
|
535
|
+
title: scored[0].title,
|
|
536
|
+
leverage: scored[0].leverage,
|
|
537
|
+
reason: 'Highest combined priority and leverage score',
|
|
538
|
+
}
|
|
539
|
+
: null;
|
|
540
|
+
const totalLeverage = Object.values(leverageMap).reduce((s, v) => s + v, 0);
|
|
541
|
+
const avgLeverage = tasks.length > 0 ? Math.round((totalLeverage / tasks.length) * 100) / 100 : 0;
|
|
542
|
+
return {
|
|
543
|
+
recommended,
|
|
544
|
+
bottlenecks,
|
|
545
|
+
tiers: {
|
|
546
|
+
critical: critical
|
|
547
|
+
.slice(0, effectiveTierLimit)
|
|
548
|
+
.map(({ id, title, leverage }) => ({ id, title, leverage })),
|
|
549
|
+
high: high
|
|
550
|
+
.slice(0, effectiveTierLimit)
|
|
551
|
+
.map(({ id, title, leverage }) => ({ id, title, leverage })),
|
|
552
|
+
normal: normal
|
|
553
|
+
.slice(0, effectiveTierLimit)
|
|
554
|
+
.map(({ id, title, leverage }) => ({ id, title, leverage })),
|
|
555
|
+
},
|
|
556
|
+
metrics: {
|
|
557
|
+
totalTasks: tasks.length,
|
|
558
|
+
actionable: actionable.length,
|
|
559
|
+
blocked: blocked.length,
|
|
560
|
+
avgLeverage,
|
|
561
|
+
},
|
|
562
|
+
tierLimit: effectiveTierLimit,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
// ============================================================================
|
|
566
|
+
// taskRestore
|
|
567
|
+
// ============================================================================
|
|
568
|
+
/**
|
|
569
|
+
* Restore a cancelled task back to pending.
|
|
570
|
+
*
|
|
571
|
+
* @param projectRoot - Absolute path to the CLEO project root directory
|
|
572
|
+
* @param taskId - The cancelled task ID to restore
|
|
573
|
+
* @param params - Optional restore options
|
|
574
|
+
* @param params.cascade - When true, also restores cancelled child tasks recursively
|
|
575
|
+
* @param params.notes - Optional note appended to each restored task's notes array
|
|
576
|
+
* @returns The task ID, list of restored task IDs, and total count
|
|
577
|
+
*
|
|
578
|
+
* @remarks
|
|
579
|
+
* Only tasks with status "cancelled" can be restored. Restored tasks are set to
|
|
580
|
+
* "pending" with cancellation metadata cleared. A timestamped note is appended.
|
|
581
|
+
*
|
|
582
|
+
* @example
|
|
583
|
+
* ```typescript
|
|
584
|
+
* const { restored, count } = await coreTaskRestore('/project', 'T099', { cascade: true });
|
|
585
|
+
* console.log(`Restored ${count} tasks:`, restored);
|
|
586
|
+
* ```
|
|
587
|
+
*
|
|
588
|
+
* @task T4790
|
|
589
|
+
*/
|
|
590
|
+
export async function coreTaskRestore(projectRoot, taskId, params) {
|
|
591
|
+
const accessor = await getAccessor(projectRoot);
|
|
592
|
+
const task = await accessor.loadSingleTask(taskId);
|
|
593
|
+
if (!task) {
|
|
594
|
+
throw new Error(`Task '${taskId}' not found`);
|
|
595
|
+
}
|
|
596
|
+
if (task.status !== 'cancelled') {
|
|
597
|
+
throw new Error(`Task '${taskId}' is not cancelled (status: ${task.status}). Only cancelled tasks can be restored.`);
|
|
598
|
+
}
|
|
599
|
+
const tasksToRestore = [task];
|
|
600
|
+
if (params?.cascade) {
|
|
601
|
+
const findCancelledChildren = async (parentId) => {
|
|
602
|
+
const children = await accessor.getChildren(parentId);
|
|
603
|
+
const cancelledChildren = children.filter((t) => t.status === 'cancelled');
|
|
604
|
+
for (const child of cancelledChildren) {
|
|
605
|
+
tasksToRestore.push(child);
|
|
606
|
+
await findCancelledChildren(child.id);
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
await findCancelledChildren(taskId);
|
|
610
|
+
}
|
|
611
|
+
const now = new Date().toISOString();
|
|
612
|
+
const restored = [];
|
|
613
|
+
for (const t of tasksToRestore) {
|
|
614
|
+
t.status = 'pending';
|
|
615
|
+
t.cancelledAt = undefined;
|
|
616
|
+
t.cancellationReason = undefined;
|
|
617
|
+
t.updatedAt = now;
|
|
618
|
+
if (!t.notes)
|
|
619
|
+
t.notes = [];
|
|
620
|
+
t.notes.push(`[${now}] Restored from cancelled${params?.notes ? ': ' + params.notes : ''}`);
|
|
621
|
+
restored.push(t.id);
|
|
622
|
+
}
|
|
623
|
+
for (const t of tasksToRestore) {
|
|
624
|
+
await accessor.upsertSingleTask(t);
|
|
625
|
+
}
|
|
626
|
+
return { task: taskId, restored, count: restored.length };
|
|
627
|
+
}
|
|
628
|
+
// ============================================================================
|
|
629
|
+
// taskCancel
|
|
630
|
+
// ============================================================================
|
|
631
|
+
/**
|
|
632
|
+
* Cancel a task (sets status to 'cancelled', a soft terminal state).
|
|
633
|
+
* Use restore to reverse. Use delete for permanent removal.
|
|
634
|
+
*
|
|
635
|
+
* @param projectRoot - Absolute path to the CLEO project root directory
|
|
636
|
+
* @param taskId - The task ID to cancel
|
|
637
|
+
* @param params - Optional cancel options
|
|
638
|
+
* @param params.reason - Human-readable cancellation reason stored on the task
|
|
639
|
+
* @returns Confirmation with cancelled flag and timestamp
|
|
640
|
+
*
|
|
641
|
+
* @remarks
|
|
642
|
+
* Cancellation is a soft terminal state -- the task remains in the database and
|
|
643
|
+
* can be restored via {@link coreTaskRestore}. Not all statuses are cancellable;
|
|
644
|
+
* the `canCancel` guard determines eligibility.
|
|
645
|
+
*
|
|
646
|
+
* @example
|
|
647
|
+
* ```typescript
|
|
648
|
+
* const result = await coreTaskCancel('/project', 'T077', { reason: 'Superseded by T080' });
|
|
649
|
+
* console.log(result.cancelledAt);
|
|
650
|
+
* ```
|
|
651
|
+
*
|
|
652
|
+
* @task T4529
|
|
653
|
+
*/
|
|
654
|
+
export async function coreTaskCancel(projectRoot, taskId, params) {
|
|
655
|
+
const accessor = await getAccessor(projectRoot);
|
|
656
|
+
const task = await accessor.loadSingleTask(taskId);
|
|
657
|
+
if (!task) {
|
|
658
|
+
throw new Error(`Task ${taskId} not found`);
|
|
659
|
+
}
|
|
660
|
+
const check = canCancel(task);
|
|
661
|
+
if (!check.allowed) {
|
|
662
|
+
throw new Error(check.reason);
|
|
663
|
+
}
|
|
664
|
+
const cancelledAt = new Date().toISOString();
|
|
665
|
+
task.status = 'cancelled';
|
|
666
|
+
task.cancelledAt = cancelledAt;
|
|
667
|
+
task.cancellationReason = params?.reason ?? undefined;
|
|
668
|
+
task.updatedAt = cancelledAt;
|
|
669
|
+
await accessor.upsertSingleTask(task);
|
|
670
|
+
return { task: taskId, cancelled: true, reason: params?.reason, cancelledAt };
|
|
671
|
+
}
|
|
672
|
+
// ============================================================================
|
|
673
|
+
// taskUnarchive
|
|
674
|
+
// ============================================================================
|
|
675
|
+
/**
|
|
676
|
+
* Move an archived task back to active tasks.
|
|
677
|
+
*
|
|
678
|
+
* @param projectRoot - Absolute path to the CLEO project root directory
|
|
679
|
+
* @param taskId - The archived task ID to unarchive
|
|
680
|
+
* @param params - Optional unarchive options
|
|
681
|
+
* @param params.status - Target status for the restored task (default: "pending")
|
|
682
|
+
* @param params.preserveStatus - When true, keeps the task's original archived status
|
|
683
|
+
* @returns Confirmation with task ID, title, and resulting status
|
|
684
|
+
*
|
|
685
|
+
* @remarks
|
|
686
|
+
* Removes the task from the archive file and upserts it into the active task store.
|
|
687
|
+
* Throws if the task already exists in active tasks or is not found in the archive.
|
|
688
|
+
*
|
|
689
|
+
* @example
|
|
690
|
+
* ```typescript
|
|
691
|
+
* const result = await coreTaskUnarchive('/project', 'T055', { status: 'active' });
|
|
692
|
+
* console.log(`${result.title} is now ${result.status}`);
|
|
693
|
+
* ```
|
|
694
|
+
*
|
|
695
|
+
* @task T4790
|
|
696
|
+
*/
|
|
697
|
+
export async function coreTaskUnarchive(projectRoot, taskId, params) {
|
|
698
|
+
const accessor = await getAccessor(projectRoot);
|
|
699
|
+
// Check if task already exists in active tasks
|
|
700
|
+
const existingTask = await accessor.taskExists(taskId);
|
|
701
|
+
if (existingTask) {
|
|
702
|
+
throw new Error(`Task '${taskId}' already exists in active tasks`);
|
|
703
|
+
}
|
|
704
|
+
const archive = await accessor.loadArchive();
|
|
705
|
+
if (!archive || !archive.archivedTasks) {
|
|
706
|
+
throw new Error('No archive file found');
|
|
707
|
+
}
|
|
708
|
+
const taskIndex = archive.archivedTasks.findIndex((t) => t.id === taskId);
|
|
709
|
+
if (taskIndex === -1) {
|
|
710
|
+
throw new Error(`Task '${taskId}' not found in archive`);
|
|
711
|
+
}
|
|
712
|
+
const task = archive.archivedTasks[taskIndex];
|
|
713
|
+
// Remove archive metadata if present on the raw record
|
|
714
|
+
if ('_archive' in task) {
|
|
715
|
+
Reflect.deleteProperty(task, '_archive');
|
|
716
|
+
}
|
|
717
|
+
if (!params?.preserveStatus) {
|
|
718
|
+
const rawStatus = params?.status || 'pending';
|
|
719
|
+
if (!TASK_STATUSES.includes(rawStatus)) {
|
|
720
|
+
throw new Error(`Invalid status: ${rawStatus}`);
|
|
721
|
+
}
|
|
722
|
+
// rawStatus is validated above as a member of TASK_STATUSES
|
|
723
|
+
const targetStatus = rawStatus;
|
|
724
|
+
if (targetStatus !== 'done') {
|
|
725
|
+
task.completedAt = undefined;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
task.updatedAt = new Date().toISOString();
|
|
729
|
+
// Fine-grained: upsert the restored task (now active)
|
|
730
|
+
await accessor.upsertSingleTask(task);
|
|
731
|
+
return { task: taskId, unarchived: true, title: task.title, status: task.status };
|
|
732
|
+
}
|
|
733
|
+
// ============================================================================
|
|
734
|
+
// taskReorder
|
|
735
|
+
// ============================================================================
|
|
736
|
+
/**
|
|
737
|
+
* Change task position within its sibling group.
|
|
738
|
+
*
|
|
739
|
+
* @param projectRoot - Absolute path to the CLEO project root directory
|
|
740
|
+
* @param taskId - The task ID to reorder
|
|
741
|
+
* @param position - Target 1-based position within the sibling group
|
|
742
|
+
* @returns Confirmation with the new position and total sibling count
|
|
743
|
+
*
|
|
744
|
+
* @remarks
|
|
745
|
+
* Reorders by adjusting `position` and `positionVersion` fields on all siblings.
|
|
746
|
+
* Position is clamped to valid bounds. Uses bulk field updates for efficiency.
|
|
747
|
+
*
|
|
748
|
+
* @example
|
|
749
|
+
* ```typescript
|
|
750
|
+
* const result = await coreTaskReorder('/project', 'T012', 1);
|
|
751
|
+
* console.log(`Moved to position ${result.newPosition} of ${result.totalSiblings}`);
|
|
752
|
+
* ```
|
|
753
|
+
*
|
|
754
|
+
* @task T4790
|
|
755
|
+
*/
|
|
756
|
+
export async function coreTaskReorder(projectRoot, taskId, position) {
|
|
757
|
+
const accessor = await getAccessor(projectRoot);
|
|
758
|
+
const task = await accessor.loadSingleTask(taskId);
|
|
759
|
+
if (!task) {
|
|
760
|
+
throw new Error(`Task '${taskId}' not found`);
|
|
761
|
+
}
|
|
762
|
+
// Get siblings: tasks with same parentId
|
|
763
|
+
const parentFilter = task.parentId ? { parentId: task.parentId } : {};
|
|
764
|
+
const { tasks: siblingCandidates } = await accessor.queryTasks(parentFilter);
|
|
765
|
+
// For root-level tasks (no parentId), filter to only those without a parentId
|
|
766
|
+
const allSiblings = task.parentId
|
|
767
|
+
? siblingCandidates.sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
|
|
768
|
+
: siblingCandidates
|
|
769
|
+
.filter((t) => !t.parentId)
|
|
770
|
+
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
|
|
771
|
+
const currentIndex = allSiblings.findIndex((t) => t.id === taskId);
|
|
772
|
+
const newIndex = Math.max(0, Math.min(position - 1, allSiblings.length - 1));
|
|
773
|
+
allSiblings.splice(currentIndex, 1);
|
|
774
|
+
allSiblings.splice(newIndex, 0, task);
|
|
775
|
+
// Use bulk SQL for position updates (T025) — updateTaskFields is lighter than upsertSingleTask
|
|
776
|
+
const now = new Date().toISOString();
|
|
777
|
+
for (let i = 0; i < allSiblings.length; i++) {
|
|
778
|
+
const sibling = allSiblings[i];
|
|
779
|
+
const newPos = i + 1;
|
|
780
|
+
const newVersion = (sibling.positionVersion ?? 0) + 1;
|
|
781
|
+
// Only update if position actually changed
|
|
782
|
+
if (sibling.position !== newPos || sibling.id === taskId) {
|
|
783
|
+
await accessor.updateTaskFields(sibling.id, {
|
|
784
|
+
position: newPos,
|
|
785
|
+
positionVersion: newVersion,
|
|
786
|
+
updatedAt: now,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
sibling.position = newPos;
|
|
790
|
+
sibling.positionVersion = newVersion;
|
|
791
|
+
sibling.updatedAt = now;
|
|
792
|
+
}
|
|
793
|
+
return {
|
|
794
|
+
task: taskId,
|
|
795
|
+
reordered: true,
|
|
796
|
+
newPosition: newIndex + 1,
|
|
797
|
+
totalSiblings: allSiblings.length,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
// ============================================================================
|
|
801
|
+
// taskReparent
|
|
802
|
+
// ============================================================================
|
|
803
|
+
/**
|
|
804
|
+
* Move task under a different parent.
|
|
805
|
+
*
|
|
806
|
+
* @param projectRoot - Absolute path to the CLEO project root directory
|
|
807
|
+
* @param taskId - The task ID to reparent
|
|
808
|
+
* @param newParentId - The new parent task ID, or null to promote to root level
|
|
809
|
+
* @returns Confirmation with old and new parent IDs and optional type change
|
|
810
|
+
*
|
|
811
|
+
* @remarks
|
|
812
|
+
* Validates against circular references, depth limits, and sibling limits from
|
|
813
|
+
* the project hierarchy config. Automatically adjusts task type based on new depth
|
|
814
|
+
* (depth 1 = "task", depth >= 2 = "subtask").
|
|
815
|
+
*
|
|
816
|
+
* @example
|
|
817
|
+
* ```typescript
|
|
818
|
+
* const result = await coreTaskReparent('/project', 'T015', 'T010');
|
|
819
|
+
* console.log(`Moved from ${result.oldParent} to ${result.newParent}`);
|
|
820
|
+
* ```
|
|
821
|
+
*
|
|
822
|
+
* @task T4790
|
|
823
|
+
*/
|
|
824
|
+
export async function coreTaskReparent(projectRoot, taskId, newParentId) {
|
|
825
|
+
const accessor = await getAccessor(projectRoot);
|
|
826
|
+
const task = await accessor.loadSingleTask(taskId);
|
|
827
|
+
if (!task) {
|
|
828
|
+
throw new Error(`Task '${taskId}' not found`);
|
|
829
|
+
}
|
|
830
|
+
const effectiveParentId = newParentId || null;
|
|
831
|
+
if (!effectiveParentId) {
|
|
832
|
+
const oldParent = task.parentId ?? null;
|
|
833
|
+
task.parentId = null;
|
|
834
|
+
if (task.type === 'subtask')
|
|
835
|
+
task.type = 'task';
|
|
836
|
+
task.updatedAt = new Date().toISOString();
|
|
837
|
+
await accessor.upsertSingleTask(task);
|
|
838
|
+
return { task: taskId, reparented: true, oldParent, newParent: null, newType: task.type };
|
|
839
|
+
}
|
|
840
|
+
const newParent = await accessor.loadSingleTask(effectiveParentId);
|
|
841
|
+
if (!newParent) {
|
|
842
|
+
throw new Error(`Parent task '${effectiveParentId}' not found`);
|
|
843
|
+
}
|
|
844
|
+
if (newParent.type === 'subtask') {
|
|
845
|
+
throw new Error(`Cannot parent under subtask '${effectiveParentId}'`);
|
|
846
|
+
}
|
|
847
|
+
// Check circular reference using subtree
|
|
848
|
+
const subtree = await accessor.getSubtree(taskId);
|
|
849
|
+
if (subtree.some((t) => t.id === effectiveParentId)) {
|
|
850
|
+
throw new Error(`Moving '${taskId}' under '${effectiveParentId}' would create circular reference`);
|
|
851
|
+
}
|
|
852
|
+
// Check depth limit using ancestor chain
|
|
853
|
+
const ancestors = await accessor.getAncestorChain(effectiveParentId);
|
|
854
|
+
const parentDepth = ancestors.length;
|
|
855
|
+
const reparentLimits = getHierarchyLimits(projectRoot);
|
|
856
|
+
if (parentDepth + 1 >= reparentLimits.maxDepth) {
|
|
857
|
+
throw new Error(`Move would exceed max depth of ${reparentLimits.maxDepth}`);
|
|
858
|
+
}
|
|
859
|
+
// Check sibling limit (0 = unlimited)
|
|
860
|
+
const children = await accessor.getChildren(effectiveParentId);
|
|
861
|
+
const siblingCount = children.filter((t) => t.id !== taskId).length;
|
|
862
|
+
if (reparentLimits.maxSiblings > 0 && siblingCount >= reparentLimits.maxSiblings) {
|
|
863
|
+
throw new Error(`Cannot add child to ${effectiveParentId}: max siblings (${reparentLimits.maxSiblings}) exceeded`);
|
|
864
|
+
}
|
|
865
|
+
const oldParent = task.parentId ?? null;
|
|
866
|
+
task.parentId = effectiveParentId;
|
|
867
|
+
const newDepth = parentDepth + 1;
|
|
868
|
+
if (newDepth === 1)
|
|
869
|
+
task.type = 'task';
|
|
870
|
+
else if (newDepth >= 2)
|
|
871
|
+
task.type = 'subtask';
|
|
872
|
+
task.updatedAt = new Date().toISOString();
|
|
873
|
+
await accessor.upsertSingleTask(task);
|
|
874
|
+
return {
|
|
875
|
+
task: taskId,
|
|
876
|
+
reparented: true,
|
|
877
|
+
oldParent,
|
|
878
|
+
newParent: effectiveParentId,
|
|
879
|
+
newType: task.type,
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
// ============================================================================
|
|
883
|
+
// taskPromote
|
|
884
|
+
// ============================================================================
|
|
885
|
+
/**
|
|
886
|
+
* Promote a subtask to task or task to root.
|
|
887
|
+
*
|
|
888
|
+
* @param projectRoot - Absolute path to the CLEO project root directory
|
|
889
|
+
* @param taskId - The task ID to promote
|
|
890
|
+
* @returns Confirmation with previous parent and whether the type changed
|
|
891
|
+
*
|
|
892
|
+
* @remarks
|
|
893
|
+
* Removes the task's parentId, making it a root-level task. If the task was
|
|
894
|
+
* a "subtask", its type is changed to "task". No-op if the task is already root-level.
|
|
895
|
+
*
|
|
896
|
+
* @example
|
|
897
|
+
* ```typescript
|
|
898
|
+
* const result = await coreTaskPromote('/project', 'T025');
|
|
899
|
+
* if (result.promoted) console.log('Detached from', result.previousParent);
|
|
900
|
+
* ```
|
|
901
|
+
*
|
|
902
|
+
* @task T4790
|
|
903
|
+
*/
|
|
904
|
+
export async function coreTaskPromote(projectRoot, taskId) {
|
|
905
|
+
const accessor = await getAccessor(projectRoot);
|
|
906
|
+
const task = await accessor.loadSingleTask(taskId);
|
|
907
|
+
if (!task) {
|
|
908
|
+
throw new Error(`Task '${taskId}' not found`);
|
|
909
|
+
}
|
|
910
|
+
if (!task.parentId) {
|
|
911
|
+
return { task: taskId, promoted: false, previousParent: null, typeChanged: false };
|
|
912
|
+
}
|
|
913
|
+
const oldParent = task.parentId;
|
|
914
|
+
task.parentId = null;
|
|
915
|
+
task.updatedAt = new Date().toISOString();
|
|
916
|
+
let typeChanged = false;
|
|
917
|
+
if (task.type === 'subtask') {
|
|
918
|
+
task.type = 'task';
|
|
919
|
+
typeChanged = true;
|
|
920
|
+
}
|
|
921
|
+
await accessor.upsertSingleTask(task);
|
|
922
|
+
return { task: taskId, promoted: true, previousParent: oldParent, typeChanged };
|
|
923
|
+
}
|
|
924
|
+
// ============================================================================
|
|
925
|
+
// taskReopen
|
|
926
|
+
// ============================================================================
|
|
927
|
+
/**
|
|
928
|
+
* Reopen a completed task.
|
|
929
|
+
*
|
|
930
|
+
* @param projectRoot - Absolute path to the CLEO project root directory
|
|
931
|
+
* @param taskId - The completed task ID to reopen
|
|
932
|
+
* @param params - Optional reopen options
|
|
933
|
+
* @param params.status - Target status after reopening ("pending" or "active", default: "pending")
|
|
934
|
+
* @param params.reason - Optional reason appended to the task's notes
|
|
935
|
+
* @returns Confirmation with previous and new status
|
|
936
|
+
*
|
|
937
|
+
* @remarks
|
|
938
|
+
* Only tasks with status "done" can be reopened. Clears the `completedAt` timestamp
|
|
939
|
+
* and appends a timestamped note recording the reopen event.
|
|
940
|
+
*
|
|
941
|
+
* @example
|
|
942
|
+
* ```typescript
|
|
943
|
+
* const result = await coreTaskReopen('/project', 'T033', { status: 'active', reason: 'Tests failed' });
|
|
944
|
+
* console.log(`${result.previousStatus} -> ${result.newStatus}`);
|
|
945
|
+
* ```
|
|
946
|
+
*
|
|
947
|
+
* @task T4790
|
|
948
|
+
*/
|
|
949
|
+
export async function coreTaskReopen(projectRoot, taskId, params) {
|
|
950
|
+
const accessor = await getAccessor(projectRoot);
|
|
951
|
+
const task = await accessor.loadSingleTask(taskId);
|
|
952
|
+
if (!task) {
|
|
953
|
+
throw new Error(`Task '${taskId}' not found`);
|
|
954
|
+
}
|
|
955
|
+
if (task.status !== 'done') {
|
|
956
|
+
throw new Error(`Task '${taskId}' is not completed (status: ${task.status}). Only done tasks can be reopened.`);
|
|
957
|
+
}
|
|
958
|
+
const targetStatus = params?.status || 'pending';
|
|
959
|
+
if (targetStatus !== 'pending' && targetStatus !== 'active') {
|
|
960
|
+
throw new Error(`Invalid target status: ${targetStatus}. Must be 'pending' or 'active'.`);
|
|
961
|
+
}
|
|
962
|
+
const previousStatus = task.status;
|
|
963
|
+
task.status = targetStatus;
|
|
964
|
+
task.completedAt = undefined;
|
|
965
|
+
task.updatedAt = new Date().toISOString();
|
|
966
|
+
if (!task.notes)
|
|
967
|
+
task.notes = [];
|
|
968
|
+
const reason = params?.reason;
|
|
969
|
+
task.notes.push(`[${task.updatedAt}] Reopened from ${previousStatus}${reason ? ': ' + reason : ''}`);
|
|
970
|
+
await accessor.upsertSingleTask(task);
|
|
971
|
+
return { task: taskId, reopened: true, previousStatus, newStatus: targetStatus };
|
|
972
|
+
}
|
|
973
|
+
// ============================================================================
|
|
974
|
+
// taskComplexityEstimate
|
|
975
|
+
// ============================================================================
|
|
976
|
+
/**
|
|
977
|
+
* Deterministic complexity scoring from task metadata.
|
|
978
|
+
*
|
|
979
|
+
* @param projectRoot - Absolute path to the CLEO project root directory
|
|
980
|
+
* @param params - Parameters containing the task ID to estimate
|
|
981
|
+
* @param params.taskId - The task ID to compute complexity for
|
|
982
|
+
* @returns Complexity size ("small"/"medium"/"large"), numeric score, contributing factors, and metadata counts
|
|
983
|
+
*
|
|
984
|
+
* @remarks
|
|
985
|
+
* Scores are computed from description length, acceptance criteria count, dependency depth,
|
|
986
|
+
* subtask count, and file reference count. Each factor contributes 0-3 points.
|
|
987
|
+
* Total score 0-3 = small, 4-7 = medium, 8+ = large.
|
|
988
|
+
*
|
|
989
|
+
* @example
|
|
990
|
+
* ```typescript
|
|
991
|
+
* const est = await coreTaskComplexityEstimate('/project', { taskId: 'T042' });
|
|
992
|
+
* console.log(`${est.size} (score: ${est.score})`);
|
|
993
|
+
* ```
|
|
994
|
+
*
|
|
995
|
+
* @task T4790
|
|
996
|
+
*/
|
|
997
|
+
export async function coreTaskComplexityEstimate(projectRoot, params) {
|
|
998
|
+
const allTasks = await loadAllTasks(projectRoot);
|
|
999
|
+
const task = allTasks.find((t) => t.id === params.taskId);
|
|
1000
|
+
if (!task) {
|
|
1001
|
+
throw new Error(`Task '${params.taskId}' not found`);
|
|
1002
|
+
}
|
|
1003
|
+
const factors = [];
|
|
1004
|
+
let score = 0;
|
|
1005
|
+
const descLen = (task.description || '').length;
|
|
1006
|
+
let descScore;
|
|
1007
|
+
let descLabel;
|
|
1008
|
+
if (descLen < 100) {
|
|
1009
|
+
descScore = 1;
|
|
1010
|
+
descLabel = 'short';
|
|
1011
|
+
}
|
|
1012
|
+
else if (descLen < 500) {
|
|
1013
|
+
descScore = 2;
|
|
1014
|
+
descLabel = 'medium';
|
|
1015
|
+
}
|
|
1016
|
+
else {
|
|
1017
|
+
descScore = 3;
|
|
1018
|
+
descLabel = 'long';
|
|
1019
|
+
}
|
|
1020
|
+
score += descScore;
|
|
1021
|
+
factors.push({
|
|
1022
|
+
name: 'descriptionLength',
|
|
1023
|
+
value: descScore,
|
|
1024
|
+
detail: `${descLabel} (${descLen} chars)`,
|
|
1025
|
+
});
|
|
1026
|
+
const acceptanceCount = task.acceptance?.length ?? 0;
|
|
1027
|
+
const acceptanceScore = Math.min(acceptanceCount, 3);
|
|
1028
|
+
score += acceptanceScore;
|
|
1029
|
+
factors.push({
|
|
1030
|
+
name: 'acceptanceCriteria',
|
|
1031
|
+
value: acceptanceScore,
|
|
1032
|
+
detail: `${acceptanceCount} criteria`,
|
|
1033
|
+
});
|
|
1034
|
+
const taskMap = new Map(allTasks.map((t) => [t.id, t]));
|
|
1035
|
+
const dependencyDepth = measureDependencyDepth(params.taskId, taskMap);
|
|
1036
|
+
const depthScore = Math.min(dependencyDepth, 3);
|
|
1037
|
+
score += depthScore;
|
|
1038
|
+
factors.push({ name: 'dependencyDepth', value: depthScore, detail: `depth ${dependencyDepth}` });
|
|
1039
|
+
const subtaskCount = allTasks.filter((t) => t.parentId === params.taskId).length;
|
|
1040
|
+
const subtaskScore = Math.min(subtaskCount, 3);
|
|
1041
|
+
score += subtaskScore;
|
|
1042
|
+
factors.push({ name: 'subtaskCount', value: subtaskScore, detail: `${subtaskCount} subtasks` });
|
|
1043
|
+
const fileCount = task.files?.length ?? 0;
|
|
1044
|
+
const fileScore = Math.min(fileCount, 3);
|
|
1045
|
+
score += fileScore;
|
|
1046
|
+
factors.push({ name: 'fileReferences', value: fileScore, detail: `${fileCount} files` });
|
|
1047
|
+
let size;
|
|
1048
|
+
if (score <= 3)
|
|
1049
|
+
size = 'small';
|
|
1050
|
+
else if (score <= 7)
|
|
1051
|
+
size = 'medium';
|
|
1052
|
+
else
|
|
1053
|
+
size = 'large';
|
|
1054
|
+
return { size, score, factors, dependencyDepth, subtaskCount, fileCount };
|
|
1055
|
+
}
|
|
1056
|
+
// ============================================================================
|
|
1057
|
+
// taskDepsOverview
|
|
1058
|
+
// ============================================================================
|
|
1059
|
+
/**
|
|
1060
|
+
* Overview of all dependencies across the project.
|
|
1061
|
+
*
|
|
1062
|
+
* @param projectRoot - Absolute path to the CLEO project root directory
|
|
1063
|
+
* @returns Project-wide dependency summary including blocked tasks, ready tasks, and validation results
|
|
1064
|
+
*
|
|
1065
|
+
* @remarks
|
|
1066
|
+
* Aggregates dependency data across all tasks to provide a high-level view of
|
|
1067
|
+
* the dependency graph health, including which tasks are blocked and what would unblock them.
|
|
1068
|
+
*
|
|
1069
|
+
* @example
|
|
1070
|
+
* ```typescript
|
|
1071
|
+
* const overview = await coreTaskDepsOverview('/project');
|
|
1072
|
+
* console.log(`${overview.blockedTasks.length} blocked, ${overview.readyTasks.length} ready`);
|
|
1073
|
+
* ```
|
|
1074
|
+
*
|
|
1075
|
+
* @task T5157
|
|
1076
|
+
*/
|
|
1077
|
+
export async function coreTaskDepsOverview(projectRoot) {
|
|
1078
|
+
const allTasks = await loadAllTasks(projectRoot);
|
|
1079
|
+
const tasksAsTask = allTasks;
|
|
1080
|
+
const tasksWithDeps = allTasks.filter((t) => t.depends && t.depends.length > 0);
|
|
1081
|
+
const blocked = getBlockedTasks(tasksAsTask);
|
|
1082
|
+
const ready = getReadyTasks(tasksAsTask);
|
|
1083
|
+
const validation = validateDependencies(tasksAsTask);
|
|
1084
|
+
return {
|
|
1085
|
+
totalTasks: allTasks.length,
|
|
1086
|
+
tasksWithDeps: tasksWithDeps.length,
|
|
1087
|
+
blockedTasks: blocked.map((t) => ({
|
|
1088
|
+
id: t.id,
|
|
1089
|
+
title: t.title,
|
|
1090
|
+
status: t.status,
|
|
1091
|
+
unblockedBy: (t.depends ?? []).filter((depId) => {
|
|
1092
|
+
const dep = allTasks.find((x) => x.id === depId);
|
|
1093
|
+
return dep && dep.status !== 'done' && dep.status !== 'cancelled';
|
|
1094
|
+
}),
|
|
1095
|
+
})),
|
|
1096
|
+
readyTasks: ready
|
|
1097
|
+
.filter((t) => t.status !== 'done' && t.status !== 'cancelled')
|
|
1098
|
+
.map((t) => ({
|
|
1099
|
+
id: t.id,
|
|
1100
|
+
title: t.title,
|
|
1101
|
+
status: t.status,
|
|
1102
|
+
})),
|
|
1103
|
+
validation: {
|
|
1104
|
+
valid: validation.valid,
|
|
1105
|
+
errorCount: validation.errors.length,
|
|
1106
|
+
warningCount: validation.warnings.length,
|
|
1107
|
+
},
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
// ============================================================================
|
|
1111
|
+
// taskDepsCycles
|
|
1112
|
+
// ============================================================================
|
|
1113
|
+
/**
|
|
1114
|
+
* Detect circular dependencies across the project.
|
|
1115
|
+
*
|
|
1116
|
+
* @param projectRoot - Absolute path to the CLEO project root directory
|
|
1117
|
+
* @returns Whether cycles exist and the list of detected cycles with their task paths
|
|
1118
|
+
*
|
|
1119
|
+
* @remarks
|
|
1120
|
+
* Iterates through all tasks with dependencies and uses cycle detection to find
|
|
1121
|
+
* circular chains. Each cycle includes the full path (e.g. [A, B, C, A]) and
|
|
1122
|
+
* the tasks involved with their titles.
|
|
1123
|
+
*
|
|
1124
|
+
* @example
|
|
1125
|
+
* ```typescript
|
|
1126
|
+
* const { hasCycles, cycles } = await coreTaskDepsCycles('/project');
|
|
1127
|
+
* if (hasCycles) console.log('Circular deps:', cycles.map(c => c.path.join(' -> ')));
|
|
1128
|
+
* ```
|
|
1129
|
+
*
|
|
1130
|
+
* @task T5157
|
|
1131
|
+
*/
|
|
1132
|
+
export async function coreTaskDepsCycles(projectRoot) {
|
|
1133
|
+
const allTasks = await loadAllTasks(projectRoot);
|
|
1134
|
+
const tasksAsTask = allTasks;
|
|
1135
|
+
const taskMap = new Map(allTasks.map((t) => [t.id, t]));
|
|
1136
|
+
const visited = new Set();
|
|
1137
|
+
const cycles = [];
|
|
1138
|
+
for (const task of allTasks) {
|
|
1139
|
+
if (visited.has(task.id))
|
|
1140
|
+
continue;
|
|
1141
|
+
if (!task.depends?.length)
|
|
1142
|
+
continue;
|
|
1143
|
+
const cycle = detectCircularDeps(task.id, tasksAsTask);
|
|
1144
|
+
if (cycle.length > 0) {
|
|
1145
|
+
cycles.push({
|
|
1146
|
+
path: cycle,
|
|
1147
|
+
// Deduplicate: detectCircularDeps returns [A,B,C,A] where
|
|
1148
|
+
// last element closes the cycle. Use Set for robustness.
|
|
1149
|
+
tasks: [...new Set(cycle)].map((id) => {
|
|
1150
|
+
const t = taskMap.get(id);
|
|
1151
|
+
return { id, title: t?.title ?? 'unknown' };
|
|
1152
|
+
}),
|
|
1153
|
+
});
|
|
1154
|
+
for (const id of cycle) {
|
|
1155
|
+
visited.add(id);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
return { hasCycles: cycles.length > 0, cycles };
|
|
1160
|
+
}
|
|
1161
|
+
// ============================================================================
|
|
1162
|
+
// taskDepends
|
|
1163
|
+
// ============================================================================
|
|
1164
|
+
/**
|
|
1165
|
+
* List dependencies for a task in a given direction.
|
|
1166
|
+
*
|
|
1167
|
+
* @param projectRoot - Absolute path to the CLEO project root directory
|
|
1168
|
+
* @param taskId - The task ID to inspect
|
|
1169
|
+
* @param direction - Direction to traverse: "upstream" (what this task depends on), "downstream" (what depends on it), or "both"
|
|
1170
|
+
* @param options - Optional display configuration
|
|
1171
|
+
* @param options.tree - When true, includes a recursive upstream dependency tree
|
|
1172
|
+
* @returns Upstream and downstream deps, transitive chain length, leaf blockers, and readiness status
|
|
1173
|
+
*
|
|
1174
|
+
* @remarks
|
|
1175
|
+
* Combines direct dependency lookups with transitive analysis. Leaf blockers are
|
|
1176
|
+
* the deepest unresolved tasks in the dependency chain -- resolving them first
|
|
1177
|
+
* has the most impact on unblocking.
|
|
1178
|
+
*
|
|
1179
|
+
* @example
|
|
1180
|
+
* ```typescript
|
|
1181
|
+
* const deps = await coreTaskDepends('/project', 'T100', 'both', { tree: true });
|
|
1182
|
+
* console.log('Leaf blockers:', deps.leafBlockers.map(b => b.id));
|
|
1183
|
+
* ```
|
|
1184
|
+
*
|
|
1185
|
+
* @task T4790
|
|
1186
|
+
*/
|
|
1187
|
+
export async function coreTaskDepends(projectRoot, taskId, direction = 'both', options) {
|
|
1188
|
+
const allTasks = await loadAllTasks(projectRoot);
|
|
1189
|
+
const task = allTasks.find((t) => t.id === taskId);
|
|
1190
|
+
if (!task) {
|
|
1191
|
+
throw new Error(`Task '${taskId}' not found`);
|
|
1192
|
+
}
|
|
1193
|
+
const taskMap = new Map(allTasks.map((t) => [t.id, t]));
|
|
1194
|
+
const upstream = [];
|
|
1195
|
+
if (direction === 'upstream' || direction === 'both') {
|
|
1196
|
+
for (const depId of task.depends ?? []) {
|
|
1197
|
+
const dep = taskMap.get(depId);
|
|
1198
|
+
if (dep) {
|
|
1199
|
+
upstream.push({ id: dep.id, title: dep.title, status: dep.status });
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
const downstream = [];
|
|
1204
|
+
if (direction === 'downstream' || direction === 'both') {
|
|
1205
|
+
for (const t of allTasks) {
|
|
1206
|
+
if (t.depends?.includes(taskId)) {
|
|
1207
|
+
downstream.push({ id: t.id, title: t.title, status: t.status });
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
// Transitive dependency hints
|
|
1212
|
+
const tasksAsTask = allTasks;
|
|
1213
|
+
const transitiveIds = getTransitiveBlockers(taskId, tasksAsTask);
|
|
1214
|
+
const unresolvedChain = transitiveIds.length;
|
|
1215
|
+
const leafIds = getLeafBlockers(taskId, tasksAsTask);
|
|
1216
|
+
const leafBlockers = leafIds.map((id) => {
|
|
1217
|
+
const t = taskMap.get(id);
|
|
1218
|
+
return { id: t.id, title: t.title, status: t.status };
|
|
1219
|
+
});
|
|
1220
|
+
const allDepsReady = unresolvedChain === 0;
|
|
1221
|
+
const hint = unresolvedChain > 0
|
|
1222
|
+
? `Run 'ct deps show ${taskId} --tree' for full dependency graph`
|
|
1223
|
+
: undefined;
|
|
1224
|
+
// Optional upstream tree
|
|
1225
|
+
let upstreamTree;
|
|
1226
|
+
if (options?.tree) {
|
|
1227
|
+
upstreamTree = buildUpstreamTree(taskId, taskMap);
|
|
1228
|
+
}
|
|
1229
|
+
return {
|
|
1230
|
+
taskId,
|
|
1231
|
+
direction,
|
|
1232
|
+
upstream,
|
|
1233
|
+
downstream,
|
|
1234
|
+
unresolvedChain,
|
|
1235
|
+
leafBlockers,
|
|
1236
|
+
allDepsReady,
|
|
1237
|
+
...(hint && { hint }),
|
|
1238
|
+
...(upstreamTree && { upstreamTree }),
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
// ============================================================================
|
|
1242
|
+
// taskStats
|
|
1243
|
+
// ============================================================================
|
|
1244
|
+
/**
|
|
1245
|
+
* Compute task statistics.
|
|
1246
|
+
*
|
|
1247
|
+
* @param projectRoot - Absolute path to the CLEO project root directory
|
|
1248
|
+
* @param epicId - Optional epic ID to scope stats to that subtree
|
|
1249
|
+
* @returns Status counts, priority distribution, and type distribution
|
|
1250
|
+
*
|
|
1251
|
+
* @remarks
|
|
1252
|
+
* When an epicId is provided, statistics are scoped to that epic and all its
|
|
1253
|
+
* transitive children. Without an epicId, stats cover the entire project.
|
|
1254
|
+
*
|
|
1255
|
+
* @example
|
|
1256
|
+
* ```typescript
|
|
1257
|
+
* const stats = await coreTaskStats('/project', 'T001');
|
|
1258
|
+
* console.log(`${stats.done}/${stats.total} complete`);
|
|
1259
|
+
* ```
|
|
1260
|
+
*
|
|
1261
|
+
* @task T4790
|
|
1262
|
+
*/
|
|
1263
|
+
export async function coreTaskStats(projectRoot, epicId) {
|
|
1264
|
+
const allTasks = await loadAllTasks(projectRoot);
|
|
1265
|
+
let tasks = allTasks;
|
|
1266
|
+
if (epicId) {
|
|
1267
|
+
const epicIds = new Set();
|
|
1268
|
+
epicIds.add(epicId);
|
|
1269
|
+
const collectChildren = (parentId) => {
|
|
1270
|
+
for (const t of allTasks) {
|
|
1271
|
+
if (t.parentId === parentId && !epicIds.has(t.id)) {
|
|
1272
|
+
epicIds.add(t.id);
|
|
1273
|
+
collectChildren(t.id);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
collectChildren(epicId);
|
|
1278
|
+
tasks = allTasks.filter((t) => epicIds.has(t.id));
|
|
1279
|
+
}
|
|
1280
|
+
const byStatus = {};
|
|
1281
|
+
const byPriority = {};
|
|
1282
|
+
const byType = {};
|
|
1283
|
+
for (const task of tasks) {
|
|
1284
|
+
byStatus[task.status] = (byStatus[task.status] ?? 0) + 1;
|
|
1285
|
+
byPriority[task.priority] = (byPriority[task.priority] ?? 0) + 1;
|
|
1286
|
+
const taskType = task.type ?? 'task';
|
|
1287
|
+
byType[taskType] = (byType[taskType] ?? 0) + 1;
|
|
1288
|
+
}
|
|
1289
|
+
return {
|
|
1290
|
+
total: tasks.length,
|
|
1291
|
+
pending: byStatus['pending'] ?? 0,
|
|
1292
|
+
active: byStatus['active'] ?? 0,
|
|
1293
|
+
blocked: byStatus['blocked'] ?? 0,
|
|
1294
|
+
done: byStatus['done'] ?? 0,
|
|
1295
|
+
cancelled: byStatus['cancelled'] ?? 0,
|
|
1296
|
+
byPriority,
|
|
1297
|
+
byType,
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
// ============================================================================
|
|
1301
|
+
// taskExport
|
|
1302
|
+
// ============================================================================
|
|
1303
|
+
/**
|
|
1304
|
+
* Export tasks as JSON or CSV.
|
|
1305
|
+
*
|
|
1306
|
+
* @param projectRoot - Absolute path to the CLEO project root directory
|
|
1307
|
+
* @param params - Optional export configuration
|
|
1308
|
+
* @param params.format - Output format: "json" (default) or "csv"
|
|
1309
|
+
* @param params.status - Filter to only tasks with this status
|
|
1310
|
+
* @param params.parent - Filter to tasks under this parent ID (recursive)
|
|
1311
|
+
* @returns Export payload with format, content/tasks, and task count
|
|
1312
|
+
*
|
|
1313
|
+
* @remarks
|
|
1314
|
+
* CSV output includes columns: id, title, status, priority, type, parentId, createdAt.
|
|
1315
|
+
* JSON output returns the full task objects. Both formats support status and parent filtering.
|
|
1316
|
+
*
|
|
1317
|
+
* @example
|
|
1318
|
+
* ```typescript
|
|
1319
|
+
* const result = await coreTaskExport('/project', { format: 'csv', status: 'done' });
|
|
1320
|
+
* console.log(result.content); // CSV string
|
|
1321
|
+
* ```
|
|
1322
|
+
*
|
|
1323
|
+
* @task T4790
|
|
1324
|
+
*/
|
|
1325
|
+
export async function coreTaskExport(projectRoot, params) {
|
|
1326
|
+
const allTasks = await loadAllTasks(projectRoot);
|
|
1327
|
+
let tasks = allTasks;
|
|
1328
|
+
if (params?.status) {
|
|
1329
|
+
tasks = tasks.filter((t) => t.status === params.status);
|
|
1330
|
+
}
|
|
1331
|
+
if (params?.parent) {
|
|
1332
|
+
const parentIds = new Set();
|
|
1333
|
+
parentIds.add(params.parent);
|
|
1334
|
+
const collectChildren = (parentId) => {
|
|
1335
|
+
for (const t of allTasks) {
|
|
1336
|
+
if (t.parentId === parentId && !parentIds.has(t.id)) {
|
|
1337
|
+
parentIds.add(t.id);
|
|
1338
|
+
collectChildren(t.id);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
collectChildren(params.parent);
|
|
1343
|
+
tasks = tasks.filter((t) => parentIds.has(t.id));
|
|
1344
|
+
}
|
|
1345
|
+
if (params?.format === 'csv') {
|
|
1346
|
+
const headers = ['id', 'title', 'status', 'priority', 'type', 'parentId', 'createdAt'];
|
|
1347
|
+
const rows = tasks.map((t) => [
|
|
1348
|
+
t.id,
|
|
1349
|
+
`"${(t.title || '').replace(/"/g, '""')}"`,
|
|
1350
|
+
t.status,
|
|
1351
|
+
t.priority,
|
|
1352
|
+
t.type ?? 'task',
|
|
1353
|
+
t.parentId ?? '',
|
|
1354
|
+
t.createdAt,
|
|
1355
|
+
].join(','));
|
|
1356
|
+
const csv = [headers.join(','), ...rows].join('\n');
|
|
1357
|
+
return { format: 'csv', content: csv, taskCount: tasks.length };
|
|
1358
|
+
}
|
|
1359
|
+
return { format: 'json', tasks, taskCount: tasks.length };
|
|
1360
|
+
}
|
|
1361
|
+
// ============================================================================
|
|
1362
|
+
// taskHistory
|
|
1363
|
+
// ============================================================================
|
|
1364
|
+
/**
|
|
1365
|
+
* Get task history from the audit log.
|
|
1366
|
+
*
|
|
1367
|
+
* @param projectRoot - Absolute path to the CLEO project root directory
|
|
1368
|
+
* @param taskId - The task ID to retrieve history for
|
|
1369
|
+
* @param limit - Maximum number of history entries to return (default: 100)
|
|
1370
|
+
* @returns Array of audit log entries ordered by timestamp descending
|
|
1371
|
+
*
|
|
1372
|
+
* @remarks
|
|
1373
|
+
* Queries the SQLite audit_log table for all operations on the given task.
|
|
1374
|
+
* Returns an empty array if the database is unavailable or no entries exist.
|
|
1375
|
+
*
|
|
1376
|
+
* @example
|
|
1377
|
+
* ```typescript
|
|
1378
|
+
* const history = await coreTaskHistory('/project', 'T042', 10);
|
|
1379
|
+
* for (const entry of history) console.log(entry.timestamp, entry.operation);
|
|
1380
|
+
* ```
|
|
1381
|
+
*
|
|
1382
|
+
* @task T4790
|
|
1383
|
+
*/
|
|
1384
|
+
export async function coreTaskHistory(projectRoot, taskId, limit) {
|
|
1385
|
+
try {
|
|
1386
|
+
const { getDb } = await import('../store/sqlite.js');
|
|
1387
|
+
const { auditLog } = await import('../store/tasks-schema.js');
|
|
1388
|
+
const { sql } = await import('drizzle-orm');
|
|
1389
|
+
const db = await getDb(projectRoot);
|
|
1390
|
+
const maxRows = limit && limit > 0 ? limit : 100;
|
|
1391
|
+
const rows = await db.all(sql `SELECT * FROM ${auditLog}
|
|
1392
|
+
WHERE ${auditLog.taskId} = ${taskId}
|
|
1393
|
+
ORDER BY ${auditLog.timestamp} DESC
|
|
1394
|
+
LIMIT ${maxRows}`);
|
|
1395
|
+
return rows.map((row) => ({
|
|
1396
|
+
id: row.id,
|
|
1397
|
+
timestamp: row.timestamp,
|
|
1398
|
+
operation: row.operation ?? row.action,
|
|
1399
|
+
action: row.action,
|
|
1400
|
+
taskId: row.task_id,
|
|
1401
|
+
actor: row.actor,
|
|
1402
|
+
details: row.details_json ? JSON.parse(row.details_json) : {},
|
|
1403
|
+
before: row.before_json ? JSON.parse(row.before_json) : undefined,
|
|
1404
|
+
after: row.after_json ? JSON.parse(row.after_json) : undefined,
|
|
1405
|
+
domain: row.domain,
|
|
1406
|
+
sessionId: row.session_id,
|
|
1407
|
+
requestId: row.request_id,
|
|
1408
|
+
durationMs: row.duration_ms,
|
|
1409
|
+
success: row.success === null ? undefined : row.success === 1,
|
|
1410
|
+
source: row.source,
|
|
1411
|
+
gateway: row.gateway,
|
|
1412
|
+
error: row.error_message,
|
|
1413
|
+
}));
|
|
1414
|
+
}
|
|
1415
|
+
catch {
|
|
1416
|
+
return [];
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
// ============================================================================
|
|
1420
|
+
// taskLint
|
|
1421
|
+
// ============================================================================
|
|
1422
|
+
/**
|
|
1423
|
+
* Lint tasks for common issues.
|
|
1424
|
+
*
|
|
1425
|
+
* @param projectRoot - Absolute path to the CLEO project root directory
|
|
1426
|
+
* @param taskId - Optional task ID to lint; omit to lint all tasks
|
|
1427
|
+
* @returns Array of lint issues with severity, rule name, and descriptive message
|
|
1428
|
+
*
|
|
1429
|
+
* @remarks
|
|
1430
|
+
* Checks for: duplicate IDs, missing titles, missing descriptions, identical
|
|
1431
|
+
* title/description, duplicate descriptions, invalid statuses, future timestamps,
|
|
1432
|
+
* invalid parent references, and invalid dependency references.
|
|
1433
|
+
*
|
|
1434
|
+
* @example
|
|
1435
|
+
* ```typescript
|
|
1436
|
+
* const issues = await coreTaskLint('/project');
|
|
1437
|
+
* const errors = issues.filter(i => i.severity === 'error');
|
|
1438
|
+
* console.log(`${errors.length} errors found`);
|
|
1439
|
+
* ```
|
|
1440
|
+
*
|
|
1441
|
+
* @task T4790
|
|
1442
|
+
*/
|
|
1443
|
+
export async function coreTaskLint(projectRoot, taskId) {
|
|
1444
|
+
const allTasks = await loadAllTasks(projectRoot);
|
|
1445
|
+
const tasks = taskId ? allTasks.filter((t) => t.id === taskId) : allTasks;
|
|
1446
|
+
if (taskId && tasks.length === 0) {
|
|
1447
|
+
throw new Error(`Task '${taskId}' not found`);
|
|
1448
|
+
}
|
|
1449
|
+
const issues = [];
|
|
1450
|
+
const allDescriptions = new Set();
|
|
1451
|
+
const allIds = new Set();
|
|
1452
|
+
for (const task of allTasks) {
|
|
1453
|
+
if (allIds.has(task.id)) {
|
|
1454
|
+
issues.push({
|
|
1455
|
+
taskId: task.id,
|
|
1456
|
+
severity: 'error',
|
|
1457
|
+
rule: 'unique-id',
|
|
1458
|
+
message: `Duplicate task ID: ${task.id}`,
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
allIds.add(task.id);
|
|
1462
|
+
if (taskId && task.id !== taskId) {
|
|
1463
|
+
if (task.description)
|
|
1464
|
+
allDescriptions.add(task.description.toLowerCase());
|
|
1465
|
+
continue;
|
|
1466
|
+
}
|
|
1467
|
+
if (!task.title || task.title.trim().length === 0) {
|
|
1468
|
+
issues.push({
|
|
1469
|
+
taskId: task.id,
|
|
1470
|
+
severity: 'error',
|
|
1471
|
+
rule: 'title-required',
|
|
1472
|
+
message: 'Task is missing a title',
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
if (!task.description || task.description.trim().length === 0) {
|
|
1476
|
+
issues.push({
|
|
1477
|
+
taskId: task.id,
|
|
1478
|
+
severity: 'warning',
|
|
1479
|
+
rule: 'description-required',
|
|
1480
|
+
message: 'Task is missing a description',
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
if (task.title && task.description && task.title.trim() === task.description.trim()) {
|
|
1484
|
+
issues.push({
|
|
1485
|
+
taskId: task.id,
|
|
1486
|
+
severity: 'warning',
|
|
1487
|
+
rule: 'title-description-different',
|
|
1488
|
+
message: 'Title and description should not be identical',
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
if (task.description) {
|
|
1492
|
+
const descLower = task.description.toLowerCase();
|
|
1493
|
+
if (allDescriptions.has(descLower)) {
|
|
1494
|
+
issues.push({
|
|
1495
|
+
taskId: task.id,
|
|
1496
|
+
severity: 'warning',
|
|
1497
|
+
rule: 'unique-description',
|
|
1498
|
+
message: 'Duplicate task description found',
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
allDescriptions.add(descLower);
|
|
1502
|
+
}
|
|
1503
|
+
if (!TASK_STATUSES.includes(task.status)) {
|
|
1504
|
+
issues.push({
|
|
1505
|
+
taskId: task.id,
|
|
1506
|
+
severity: 'error',
|
|
1507
|
+
rule: 'valid-status',
|
|
1508
|
+
message: `Invalid status: ${task.status}`,
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
const now = new Date();
|
|
1512
|
+
if (task.createdAt && new Date(task.createdAt) > now) {
|
|
1513
|
+
issues.push({
|
|
1514
|
+
taskId: task.id,
|
|
1515
|
+
severity: 'warning',
|
|
1516
|
+
rule: 'no-future-timestamps',
|
|
1517
|
+
message: 'createdAt is in the future',
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
if (task.parentId && !allTasks.some((t) => t.id === task.parentId)) {
|
|
1521
|
+
issues.push({
|
|
1522
|
+
taskId: task.id,
|
|
1523
|
+
severity: 'error',
|
|
1524
|
+
rule: 'valid-parent',
|
|
1525
|
+
message: `Parent task '${task.parentId}' does not exist`,
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
for (const depId of task.depends ?? []) {
|
|
1529
|
+
if (!allTasks.some((t) => t.id === depId)) {
|
|
1530
|
+
issues.push({
|
|
1531
|
+
taskId: task.id,
|
|
1532
|
+
severity: 'warning',
|
|
1533
|
+
rule: 'valid-dependency',
|
|
1534
|
+
message: `Dependency '${depId}' does not exist`,
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
return issues;
|
|
1540
|
+
}
|
|
1541
|
+
// ============================================================================
|
|
1542
|
+
// taskBatchValidate
|
|
1543
|
+
// ============================================================================
|
|
1544
|
+
/**
|
|
1545
|
+
* Validate multiple tasks at once.
|
|
1546
|
+
*
|
|
1547
|
+
* @param projectRoot - Absolute path to the CLEO project root directory
|
|
1548
|
+
* @param taskIds - Array of task IDs to validate
|
|
1549
|
+
* @param checkMode - Validation depth: "full" runs all checks, "quick" checks only title/description/status
|
|
1550
|
+
* @returns Per-task validation results and an aggregate summary with error/warning counts
|
|
1551
|
+
*
|
|
1552
|
+
* @remarks
|
|
1553
|
+
* In "full" mode, additional checks include title-description equality, parent existence,
|
|
1554
|
+
* dependency existence, and future timestamp detection. Tasks that are not found are
|
|
1555
|
+
* reported as errors.
|
|
1556
|
+
*
|
|
1557
|
+
* @example
|
|
1558
|
+
* ```typescript
|
|
1559
|
+
* const { summary } = await coreTaskBatchValidate('/project', ['T001', 'T002'], 'full');
|
|
1560
|
+
* console.log(`${summary.validTasks}/${summary.totalTasks} valid`);
|
|
1561
|
+
* ```
|
|
1562
|
+
*
|
|
1563
|
+
* @task T4790
|
|
1564
|
+
*/
|
|
1565
|
+
export async function coreTaskBatchValidate(projectRoot, taskIds, checkMode = 'full') {
|
|
1566
|
+
const allTasks = await loadAllTasks(projectRoot);
|
|
1567
|
+
const results = {};
|
|
1568
|
+
let totalErrors = 0;
|
|
1569
|
+
let totalWarnings = 0;
|
|
1570
|
+
for (const id of taskIds) {
|
|
1571
|
+
const task = allTasks.find((t) => t.id === id);
|
|
1572
|
+
if (!task) {
|
|
1573
|
+
results[id] = [{ severity: 'error', rule: 'exists', message: `Task '${id}' not found` }];
|
|
1574
|
+
totalErrors++;
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
const taskIssues = [];
|
|
1578
|
+
if (!task.title || task.title.trim().length === 0) {
|
|
1579
|
+
taskIssues.push({ severity: 'error', rule: 'title-required', message: 'Missing title' });
|
|
1580
|
+
}
|
|
1581
|
+
if (!task.description || task.description.trim().length === 0) {
|
|
1582
|
+
taskIssues.push({
|
|
1583
|
+
severity: 'warning',
|
|
1584
|
+
rule: 'description-required',
|
|
1585
|
+
message: 'Missing description',
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
if (!TASK_STATUSES.includes(task.status)) {
|
|
1589
|
+
taskIssues.push({
|
|
1590
|
+
severity: 'error',
|
|
1591
|
+
rule: 'valid-status',
|
|
1592
|
+
message: `Invalid status: ${task.status}`,
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
if (checkMode === 'full') {
|
|
1596
|
+
if (task.title && task.description && task.title.trim() === task.description.trim()) {
|
|
1597
|
+
taskIssues.push({
|
|
1598
|
+
severity: 'warning',
|
|
1599
|
+
rule: 'title-description-different',
|
|
1600
|
+
message: 'Title equals description',
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
if (task.parentId && !allTasks.some((t) => t.id === task.parentId)) {
|
|
1604
|
+
taskIssues.push({
|
|
1605
|
+
severity: 'error',
|
|
1606
|
+
rule: 'valid-parent',
|
|
1607
|
+
message: `Parent '${task.parentId}' not found`,
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
for (const depId of task.depends ?? []) {
|
|
1611
|
+
if (!allTasks.some((t) => t.id === depId)) {
|
|
1612
|
+
taskIssues.push({
|
|
1613
|
+
severity: 'warning',
|
|
1614
|
+
rule: 'valid-dependency',
|
|
1615
|
+
message: `Dependency '${depId}' not found`,
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
const now = new Date();
|
|
1620
|
+
if (task.createdAt && new Date(task.createdAt) > now) {
|
|
1621
|
+
taskIssues.push({
|
|
1622
|
+
severity: 'warning',
|
|
1623
|
+
rule: 'no-future-timestamps',
|
|
1624
|
+
message: 'createdAt in future',
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
results[id] = taskIssues;
|
|
1629
|
+
totalErrors += taskIssues.filter((i) => i.severity === 'error').length;
|
|
1630
|
+
totalWarnings += taskIssues.filter((i) => i.severity === 'warning').length;
|
|
1631
|
+
}
|
|
1632
|
+
const invalidTasks = Object.values(results).filter((issues) => issues.some((i) => i.severity === 'error')).length;
|
|
1633
|
+
return {
|
|
1634
|
+
results,
|
|
1635
|
+
summary: {
|
|
1636
|
+
totalTasks: taskIds.length,
|
|
1637
|
+
validTasks: taskIds.length - invalidTasks,
|
|
1638
|
+
invalidTasks,
|
|
1639
|
+
totalIssues: totalErrors + totalWarnings,
|
|
1640
|
+
errors: totalErrors,
|
|
1641
|
+
warnings: totalWarnings,
|
|
1642
|
+
},
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
// ============================================================================
|
|
1646
|
+
// taskImport
|
|
1647
|
+
// ============================================================================
|
|
1648
|
+
/**
|
|
1649
|
+
* Import tasks from a JSON source string.
|
|
1650
|
+
*
|
|
1651
|
+
* @param projectRoot - Absolute path to the CLEO project root directory
|
|
1652
|
+
* @param source - JSON string containing an array of tasks or an object with a `tasks` array
|
|
1653
|
+
* @param overwrite - When true, overwrites existing tasks with matching IDs; otherwise skips them
|
|
1654
|
+
* @returns Import summary with counts of imported, skipped, errors, and optional ID remap table
|
|
1655
|
+
*
|
|
1656
|
+
* @remarks
|
|
1657
|
+
* When a task ID collides with an existing one and overwrite is false, a new sequential
|
|
1658
|
+
* ID is assigned and recorded in the remapTable. Tasks missing required id or title
|
|
1659
|
+
* fields are skipped with an error message.
|
|
1660
|
+
*
|
|
1661
|
+
* @example
|
|
1662
|
+
* ```typescript
|
|
1663
|
+
* const json = JSON.stringify([{ id: 'T500', title: 'New task', status: 'pending', priority: 'medium' }]);
|
|
1664
|
+
* const result = await coreTaskImport('/project', json, false);
|
|
1665
|
+
* console.log(`Imported ${result.imported}, skipped ${result.skipped}`);
|
|
1666
|
+
* ```
|
|
1667
|
+
*
|
|
1668
|
+
* @task T4790
|
|
1669
|
+
*/
|
|
1670
|
+
export async function coreTaskImport(projectRoot, source, overwrite) {
|
|
1671
|
+
const accessor = await getAccessor(projectRoot);
|
|
1672
|
+
// Load all existing task IDs using queryTasks (bulk operation needs full ID set)
|
|
1673
|
+
const { tasks: existingTasks } = await accessor.queryTasks({});
|
|
1674
|
+
let importData;
|
|
1675
|
+
try {
|
|
1676
|
+
importData = JSON.parse(source);
|
|
1677
|
+
}
|
|
1678
|
+
catch {
|
|
1679
|
+
throw new Error('Invalid JSON in import source');
|
|
1680
|
+
}
|
|
1681
|
+
let importTasks = [];
|
|
1682
|
+
if (Array.isArray(importData)) {
|
|
1683
|
+
importTasks = importData;
|
|
1684
|
+
}
|
|
1685
|
+
else if (typeof importData === 'object' && importData !== null) {
|
|
1686
|
+
const data = importData;
|
|
1687
|
+
if (Array.isArray(data.tasks)) {
|
|
1688
|
+
importTasks = data.tasks;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
if (importTasks.length === 0) {
|
|
1692
|
+
return { imported: 0, skipped: 0, errors: ['No tasks found in import source'] };
|
|
1693
|
+
}
|
|
1694
|
+
const existingIds = new Set(existingTasks.map((t) => t.id));
|
|
1695
|
+
const allIds = new Set(existingTasks.map((t) => t.id));
|
|
1696
|
+
const errors = [];
|
|
1697
|
+
let imported = 0;
|
|
1698
|
+
let skipped = 0;
|
|
1699
|
+
const remapTable = {};
|
|
1700
|
+
let nextIdNum = 0;
|
|
1701
|
+
for (const t of existingTasks) {
|
|
1702
|
+
const num = parseInt(t.id.replace('T', ''), 10);
|
|
1703
|
+
if (!Number.isNaN(num) && num > nextIdNum)
|
|
1704
|
+
nextIdNum = num;
|
|
1705
|
+
}
|
|
1706
|
+
for (const importTask of importTasks) {
|
|
1707
|
+
if (!importTask.id || !importTask.title) {
|
|
1708
|
+
errors.push(`Skipped task with missing id or title`);
|
|
1709
|
+
skipped++;
|
|
1710
|
+
continue;
|
|
1711
|
+
}
|
|
1712
|
+
if (existingIds.has(importTask.id) && !overwrite) {
|
|
1713
|
+
skipped++;
|
|
1714
|
+
continue;
|
|
1715
|
+
}
|
|
1716
|
+
let newId = importTask.id;
|
|
1717
|
+
if (allIds.has(importTask.id) && !overwrite) {
|
|
1718
|
+
nextIdNum++;
|
|
1719
|
+
newId = `T${String(nextIdNum).padStart(3, '0')}`;
|
|
1720
|
+
remapTable[importTask.id] = newId;
|
|
1721
|
+
}
|
|
1722
|
+
const now = new Date().toISOString();
|
|
1723
|
+
const newTask = {
|
|
1724
|
+
...importTask,
|
|
1725
|
+
id: newId,
|
|
1726
|
+
createdAt: importTask.createdAt || now,
|
|
1727
|
+
updatedAt: now,
|
|
1728
|
+
};
|
|
1729
|
+
// Use targeted upsert per task instead of bulk saveTaskFile
|
|
1730
|
+
await accessor.upsertSingleTask(newTask);
|
|
1731
|
+
allIds.add(newId);
|
|
1732
|
+
imported++;
|
|
1733
|
+
}
|
|
1734
|
+
return {
|
|
1735
|
+
imported,
|
|
1736
|
+
skipped,
|
|
1737
|
+
errors,
|
|
1738
|
+
...(Object.keys(remapTable).length > 0 ? { remapTable } : {}),
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
//# sourceMappingURL=task-ops.js.map
|