prjct-cli 1.22.0 → 1.24.0
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/CHANGELOG.md +230 -0
- package/bin/prjct +30 -13
- package/dist/bin/prjct-core.mjs +1748 -0
- package/dist/bin/prjct.mjs +17 -36672
- package/dist/cli/linear.mjs +14 -0
- package/dist/daemon/entry.mjs +1429 -0
- package/dist/templates.json +1 -0
- package/package.json +4 -5
- package/bin/prjct.ts +0 -342
- package/core/__tests__/agentic/analysis-injection.test.ts +0 -377
- package/core/__tests__/agentic/cache-eviction.test.ts +0 -294
- package/core/__tests__/agentic/command-context.test.ts +0 -281
- package/core/__tests__/agentic/command-executor.test.ts +0 -659
- package/core/__tests__/agentic/domain-classifier.test.ts +0 -330
- package/core/__tests__/agentic/injection-validator.test.ts +0 -255
- package/core/__tests__/agentic/memory-system.test.ts +0 -281
- package/core/__tests__/agentic/plan-mode.test.ts +0 -386
- package/core/__tests__/agentic/prompt-assembly.test.ts +0 -298
- package/core/__tests__/agentic/prompt-builder.test.ts +0 -243
- package/core/__tests__/agentic/response-validator.test.ts +0 -263
- package/core/__tests__/agentic/semantic-matching.test.ts +0 -131
- package/core/__tests__/agentic/smart-context.test.ts +0 -372
- package/core/__tests__/agentic/tech-normalizer.test.ts +0 -136
- package/core/__tests__/agentic/token-budget.test.ts +0 -294
- package/core/__tests__/ai-tools/formatters.test.ts +0 -476
- package/core/__tests__/domain/bm25.test.ts +0 -225
- package/core/__tests__/domain/change-propagator.test.ts +0 -100
- package/core/__tests__/domain/fibonacci.test.ts +0 -113
- package/core/__tests__/domain/file-hasher.test.ts +0 -146
- package/core/__tests__/domain/file-ranker.test.ts +0 -169
- package/core/__tests__/domain/git-cochange.test.ts +0 -121
- package/core/__tests__/domain/import-graph.test.ts +0 -156
- package/core/__tests__/domain/velocity.test.ts +0 -623
- package/core/__tests__/infrastructure/performance-tracker.test.ts +0 -328
- package/core/__tests__/schemas/model.test.ts +0 -272
- package/core/__tests__/services/dependency-validator.test.ts +0 -175
- package/core/__tests__/services/hierarchical-agent-resolver.test.ts +0 -359
- package/core/__tests__/services/nested-context-resolver.test.ts +0 -443
- package/core/__tests__/services/project-index.test.ts +0 -355
- package/core/__tests__/services/staleness-checker.test.ts +0 -204
- package/core/__tests__/storage/analysis-storage.test.ts +0 -641
- package/core/__tests__/storage/archive-storage.test.ts +0 -455
- package/core/__tests__/storage/safe-reader.test.ts +0 -262
- package/core/__tests__/storage/sqlite-migration.test.ts +0 -1016
- package/core/__tests__/storage/state-storage-feedback.test.ts +0 -463
- package/core/__tests__/storage/state-storage-history.test.ts +0 -469
- package/core/__tests__/storage/storage-manager.test.ts +0 -383
- package/core/__tests__/storage/subtask-handoff.test.ts +0 -237
- package/core/__tests__/types/fs.test.ts +0 -125
- package/core/__tests__/utils/date-helper.test.ts +0 -449
- package/core/__tests__/utils/output.test.ts +0 -278
- package/core/__tests__/utils/preserve-sections.test.ts +0 -216
- package/core/__tests__/utils/project-commands.test.ts +0 -71
- package/core/__tests__/utils/retry.test.ts +0 -381
- package/core/__tests__/workflow/state-machine.test.ts +0 -216
- package/core/agentic/agent-router.ts +0 -150
- package/core/agentic/anti-hallucination.ts +0 -141
- package/core/agentic/chain-of-thought.ts +0 -234
- package/core/agentic/command-classifier.ts +0 -141
- package/core/agentic/command-context.ts +0 -168
- package/core/agentic/command-executor.ts +0 -471
- package/core/agentic/context-builder.ts +0 -285
- package/core/agentic/domain-classifier.ts +0 -525
- package/core/agentic/environment-block.ts +0 -102
- package/core/agentic/ground-truth.ts +0 -706
- package/core/agentic/index.ts +0 -193
- package/core/agentic/injection-validator.ts +0 -208
- package/core/agentic/loop-detector.ts +0 -451
- package/core/agentic/memory-system.ts +0 -1547
- package/core/agentic/orchestrator-executor.ts +0 -579
- package/core/agentic/plan-mode.ts +0 -525
- package/core/agentic/prompt-builder.ts +0 -1069
- package/core/agentic/response-validator.ts +0 -98
- package/core/agentic/services.ts +0 -167
- package/core/agentic/skill-loader.ts +0 -106
- package/core/agentic/smart-context.ts +0 -393
- package/core/agentic/tech-normalizer.ts +0 -167
- package/core/agentic/template-executor.ts +0 -272
- package/core/agentic/template-loader.ts +0 -109
- package/core/agentic/token-budget.ts +0 -226
- package/core/agentic/tool-registry.ts +0 -146
- package/core/agents/index.ts +0 -28
- package/core/agents/performance.ts +0 -429
- package/core/ai-tools/formatters.ts +0 -341
- package/core/ai-tools/generator.ts +0 -144
- package/core/ai-tools/index.ts +0 -15
- package/core/ai-tools/registry.ts +0 -201
- package/core/bus/bus.ts +0 -314
- package/core/bus/index.ts +0 -8
- package/core/cli/linear.ts +0 -500
- package/core/cli/lint-meta-commentary.ts +0 -177
- package/core/cli/start.ts +0 -386
- package/core/commands/analysis.ts +0 -1274
- package/core/commands/analytics.ts +0 -342
- package/core/commands/base.ts +0 -118
- package/core/commands/cleanup.ts +0 -157
- package/core/commands/command-data.ts +0 -463
- package/core/commands/commands.ts +0 -306
- package/core/commands/context.ts +0 -238
- package/core/commands/design.ts +0 -77
- package/core/commands/index.ts +0 -19
- package/core/commands/maintenance.ts +0 -77
- package/core/commands/performance.ts +0 -114
- package/core/commands/planning.ts +0 -662
- package/core/commands/register.ts +0 -127
- package/core/commands/registry.ts +0 -444
- package/core/commands/setup.ts +0 -280
- package/core/commands/shipping.ts +0 -267
- package/core/commands/snapshots.ts +0 -297
- package/core/commands/uninstall.ts +0 -542
- package/core/commands/velocity.ts +0 -149
- package/core/commands/workflow.ts +0 -505
- package/core/config/command-context.config.json +0 -66
- package/core/constants/index.ts +0 -379
- package/core/context/generator.ts +0 -368
- package/core/context-tools/files-tool.ts +0 -577
- package/core/context-tools/imports-tool.ts +0 -400
- package/core/context-tools/index.ts +0 -434
- package/core/context-tools/recent-tool.ts +0 -301
- package/core/context-tools/signatures-tool.ts +0 -495
- package/core/context-tools/summary-tool.ts +0 -301
- package/core/context-tools/token-counter.ts +0 -273
- package/core/context-tools/types.ts +0 -253
- package/core/domain/agent-generator.ts +0 -186
- package/core/domain/agent-loader.ts +0 -419
- package/core/domain/analyzer.ts +0 -387
- package/core/domain/architecture-generator.ts +0 -108
- package/core/domain/bm25.ts +0 -525
- package/core/domain/change-propagator.ts +0 -162
- package/core/domain/context-estimator.ts +0 -175
- package/core/domain/fibonacci.ts +0 -128
- package/core/domain/file-hasher.ts +0 -296
- package/core/domain/file-ranker.ts +0 -151
- package/core/domain/git-cochange.ts +0 -250
- package/core/domain/import-graph.ts +0 -315
- package/core/domain/snapshot-manager.ts +0 -415
- package/core/domain/task-stack.ts +0 -578
- package/core/domain/velocity.ts +0 -470
- package/core/errors.ts +0 -335
- package/core/events/events.ts +0 -85
- package/core/events/index.ts +0 -8
- package/core/index.ts +0 -481
- package/core/infrastructure/agent-detector.ts +0 -135
- package/core/infrastructure/ai-provider.ts +0 -578
- package/core/infrastructure/author-detector.ts +0 -133
- package/core/infrastructure/capability-installer.ts +0 -76
- package/core/infrastructure/claude-agent.ts +0 -297
- package/core/infrastructure/command-installer.ts +0 -752
- package/core/infrastructure/config-manager.ts +0 -364
- package/core/infrastructure/editors-config.ts +0 -172
- package/core/infrastructure/path-manager.ts +0 -571
- package/core/infrastructure/performance-tracker.ts +0 -326
- package/core/infrastructure/permission-manager.ts +0 -289
- package/core/infrastructure/setup.ts +0 -1061
- package/core/infrastructure/update-checker.ts +0 -246
- package/core/integrations/issue-tracker/enricher.ts +0 -271
- package/core/integrations/issue-tracker/index.ts +0 -8
- package/core/integrations/issue-tracker/manager.ts +0 -286
- package/core/integrations/issue-tracker/types.ts +0 -310
- package/core/integrations/jira/cache.ts +0 -57
- package/core/integrations/jira/client.ts +0 -688
- package/core/integrations/jira/index.ts +0 -23
- package/core/integrations/jira/service.ts +0 -244
- package/core/integrations/linear/cache.ts +0 -68
- package/core/integrations/linear/client.ts +0 -436
- package/core/integrations/linear/index.ts +0 -20
- package/core/integrations/linear/service.ts +0 -260
- package/core/integrations/linear/sync.ts +0 -314
- package/core/outcomes/analyzer.ts +0 -286
- package/core/outcomes/index.ts +0 -34
- package/core/outcomes/recorder.ts +0 -195
- package/core/plugin/builtin/webhook.ts +0 -148
- package/core/plugin/hooks.ts +0 -315
- package/core/plugin/index.ts +0 -50
- package/core/plugin/loader.ts +0 -354
- package/core/plugin/registry.ts +0 -326
- package/core/schemas/agents.ts +0 -27
- package/core/schemas/analysis.ts +0 -530
- package/core/schemas/classification.ts +0 -91
- package/core/schemas/command-context.ts +0 -29
- package/core/schemas/enriched-task.ts +0 -291
- package/core/schemas/ideas.ts +0 -114
- package/core/schemas/index.ts +0 -53
- package/core/schemas/issues.ts +0 -159
- package/core/schemas/llm-output.ts +0 -170
- package/core/schemas/metrics.ts +0 -143
- package/core/schemas/model.ts +0 -153
- package/core/schemas/outcomes.ts +0 -487
- package/core/schemas/performance.ts +0 -128
- package/core/schemas/permissions.ts +0 -180
- package/core/schemas/prd.ts +0 -450
- package/core/schemas/project.ts +0 -57
- package/core/schemas/roadmap.ts +0 -322
- package/core/schemas/schemas.ts +0 -38
- package/core/schemas/shipped.ts +0 -109
- package/core/schemas/state.ts +0 -284
- package/core/schemas/velocity.ts +0 -103
- package/core/server/index.ts +0 -21
- package/core/server/routes-extended.ts +0 -566
- package/core/server/routes.ts +0 -176
- package/core/server/server.ts +0 -149
- package/core/server/sse.ts +0 -192
- package/core/services/agent-generator.ts +0 -385
- package/core/services/agent-service.ts +0 -168
- package/core/services/breakdown-service.ts +0 -124
- package/core/services/context-generator.ts +0 -445
- package/core/services/context-selector.ts +0 -429
- package/core/services/dependency-validator.ts +0 -318
- package/core/services/diff-generator.ts +0 -313
- package/core/services/doctor-service.ts +0 -423
- package/core/services/file-categorizer.ts +0 -448
- package/core/services/file-scorer.ts +0 -270
- package/core/services/git-analyzer.ts +0 -293
- package/core/services/hierarchical-agent-resolver.ts +0 -236
- package/core/services/hooks-service.ts +0 -685
- package/core/services/index.ts +0 -46
- package/core/services/local-state-generator.ts +0 -158
- package/core/services/memory-service.ts +0 -181
- package/core/services/nested-context-resolver.ts +0 -842
- package/core/services/project-index.ts +0 -911
- package/core/services/project-service.ts +0 -155
- package/core/services/session-tracker.ts +0 -287
- package/core/services/skill-installer.ts +0 -447
- package/core/services/skill-lock.ts +0 -132
- package/core/services/skill-service.ts +0 -306
- package/core/services/stack-detector.ts +0 -229
- package/core/services/staleness-checker.ts +0 -327
- package/core/services/sync-service.ts +0 -1515
- package/core/services/sync-verifier.ts +0 -253
- package/core/services/watch-service.ts +0 -312
- package/core/session/compaction.ts +0 -248
- package/core/session/index.ts +0 -35
- package/core/session/log-migration.ts +0 -88
- package/core/session/metrics.ts +0 -323
- package/core/session/session-log-manager.ts +0 -307
- package/core/session/task-session-manager.ts +0 -404
- package/core/session/utils.ts +0 -51
- package/core/storage/analysis-storage.ts +0 -373
- package/core/storage/archive-storage.ts +0 -205
- package/core/storage/database.ts +0 -575
- package/core/storage/ideas-storage.ts +0 -298
- package/core/storage/index-storage.ts +0 -523
- package/core/storage/index.ts +0 -79
- package/core/storage/metrics-storage.ts +0 -321
- package/core/storage/migrate-json.ts +0 -720
- package/core/storage/queue-storage.ts +0 -336
- package/core/storage/safe-reader.ts +0 -105
- package/core/storage/shipped-storage.ts +0 -253
- package/core/storage/state-storage.ts +0 -1035
- package/core/storage/storage-manager.ts +0 -205
- package/core/storage/storage.ts +0 -177
- package/core/storage/velocity-storage.ts +0 -149
- package/core/sync/auth-config.ts +0 -138
- package/core/sync/index.ts +0 -31
- package/core/sync/oauth-handler.ts +0 -143
- package/core/sync/sync-client.ts +0 -251
- package/core/sync/sync-manager.ts +0 -327
- package/core/tsconfig.json +0 -22
- package/core/types/agentic.ts +0 -760
- package/core/types/agents.ts +0 -150
- package/core/types/bus.ts +0 -193
- package/core/types/citations.ts +0 -22
- package/core/types/commands.ts +0 -399
- package/core/types/config.ts +0 -92
- package/core/types/core.ts +0 -96
- package/core/types/diff.ts +0 -41
- package/core/types/domain.ts +0 -71
- package/core/types/errors.ts +0 -111
- package/core/types/events.ts +0 -42
- package/core/types/fs.ts +0 -72
- package/core/types/index.ts +0 -510
- package/core/types/infrastructure.ts +0 -210
- package/core/types/integrations.ts +0 -31
- package/core/types/jira.ts +0 -51
- package/core/types/logger.ts +0 -17
- package/core/types/memory.ts +0 -313
- package/core/types/outcomes.ts +0 -190
- package/core/types/output.ts +0 -47
- package/core/types/plugin.ts +0 -25
- package/core/types/project-sync.ts +0 -129
- package/core/types/provider.ts +0 -163
- package/core/types/server.ts +0 -71
- package/core/types/services.ts +0 -84
- package/core/types/session.ts +0 -135
- package/core/types/stack.ts +0 -19
- package/core/types/storage.ts +0 -318
- package/core/types/sync-verifier.ts +0 -33
- package/core/types/sync.ts +0 -121
- package/core/types/task.ts +0 -72
- package/core/types/template.ts +0 -24
- package/core/types/utils.ts +0 -92
- package/core/types/workflow.ts +0 -23
- package/core/utils/agent-stream.ts +0 -140
- package/core/utils/animations.ts +0 -251
- package/core/utils/branding.ts +0 -88
- package/core/utils/cache.ts +0 -187
- package/core/utils/citations.ts +0 -39
- package/core/utils/collection-filters.ts +0 -209
- package/core/utils/date-helper.ts +0 -176
- package/core/utils/error-messages.ts +0 -38
- package/core/utils/file-helper.ts +0 -277
- package/core/utils/fs-helpers.ts +0 -14
- package/core/utils/help.ts +0 -314
- package/core/utils/jsonl-helper.ts +0 -290
- package/core/utils/keychain.ts +0 -127
- package/core/utils/logger.ts +0 -77
- package/core/utils/markdown-builder.ts +0 -280
- package/core/utils/next-steps.ts +0 -95
- package/core/utils/output.ts +0 -403
- package/core/utils/preserve-sections.ts +0 -218
- package/core/utils/project-commands.ts +0 -126
- package/core/utils/project-credentials.ts +0 -143
- package/core/utils/provider-cache.ts +0 -49
- package/core/utils/retry.ts +0 -318
- package/core/utils/runtime.ts +0 -108
- package/core/utils/session-helper.ts +0 -278
- package/core/utils/subtask-table.ts +0 -227
- package/core/utils/version.ts +0 -128
- package/core/wizard/index.ts +0 -13
- package/core/wizard/onboarding.ts +0 -633
- package/core/workflow/index.ts +0 -7
- package/core/workflow/state-machine.ts +0 -198
- package/core/workflow/workflow-preferences.ts +0 -294
- package/dist/core/infrastructure/command-installer.js +0 -1141
- package/dist/core/infrastructure/editors-config.js +0 -177
- package/dist/core/infrastructure/setup.js +0 -2244
- package/dist/core/utils/version.js +0 -141
- package/templates/agentic/agent-routing.md +0 -45
- package/templates/agentic/agents/uxui.md +0 -63
- package/templates/agentic/checklist-routing.md +0 -98
- package/templates/agentic/orchestrator.md +0 -68
- package/templates/agentic/task-fragmentation.md +0 -89
- package/templates/agents/AGENTS.md +0 -68
- package/templates/analysis/analyze.md +0 -84
- package/templates/analysis/patterns.md +0 -60
- package/templates/antigravity/SKILL.md +0 -39
- package/templates/architect/discovery.md +0 -67
- package/templates/architect/phases.md +0 -59
- package/templates/checklists/architecture.md +0 -28
- package/templates/checklists/code-quality.md +0 -28
- package/templates/checklists/data.md +0 -33
- package/templates/checklists/documentation.md +0 -33
- package/templates/checklists/infrastructure.md +0 -33
- package/templates/checklists/performance.md +0 -33
- package/templates/checklists/security.md +0 -33
- package/templates/checklists/testing.md +0 -33
- package/templates/checklists/ux-ui.md +0 -37
- package/templates/commands/analyze.md +0 -56
- package/templates/commands/auth.md +0 -234
- package/templates/commands/bug.md +0 -163
- package/templates/commands/cleanup.md +0 -19
- package/templates/commands/dash.md +0 -99
- package/templates/commands/design.md +0 -15
- package/templates/commands/done.md +0 -291
- package/templates/commands/enrich.md +0 -174
- package/templates/commands/git.md +0 -295
- package/templates/commands/history.md +0 -389
- package/templates/commands/idea.md +0 -88
- package/templates/commands/impact.md +0 -864
- package/templates/commands/init.md +0 -54
- package/templates/commands/jira.md +0 -278
- package/templates/commands/linear.md +0 -288
- package/templates/commands/merge.md +0 -206
- package/templates/commands/next.md +0 -80
- package/templates/commands/p.md +0 -67
- package/templates/commands/p.toml +0 -37
- package/templates/commands/pause.md +0 -136
- package/templates/commands/plan.md +0 -696
- package/templates/commands/prd.md +0 -356
- package/templates/commands/resume.md +0 -171
- package/templates/commands/review.md +0 -276
- package/templates/commands/serve.md +0 -118
- package/templates/commands/setup.md +0 -91
- package/templates/commands/ship.md +0 -475
- package/templates/commands/skill.md +0 -259
- package/templates/commands/spec.md +0 -218
- package/templates/commands/status.md +0 -207
- package/templates/commands/sync.md +0 -104
- package/templates/commands/task.md +0 -312
- package/templates/commands/test.md +0 -93
- package/templates/commands/update.md +0 -63
- package/templates/commands/verify.md +0 -204
- package/templates/commands/workflow.md +0 -150
- package/templates/config/skill-mappings.json +0 -82
- package/templates/context/dashboard.md +0 -256
- package/templates/context/roadmap.md +0 -221
- package/templates/cursor/commands/bug.md +0 -8
- package/templates/cursor/commands/done.md +0 -4
- package/templates/cursor/commands/pause.md +0 -6
- package/templates/cursor/commands/resume.md +0 -4
- package/templates/cursor/commands/ship.md +0 -8
- package/templates/cursor/commands/sync.md +0 -4
- package/templates/cursor/commands/task.md +0 -8
- package/templates/cursor/p.md +0 -29
- package/templates/cursor/router.mdc +0 -28
- package/templates/design/api.md +0 -95
- package/templates/design/architecture.md +0 -77
- package/templates/design/component.md +0 -89
- package/templates/design/database.md +0 -78
- package/templates/design/flow.md +0 -94
- package/templates/global/ANTIGRAVITY.md +0 -254
- package/templates/global/CLAUDE.md +0 -497
- package/templates/global/CURSOR.mdc +0 -266
- package/templates/global/GEMINI.md +0 -293
- package/templates/global/STORAGE-SPEC.md +0 -391
- package/templates/global/WINDSURF.md +0 -266
- package/templates/global/modules/CLAUDE-commands.md +0 -70
- package/templates/global/modules/CLAUDE-core.md +0 -105
- package/templates/global/modules/CLAUDE-git.md +0 -50
- package/templates/global/modules/CLAUDE-intelligence.md +0 -92
- package/templates/global/modules/CLAUDE-storage.md +0 -50
- package/templates/global/modules/module-config.json +0 -36
- package/templates/mcp-config.json +0 -19
- package/templates/permissions/default.jsonc +0 -60
- package/templates/permissions/permissive.jsonc +0 -49
- package/templates/permissions/strict.jsonc +0 -58
- package/templates/planning-methodology.md +0 -195
- package/templates/skills/code-review.md +0 -47
- package/templates/skills/debug.md +0 -61
- package/templates/skills/refactor.md +0 -47
- package/templates/subagents/agent-base.md +0 -20
- package/templates/subagents/domain/backend.md +0 -109
- package/templates/subagents/domain/database.md +0 -121
- package/templates/subagents/domain/devops.md +0 -152
- package/templates/subagents/domain/frontend.md +0 -103
- package/templates/subagents/domain/testing.md +0 -169
- package/templates/subagents/pm-expert.md +0 -366
- package/templates/subagents/workflow/chief-architect.md +0 -657
- package/templates/subagents/workflow/prjct-planner.md +0 -159
- package/templates/subagents/workflow/prjct-shipper.md +0 -188
- package/templates/subagents/workflow/prjct-workflow.md +0 -98
- package/templates/tools/bash.txt +0 -22
- package/templates/tools/edit.txt +0 -18
- package/templates/tools/glob.txt +0 -19
- package/templates/tools/grep.txt +0 -21
- package/templates/tools/read.txt +0 -14
- package/templates/tools/task.txt +0 -20
- package/templates/tools/webfetch.txt +0 -16
- package/templates/tools/websearch.txt +0 -18
- package/templates/tools/write.txt +0 -17
- package/templates/windsurf/router.md +0 -28
- package/templates/windsurf/workflows/bug.md +0 -8
- package/templates/windsurf/workflows/done.md +0 -4
- package/templates/windsurf/workflows/pause.md +0 -4
- package/templates/windsurf/workflows/resume.md +0 -4
- package/templates/windsurf/workflows/ship.md +0 -8
- package/templates/windsurf/workflows/sync.md +0 -4
- package/templates/windsurf/workflows/task.md +0 -8
|
@@ -1,1035 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* State Storage
|
|
3
|
-
*
|
|
4
|
-
* Manages current task state via storage/state.json
|
|
5
|
-
* Generates context/now.md for Claude
|
|
6
|
-
*
|
|
7
|
-
* Note: Local .prjct-state.md is generated by sync-service which has access
|
|
8
|
-
* to projectPath. This class only has projectId.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { generateUUID } from '../schemas'
|
|
12
|
-
import type {
|
|
13
|
-
CurrentTask,
|
|
14
|
-
PreviousTask,
|
|
15
|
-
StateJson,
|
|
16
|
-
Subtask,
|
|
17
|
-
SubtaskCompletionData,
|
|
18
|
-
TaskFeedback,
|
|
19
|
-
TaskHistoryEntry,
|
|
20
|
-
} from '../schemas/state'
|
|
21
|
-
import { StateJsonSchema, SubtaskCompletionDataSchema } from '../schemas/state'
|
|
22
|
-
import { getTimestamp, toRelative } from '../utils/date-helper'
|
|
23
|
-
import { md } from '../utils/markdown-builder'
|
|
24
|
-
import type { WorkflowCommand } from '../workflow/state-machine'
|
|
25
|
-
import { workflowStateMachine } from '../workflow/state-machine'
|
|
26
|
-
import { archiveStorage } from './archive-storage'
|
|
27
|
-
import { StorageManager } from './storage-manager'
|
|
28
|
-
|
|
29
|
-
class StateStorage extends StorageManager<StateJson> {
|
|
30
|
-
constructor() {
|
|
31
|
-
super('state.json', StateJsonSchema)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
protected getDefault(): StateJson {
|
|
35
|
-
return {
|
|
36
|
-
currentTask: null,
|
|
37
|
-
previousTask: null,
|
|
38
|
-
pausedTasks: [],
|
|
39
|
-
taskHistory: [],
|
|
40
|
-
lastUpdated: '',
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
protected getMdFilename(): string {
|
|
45
|
-
return 'now.md'
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
protected getLayer(): string {
|
|
49
|
-
return 'core'
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
protected getEventType(action: 'update' | 'create' | 'delete'): string {
|
|
53
|
-
return `state.${action}d`
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
protected toMarkdown(data: StateJson): string {
|
|
57
|
-
return md()
|
|
58
|
-
.h1('NOW')
|
|
59
|
-
.when(!!data.currentTask, (m) => {
|
|
60
|
-
const task = data.currentTask!
|
|
61
|
-
m.bold(task.description)
|
|
62
|
-
.blank()
|
|
63
|
-
.raw(`Started: ${toRelative(task.startedAt)}`)
|
|
64
|
-
.raw(`Session: ${task.sessionId}`)
|
|
65
|
-
.maybe(task.featureId, (m, id) => m.raw(`Feature: ${id}`))
|
|
66
|
-
|
|
67
|
-
// Subtask progress table
|
|
68
|
-
if (task.subtasks && task.subtasks.length > 0) {
|
|
69
|
-
m.blank()
|
|
70
|
-
.h2('Subtasks Progress')
|
|
71
|
-
.raw(
|
|
72
|
-
`**Progress**: ${task.subtaskProgress?.completed || 0}/${task.subtaskProgress?.total || 0} (${task.subtaskProgress?.percentage || 0}%)`
|
|
73
|
-
)
|
|
74
|
-
.blank()
|
|
75
|
-
.raw('| # | Domain | Description | Status | Agent |')
|
|
76
|
-
.raw('|---|--------|-------------|--------|-------|')
|
|
77
|
-
|
|
78
|
-
task.subtasks.forEach((subtask, index) => {
|
|
79
|
-
const statusIcon =
|
|
80
|
-
subtask.status === 'completed'
|
|
81
|
-
? '✅'
|
|
82
|
-
: subtask.status === 'in_progress'
|
|
83
|
-
? '▶️'
|
|
84
|
-
: subtask.status === 'failed'
|
|
85
|
-
? '❌'
|
|
86
|
-
: subtask.status === 'skipped'
|
|
87
|
-
? '⏭️'
|
|
88
|
-
: subtask.status === 'blocked'
|
|
89
|
-
? '🚫'
|
|
90
|
-
: '⏳'
|
|
91
|
-
const isActive = index === task.currentSubtaskIndex ? ' **← Active**' : ''
|
|
92
|
-
m.raw(
|
|
93
|
-
`| ${index + 1} | ${subtask.domain} | ${subtask.description} | ${statusIcon} ${subtask.status}${isActive} | ${subtask.agent} |`
|
|
94
|
-
)
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
// Current subtask details
|
|
98
|
-
const currentSubtask = task.subtasks[task.currentSubtaskIndex || 0]
|
|
99
|
-
if (currentSubtask && currentSubtask.status === 'in_progress') {
|
|
100
|
-
m.blank()
|
|
101
|
-
.h3('Current Subtask')
|
|
102
|
-
.raw(`**#${(task.currentSubtaskIndex || 0) + 1}**: ${currentSubtask.description}`)
|
|
103
|
-
.raw(`**Agent**: ${currentSubtask.agent}`)
|
|
104
|
-
.raw(`**Domain**: ${currentSubtask.domain}`)
|
|
105
|
-
|
|
106
|
-
// Show dependencies
|
|
107
|
-
if (currentSubtask.dependsOn.length > 0) {
|
|
108
|
-
m.raw(`**Depends on**: ${currentSubtask.dependsOn.join(', ')}`)
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Show last completed subtask summary if available
|
|
113
|
-
const completedSubtasks = task.subtasks.filter(
|
|
114
|
-
(s) => s.status === 'completed' && s.summary
|
|
115
|
-
)
|
|
116
|
-
if (completedSubtasks.length > 0) {
|
|
117
|
-
const lastCompleted = completedSubtasks[completedSubtasks.length - 1]
|
|
118
|
-
if (lastCompleted.summary) {
|
|
119
|
-
m.blank()
|
|
120
|
-
.h3('Previous Subtask Output')
|
|
121
|
-
.raw(`**${lastCompleted.summary.title}**`)
|
|
122
|
-
.raw(lastCompleted.summary.description)
|
|
123
|
-
.maybe(lastCompleted.summary.outputForNextAgent, (m, output) =>
|
|
124
|
-
m.blank().raw(`**Available for next agent**: ${output}`)
|
|
125
|
-
)
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
})
|
|
130
|
-
.when(!data.currentTask, (m) => {
|
|
131
|
-
m.italic('No active task. Use /p:work to start.')
|
|
132
|
-
})
|
|
133
|
-
.when((data.pausedTasks?.length || 0) > 0 || !!data.previousTask, (m) => {
|
|
134
|
-
const paused = data.pausedTasks?.length
|
|
135
|
-
? data.pausedTasks
|
|
136
|
-
: data.previousTask
|
|
137
|
-
? [data.previousTask]
|
|
138
|
-
: []
|
|
139
|
-
if (paused.length === 0) return
|
|
140
|
-
m.hr().h2(`Paused (${paused.length})`)
|
|
141
|
-
paused.forEach((prev, i) => {
|
|
142
|
-
m.raw(`${i + 1}. **${prev.description}**`).raw(` Paused: ${toRelative(prev.pausedAt)}`)
|
|
143
|
-
if (prev.pauseReason) m.raw(` Reason: ${prev.pauseReason}`)
|
|
144
|
-
})
|
|
145
|
-
m.blank().italic('Use /p:resume to continue')
|
|
146
|
-
})
|
|
147
|
-
.when((data.taskHistory?.length || 0) > 0, (m) => {
|
|
148
|
-
const history = this.getTaskHistoryFromState(data)
|
|
149
|
-
if (history.length === 0) return
|
|
150
|
-
|
|
151
|
-
// Filter by current task classification if available
|
|
152
|
-
const currentTaskType = (data.currentTask as any)?.type
|
|
153
|
-
const relevantHistory = currentTaskType
|
|
154
|
-
? history.filter((h) => h.classification === currentTaskType).slice(0, 3)
|
|
155
|
-
: history.slice(0, 5)
|
|
156
|
-
|
|
157
|
-
if (relevantHistory.length === 0) return
|
|
158
|
-
|
|
159
|
-
m.hr().h2(
|
|
160
|
-
currentTaskType
|
|
161
|
-
? `Recent ${currentTaskType} tasks (${relevantHistory.length})`
|
|
162
|
-
: `Recent tasks (${relevantHistory.length})`
|
|
163
|
-
)
|
|
164
|
-
relevantHistory.forEach((entry, i) => {
|
|
165
|
-
m.raw(`${i + 1}. **${entry.title}** (${entry.classification})`)
|
|
166
|
-
.raw(
|
|
167
|
-
` Completed: ${toRelative(entry.completedAt)} | ${entry.subtaskCount} subtask${entry.subtaskCount > 1 ? 's' : ''}`
|
|
168
|
-
)
|
|
169
|
-
.raw(` Outcome: ${entry.outcome}`)
|
|
170
|
-
if (entry.linearId) m.raw(` Linear: ${entry.linearId}`)
|
|
171
|
-
if (entry.feedback?.patternsDiscovered?.length) {
|
|
172
|
-
m.raw(` Patterns: ${entry.feedback.patternsDiscovered.join(', ')}`)
|
|
173
|
-
}
|
|
174
|
-
if (entry.feedback?.issuesEncountered?.length) {
|
|
175
|
-
m.raw(` Gotchas: ${entry.feedback.issuesEncountered.join(', ')}`)
|
|
176
|
-
}
|
|
177
|
-
})
|
|
178
|
-
m.blank().italic('Task history helps identify patterns and improve decisions')
|
|
179
|
-
})
|
|
180
|
-
.blank()
|
|
181
|
-
.build()
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// =========== Transition Validation ===========
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Validate a state transition through the state machine.
|
|
188
|
-
* Throws if the transition is invalid.
|
|
189
|
-
*/
|
|
190
|
-
private validateTransition(state: StateJson, command: WorkflowCommand): void {
|
|
191
|
-
const currentState = workflowStateMachine.getCurrentState(state)
|
|
192
|
-
const result = workflowStateMachine.canTransition(currentState, command)
|
|
193
|
-
if (!result.valid) {
|
|
194
|
-
throw new Error(`${result.error}. ${result.suggestion || ''}`.trim())
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// =========== Domain Methods ===========
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Get current active task
|
|
202
|
-
*/
|
|
203
|
-
async getCurrentTask(projectId: string): Promise<CurrentTask | null> {
|
|
204
|
-
const state = await this.read(projectId)
|
|
205
|
-
return state.currentTask
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Start a new task
|
|
210
|
-
*/
|
|
211
|
-
async startTask(projectId: string, task: Omit<CurrentTask, 'startedAt'>): Promise<CurrentTask> {
|
|
212
|
-
const state = await this.read(projectId)
|
|
213
|
-
this.validateTransition(state, 'task')
|
|
214
|
-
|
|
215
|
-
const currentTask: CurrentTask = {
|
|
216
|
-
...task,
|
|
217
|
-
startedAt: getTimestamp(),
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
await this.update(projectId, (state) => ({
|
|
221
|
-
...state,
|
|
222
|
-
currentTask,
|
|
223
|
-
lastUpdated: getTimestamp(),
|
|
224
|
-
}))
|
|
225
|
-
|
|
226
|
-
// Publish incremental event
|
|
227
|
-
await this.publishEvent(projectId, 'task.started', {
|
|
228
|
-
taskId: currentTask.id,
|
|
229
|
-
description: currentTask.description,
|
|
230
|
-
startedAt: currentTask.startedAt,
|
|
231
|
-
sessionId: currentTask.sessionId,
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
return currentTask
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Update fields on the current task (partial update)
|
|
239
|
-
*/
|
|
240
|
-
async updateCurrentTask(
|
|
241
|
-
projectId: string,
|
|
242
|
-
fields: Partial<CurrentTask>
|
|
243
|
-
): Promise<CurrentTask | null> {
|
|
244
|
-
const state = await this.read(projectId)
|
|
245
|
-
if (!state.currentTask) return null
|
|
246
|
-
|
|
247
|
-
const updated: CurrentTask = { ...state.currentTask, ...fields }
|
|
248
|
-
|
|
249
|
-
await this.update(projectId, (s) => ({
|
|
250
|
-
...s,
|
|
251
|
-
currentTask: updated,
|
|
252
|
-
lastUpdated: getTimestamp(),
|
|
253
|
-
}))
|
|
254
|
-
|
|
255
|
-
return updated
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Complete current task
|
|
260
|
-
* Creates a TaskHistoryEntry and adds it to taskHistory with FIFO eviction
|
|
261
|
-
* Optionally accepts structured feedback for the task-to-analysis feedback loop (PRJ-272)
|
|
262
|
-
*/
|
|
263
|
-
async completeTask(projectId: string, feedback?: TaskFeedback): Promise<CurrentTask | null> {
|
|
264
|
-
const state = await this.read(projectId)
|
|
265
|
-
const completedTask = state.currentTask
|
|
266
|
-
|
|
267
|
-
if (!completedTask) {
|
|
268
|
-
return null
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
this.validateTransition(state, 'done')
|
|
272
|
-
|
|
273
|
-
const completedAt = getTimestamp()
|
|
274
|
-
|
|
275
|
-
// Create task history entry for completed task (with optional feedback)
|
|
276
|
-
const historyEntry = this.createTaskHistoryEntry(completedTask, completedAt, feedback)
|
|
277
|
-
|
|
278
|
-
// Get existing task history with backward compatibility
|
|
279
|
-
const existingHistory = this.getTaskHistoryFromState(state)
|
|
280
|
-
|
|
281
|
-
// Add new entry to beginning, enforce max limit with FIFO eviction
|
|
282
|
-
const taskHistory = [historyEntry, ...existingHistory].slice(0, this.maxTaskHistory)
|
|
283
|
-
|
|
284
|
-
await this.update(projectId, () => ({
|
|
285
|
-
currentTask: null,
|
|
286
|
-
previousTask: null,
|
|
287
|
-
taskHistory,
|
|
288
|
-
lastUpdated: completedAt,
|
|
289
|
-
}))
|
|
290
|
-
|
|
291
|
-
// Publish incremental event
|
|
292
|
-
await this.publishEvent(projectId, 'task.completed', {
|
|
293
|
-
taskId: completedTask.id,
|
|
294
|
-
description: completedTask.description,
|
|
295
|
-
startedAt: completedTask.startedAt,
|
|
296
|
-
completedAt,
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
return completedTask
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Create a TaskHistoryEntry from a completed task
|
|
304
|
-
* Optionally includes structured feedback for the feedback loop (PRJ-272)
|
|
305
|
-
*/
|
|
306
|
-
private createTaskHistoryEntry(
|
|
307
|
-
task: CurrentTask,
|
|
308
|
-
completedAt: string,
|
|
309
|
-
feedback?: TaskFeedback
|
|
310
|
-
): TaskHistoryEntry {
|
|
311
|
-
// Extended task properties (may be present in storage but not in schema)
|
|
312
|
-
const taskAny = task as any
|
|
313
|
-
|
|
314
|
-
// Extract subtask summaries (only completed subtasks with summaries)
|
|
315
|
-
const subtaskSummaries = (task.subtasks || [])
|
|
316
|
-
.filter((st) => st.status === 'completed' && st.summary)
|
|
317
|
-
.map((st) => st.summary!)
|
|
318
|
-
|
|
319
|
-
// Calculate outcome description from subtask summaries
|
|
320
|
-
const outcome =
|
|
321
|
-
subtaskSummaries.length > 0
|
|
322
|
-
? subtaskSummaries.map((s) => s.title).join(', ')
|
|
323
|
-
: 'Task completed'
|
|
324
|
-
|
|
325
|
-
const entry: TaskHistoryEntry = {
|
|
326
|
-
taskId: task.id,
|
|
327
|
-
title: taskAny.parentDescription || task.description,
|
|
328
|
-
classification: taskAny.type || 'improvement',
|
|
329
|
-
startedAt: task.startedAt,
|
|
330
|
-
completedAt,
|
|
331
|
-
subtaskCount: task.subtasks?.length || 0,
|
|
332
|
-
subtaskSummaries,
|
|
333
|
-
outcome,
|
|
334
|
-
branchName: taskAny.branch || 'unknown',
|
|
335
|
-
linearId: task.linearId,
|
|
336
|
-
linearUuid: task.linearUuid,
|
|
337
|
-
prUrl: taskAny.prUrl,
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Attach feedback if provided (PRJ-272)
|
|
341
|
-
if (feedback) {
|
|
342
|
-
entry.feedback = feedback
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
return entry
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/** Max number of paused tasks (configurable) */
|
|
349
|
-
private maxPausedTasks = 5
|
|
350
|
-
|
|
351
|
-
/** Max number of task history entries (configurable) */
|
|
352
|
-
private maxTaskHistory = 20
|
|
353
|
-
|
|
354
|
-
/** Staleness threshold in days */
|
|
355
|
-
private stalenessThresholdDays = 30
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Pause current task — pushes onto pausedTasks[] array
|
|
359
|
-
*/
|
|
360
|
-
async pauseTask(projectId: string, reason?: string): Promise<PreviousTask | null> {
|
|
361
|
-
const state = await this.read(projectId)
|
|
362
|
-
|
|
363
|
-
if (!state.currentTask) {
|
|
364
|
-
return null
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
this.validateTransition(state, 'pause')
|
|
368
|
-
|
|
369
|
-
const pausedTask: PreviousTask = {
|
|
370
|
-
id: state.currentTask.id,
|
|
371
|
-
description: state.currentTask.description,
|
|
372
|
-
status: 'paused',
|
|
373
|
-
startedAt: state.currentTask.startedAt,
|
|
374
|
-
pausedAt: getTimestamp(),
|
|
375
|
-
pauseReason: reason,
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Get existing paused tasks, migrate from previousTask if needed
|
|
379
|
-
const existingPaused = this.getPausedTasksFromState(state)
|
|
380
|
-
|
|
381
|
-
// Enforce max paused limit
|
|
382
|
-
const pausedTasks = [pausedTask, ...existingPaused].slice(0, this.maxPausedTasks)
|
|
383
|
-
|
|
384
|
-
await this.update(projectId, () => ({
|
|
385
|
-
currentTask: null,
|
|
386
|
-
previousTask: null, // deprecated, keep null for compat
|
|
387
|
-
pausedTasks,
|
|
388
|
-
lastUpdated: getTimestamp(),
|
|
389
|
-
}))
|
|
390
|
-
|
|
391
|
-
// Publish incremental event
|
|
392
|
-
await this.publishEvent(projectId, 'task.paused', {
|
|
393
|
-
taskId: pausedTask.id,
|
|
394
|
-
description: pausedTask.description,
|
|
395
|
-
pausedAt: pausedTask.pausedAt,
|
|
396
|
-
reason,
|
|
397
|
-
pausedCount: pausedTasks.length,
|
|
398
|
-
})
|
|
399
|
-
|
|
400
|
-
return pausedTask
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Resume most recent paused task (or by ID)
|
|
405
|
-
*/
|
|
406
|
-
async resumeTask(projectId: string, taskId?: string): Promise<CurrentTask | null> {
|
|
407
|
-
const state = await this.read(projectId)
|
|
408
|
-
|
|
409
|
-
// Migrate from previousTask if pausedTasks is empty
|
|
410
|
-
const pausedTasks = this.getPausedTasksFromState(state)
|
|
411
|
-
|
|
412
|
-
if (pausedTasks.length === 0) {
|
|
413
|
-
return null
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
this.validateTransition(state, 'resume')
|
|
417
|
-
|
|
418
|
-
// Find target task: by ID or most recent (first in array)
|
|
419
|
-
let targetIndex = 0
|
|
420
|
-
if (taskId) {
|
|
421
|
-
targetIndex = pausedTasks.findIndex((t) => t.id === taskId)
|
|
422
|
-
if (targetIndex === -1) return null
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const target = pausedTasks[targetIndex]
|
|
426
|
-
const remaining = pausedTasks.filter((_, i) => i !== targetIndex)
|
|
427
|
-
|
|
428
|
-
const currentTask: CurrentTask = {
|
|
429
|
-
id: target.id,
|
|
430
|
-
description: target.description,
|
|
431
|
-
startedAt: getTimestamp(),
|
|
432
|
-
sessionId: generateUUID(),
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
await this.update(projectId, () => ({
|
|
436
|
-
currentTask,
|
|
437
|
-
previousTask: null, // deprecated, keep null
|
|
438
|
-
pausedTasks: remaining,
|
|
439
|
-
lastUpdated: getTimestamp(),
|
|
440
|
-
}))
|
|
441
|
-
|
|
442
|
-
// Publish incremental event
|
|
443
|
-
await this.publishEvent(projectId, 'task.resumed', {
|
|
444
|
-
taskId: currentTask.id,
|
|
445
|
-
description: currentTask.description,
|
|
446
|
-
resumedAt: currentTask.startedAt,
|
|
447
|
-
remainingPaused: remaining.length,
|
|
448
|
-
})
|
|
449
|
-
|
|
450
|
-
return currentTask
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* Get paused tasks from state, migrating from legacy previousTask if needed
|
|
455
|
-
*/
|
|
456
|
-
private getPausedTasksFromState(state: StateJson): PreviousTask[] {
|
|
457
|
-
const paused = state.pausedTasks || []
|
|
458
|
-
|
|
459
|
-
// Migrate legacy previousTask into array if present
|
|
460
|
-
if (state.previousTask && state.previousTask.status === 'paused') {
|
|
461
|
-
const alreadyInArray = paused.some((t) => t.id === state.previousTask!.id)
|
|
462
|
-
if (!alreadyInArray) {
|
|
463
|
-
return [state.previousTask, ...paused]
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
return paused
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
/**
|
|
471
|
-
* Get task history from state with backward compatibility
|
|
472
|
-
* Ensures taskHistory is always an array (never undefined)
|
|
473
|
-
*/
|
|
474
|
-
private getTaskHistoryFromState(state: StateJson): TaskHistoryEntry[] {
|
|
475
|
-
return state.taskHistory || []
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
/**
|
|
479
|
-
* Get stale paused tasks (older than threshold)
|
|
480
|
-
*/
|
|
481
|
-
async getStalePausedTasks(projectId: string): Promise<PreviousTask[]> {
|
|
482
|
-
const state = await this.read(projectId)
|
|
483
|
-
const pausedTasks = this.getPausedTasksFromState(state)
|
|
484
|
-
const threshold = Date.now() - this.stalenessThresholdDays * 24 * 60 * 60 * 1000
|
|
485
|
-
|
|
486
|
-
return pausedTasks.filter((t) => new Date(t.pausedAt).getTime() < threshold)
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* Archive stale paused tasks (PRJ-267).
|
|
491
|
-
* Persists to archive table before removing from active storage.
|
|
492
|
-
* Returns archived tasks.
|
|
493
|
-
*/
|
|
494
|
-
async archiveStalePausedTasks(projectId: string): Promise<PreviousTask[]> {
|
|
495
|
-
const state = await this.read(projectId)
|
|
496
|
-
const pausedTasks = this.getPausedTasksFromState(state)
|
|
497
|
-
const threshold = Date.now() - this.stalenessThresholdDays * 24 * 60 * 60 * 1000
|
|
498
|
-
|
|
499
|
-
const stale = pausedTasks.filter((t) => new Date(t.pausedAt).getTime() < threshold)
|
|
500
|
-
const fresh = pausedTasks.filter((t) => new Date(t.pausedAt).getTime() >= threshold)
|
|
501
|
-
|
|
502
|
-
if (stale.length === 0) return []
|
|
503
|
-
|
|
504
|
-
// Persist to archive table before removal
|
|
505
|
-
archiveStorage.archiveMany(
|
|
506
|
-
projectId,
|
|
507
|
-
stale.map((task) => ({
|
|
508
|
-
entityType: 'paused_task' as const,
|
|
509
|
-
entityId: task.id,
|
|
510
|
-
entityData: task,
|
|
511
|
-
summary: task.description,
|
|
512
|
-
reason: 'staleness',
|
|
513
|
-
}))
|
|
514
|
-
)
|
|
515
|
-
|
|
516
|
-
await this.update(projectId, (s) => ({
|
|
517
|
-
...s,
|
|
518
|
-
pausedTasks: fresh,
|
|
519
|
-
previousTask: null,
|
|
520
|
-
lastUpdated: getTimestamp(),
|
|
521
|
-
}))
|
|
522
|
-
|
|
523
|
-
for (const task of stale) {
|
|
524
|
-
await this.publishEvent(projectId, 'task.archived', {
|
|
525
|
-
taskId: task.id,
|
|
526
|
-
description: task.description,
|
|
527
|
-
pausedAt: task.pausedAt,
|
|
528
|
-
reason: 'staleness',
|
|
529
|
-
})
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
return stale
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
/**
|
|
536
|
-
* Clear all task state
|
|
537
|
-
*/
|
|
538
|
-
async clearTask(projectId: string): Promise<void> {
|
|
539
|
-
await this.update(projectId, () => ({
|
|
540
|
-
currentTask: null,
|
|
541
|
-
previousTask: null,
|
|
542
|
-
pausedTasks: [],
|
|
543
|
-
lastUpdated: getTimestamp(),
|
|
544
|
-
}))
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
/**
|
|
548
|
-
* Check if there's an active or paused task
|
|
549
|
-
*/
|
|
550
|
-
async hasTask(projectId: string): Promise<boolean> {
|
|
551
|
-
const state = await this.read(projectId)
|
|
552
|
-
const paused = this.getPausedTasksFromState(state)
|
|
553
|
-
return state.currentTask !== null || paused.length > 0
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
/**
|
|
557
|
-
* Get most recently paused task
|
|
558
|
-
*/
|
|
559
|
-
async getPausedTask(projectId: string): Promise<PreviousTask | null> {
|
|
560
|
-
const state = await this.read(projectId)
|
|
561
|
-
const paused = this.getPausedTasksFromState(state)
|
|
562
|
-
return paused[0] || null
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
/**
|
|
566
|
-
* Get all paused tasks
|
|
567
|
-
*/
|
|
568
|
-
async getAllPausedTasks(projectId: string): Promise<PreviousTask[]> {
|
|
569
|
-
const state = await this.read(projectId)
|
|
570
|
-
return this.getPausedTasksFromState(state)
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
/**
|
|
574
|
-
* Get full task history (completed tasks)
|
|
575
|
-
*/
|
|
576
|
-
async getTaskHistory(projectId: string): Promise<TaskHistoryEntry[]> {
|
|
577
|
-
const state = await this.read(projectId)
|
|
578
|
-
return this.getTaskHistoryFromState(state)
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
/**
|
|
582
|
-
* Get most recent task from history
|
|
583
|
-
*/
|
|
584
|
-
async getMostRecentTask(projectId: string): Promise<TaskHistoryEntry | null> {
|
|
585
|
-
const state = await this.read(projectId)
|
|
586
|
-
const history = this.getTaskHistoryFromState(state)
|
|
587
|
-
return history[0] || null
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/**
|
|
591
|
-
* Get task history filtered by classification
|
|
592
|
-
*/
|
|
593
|
-
async getTaskHistoryByType(
|
|
594
|
-
projectId: string,
|
|
595
|
-
classification: TaskHistoryEntry['classification']
|
|
596
|
-
): Promise<TaskHistoryEntry[]> {
|
|
597
|
-
const state = await this.read(projectId)
|
|
598
|
-
const history = this.getTaskHistoryFromState(state)
|
|
599
|
-
return history.filter((t) => t.classification === classification)
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
/**
|
|
603
|
-
* Aggregate feedback from all task history entries (PRJ-272)
|
|
604
|
-
* Used by sync to feed task discoveries back into analysis and agent generation.
|
|
605
|
-
* Returns consolidated patterns, stack confirmations, issues, and agent accuracy.
|
|
606
|
-
*/
|
|
607
|
-
async getAggregatedFeedback(projectId: string): Promise<{
|
|
608
|
-
stackConfirmed: string[]
|
|
609
|
-
patternsDiscovered: string[]
|
|
610
|
-
agentAccuracy: Array<{ agent: string; rating: string; note?: string }>
|
|
611
|
-
issuesEncountered: string[]
|
|
612
|
-
knownGotchas: string[]
|
|
613
|
-
}> {
|
|
614
|
-
const history = await this.getTaskHistory(projectId)
|
|
615
|
-
const entriesWithFeedback = history.filter((h) => h.feedback)
|
|
616
|
-
|
|
617
|
-
const stackConfirmed: string[] = []
|
|
618
|
-
const patternsDiscovered: string[] = []
|
|
619
|
-
const agentAccuracy: Array<{ agent: string; rating: string; note?: string }> = []
|
|
620
|
-
const allIssues: string[] = []
|
|
621
|
-
|
|
622
|
-
for (const entry of entriesWithFeedback) {
|
|
623
|
-
const fb = entry.feedback!
|
|
624
|
-
if (fb.stackConfirmed) stackConfirmed.push(...fb.stackConfirmed)
|
|
625
|
-
if (fb.patternsDiscovered) patternsDiscovered.push(...fb.patternsDiscovered)
|
|
626
|
-
if (fb.agentAccuracy) agentAccuracy.push(...fb.agentAccuracy)
|
|
627
|
-
if (fb.issuesEncountered) allIssues.push(...fb.issuesEncountered)
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// Deduplicate patterns and stack confirmations
|
|
631
|
-
const uniqueStack = [...new Set(stackConfirmed)]
|
|
632
|
-
const uniquePatterns = [...new Set(patternsDiscovered)]
|
|
633
|
-
|
|
634
|
-
// Promote recurring issues (2+) to known gotchas
|
|
635
|
-
const issueCounts = new Map<string, number>()
|
|
636
|
-
for (const issue of allIssues) {
|
|
637
|
-
issueCounts.set(issue, (issueCounts.get(issue) || 0) + 1)
|
|
638
|
-
}
|
|
639
|
-
const knownGotchas = [...issueCounts.entries()]
|
|
640
|
-
.filter(([_, count]) => count >= 2)
|
|
641
|
-
.map(([issue]) => issue)
|
|
642
|
-
|
|
643
|
-
return {
|
|
644
|
-
stackConfirmed: uniqueStack,
|
|
645
|
-
patternsDiscovered: uniquePatterns,
|
|
646
|
-
agentAccuracy,
|
|
647
|
-
issuesEncountered: [...new Set(allIssues)],
|
|
648
|
-
knownGotchas,
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
// =========== Subtask Methods ===========
|
|
653
|
-
|
|
654
|
-
/**
|
|
655
|
-
* Create subtasks when fragmenting a task
|
|
656
|
-
* Sets first subtask to in_progress
|
|
657
|
-
*/
|
|
658
|
-
async createSubtasks(
|
|
659
|
-
projectId: string,
|
|
660
|
-
subtasks: Omit<Subtask, 'status' | 'startedAt' | 'completedAt' | 'output' | 'summary'>[]
|
|
661
|
-
): Promise<void> {
|
|
662
|
-
const state = await this.read(projectId)
|
|
663
|
-
if (!state.currentTask) return
|
|
664
|
-
|
|
665
|
-
// Convert input to full Subtask objects
|
|
666
|
-
const fullSubtasks: Subtask[] = subtasks.map((s, index) => ({
|
|
667
|
-
...s,
|
|
668
|
-
status: index === 0 ? 'in_progress' : 'pending',
|
|
669
|
-
startedAt: index === 0 ? getTimestamp() : undefined,
|
|
670
|
-
dependsOn: s.dependsOn || [],
|
|
671
|
-
}))
|
|
672
|
-
|
|
673
|
-
await this.update(projectId, (current) => ({
|
|
674
|
-
...current,
|
|
675
|
-
currentTask: {
|
|
676
|
-
...current.currentTask!,
|
|
677
|
-
subtasks: fullSubtasks,
|
|
678
|
-
currentSubtaskIndex: 0,
|
|
679
|
-
subtaskProgress: {
|
|
680
|
-
completed: 0,
|
|
681
|
-
total: fullSubtasks.length,
|
|
682
|
-
percentage: 0,
|
|
683
|
-
},
|
|
684
|
-
},
|
|
685
|
-
lastUpdated: getTimestamp(),
|
|
686
|
-
}))
|
|
687
|
-
|
|
688
|
-
// Publish event
|
|
689
|
-
await this.publishEvent(projectId, 'subtasks.created', {
|
|
690
|
-
taskId: state.currentTask.id,
|
|
691
|
-
subtaskCount: fullSubtasks.length,
|
|
692
|
-
subtasks: fullSubtasks.map((s) => ({
|
|
693
|
-
id: s.id,
|
|
694
|
-
description: s.description,
|
|
695
|
-
domain: s.domain,
|
|
696
|
-
})),
|
|
697
|
-
})
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
/**
|
|
701
|
-
* Complete current subtask and advance to next.
|
|
702
|
-
* Requires output and summary for mandatory handoff (PRJ-262).
|
|
703
|
-
* Returns the next subtask (or null if all complete).
|
|
704
|
-
*/
|
|
705
|
-
async completeSubtask(
|
|
706
|
-
projectId: string,
|
|
707
|
-
completionData: SubtaskCompletionData
|
|
708
|
-
): Promise<Subtask | null> {
|
|
709
|
-
// Validate handoff data with Zod before persisting
|
|
710
|
-
const validation = SubtaskCompletionDataSchema.safeParse(completionData)
|
|
711
|
-
if (!validation.success) {
|
|
712
|
-
const errors = validation.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`)
|
|
713
|
-
throw new Error(`Subtask completion requires handoff data:\n${errors.join('\n')}`)
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
const { output, summary } = validation.data
|
|
717
|
-
|
|
718
|
-
const state = await this.read(projectId)
|
|
719
|
-
if (!state.currentTask?.subtasks) return null
|
|
720
|
-
|
|
721
|
-
const currentIndex = state.currentTask.currentSubtaskIndex || 0
|
|
722
|
-
const current = state.currentTask.subtasks[currentIndex]
|
|
723
|
-
if (!current) return null
|
|
724
|
-
|
|
725
|
-
// Mark current as completed with mandatory handoff
|
|
726
|
-
const updatedSubtasks = [...state.currentTask.subtasks]
|
|
727
|
-
updatedSubtasks[currentIndex] = {
|
|
728
|
-
...current,
|
|
729
|
-
status: 'completed',
|
|
730
|
-
completedAt: getTimestamp(),
|
|
731
|
-
output,
|
|
732
|
-
summary,
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// Calculate new progress
|
|
736
|
-
const completed = updatedSubtasks.filter((s) => s.status === 'completed').length
|
|
737
|
-
const total = updatedSubtasks.length
|
|
738
|
-
const percentage = Math.round((completed / total) * 100)
|
|
739
|
-
|
|
740
|
-
// Advance to next subtask if available
|
|
741
|
-
const nextIndex = currentIndex + 1
|
|
742
|
-
if (nextIndex < updatedSubtasks.length) {
|
|
743
|
-
updatedSubtasks[nextIndex] = {
|
|
744
|
-
...updatedSubtasks[nextIndex],
|
|
745
|
-
status: 'in_progress',
|
|
746
|
-
startedAt: getTimestamp(),
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
await this.update(projectId, (s) => ({
|
|
751
|
-
...s,
|
|
752
|
-
currentTask: {
|
|
753
|
-
...s.currentTask!,
|
|
754
|
-
subtasks: updatedSubtasks,
|
|
755
|
-
currentSubtaskIndex: nextIndex < total ? nextIndex : currentIndex,
|
|
756
|
-
subtaskProgress: { completed, total, percentage },
|
|
757
|
-
},
|
|
758
|
-
lastUpdated: getTimestamp(),
|
|
759
|
-
}))
|
|
760
|
-
|
|
761
|
-
// Publish event with handoff data
|
|
762
|
-
await this.publishEvent(projectId, 'subtask.completed', {
|
|
763
|
-
taskId: state.currentTask.id,
|
|
764
|
-
subtaskId: current.id,
|
|
765
|
-
description: current.description,
|
|
766
|
-
output,
|
|
767
|
-
handoff: summary.outputForNextAgent,
|
|
768
|
-
filesChanged: summary.filesChanged.length,
|
|
769
|
-
progress: { completed, total, percentage },
|
|
770
|
-
})
|
|
771
|
-
|
|
772
|
-
// Return next subtask or null
|
|
773
|
-
return nextIndex < total ? updatedSubtasks[nextIndex] : null
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
/**
|
|
777
|
-
* Get current subtask
|
|
778
|
-
*/
|
|
779
|
-
async getCurrentSubtask(projectId: string): Promise<Subtask | null> {
|
|
780
|
-
const state = await this.read(projectId)
|
|
781
|
-
if (!state.currentTask?.subtasks) return null
|
|
782
|
-
const index = state.currentTask.currentSubtaskIndex || 0
|
|
783
|
-
return state.currentTask.subtasks[index] || null
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
/**
|
|
787
|
-
* Get next subtask (after current)
|
|
788
|
-
*/
|
|
789
|
-
async getNextSubtask(projectId: string): Promise<Subtask | null> {
|
|
790
|
-
const state = await this.read(projectId)
|
|
791
|
-
if (!state.currentTask?.subtasks) return null
|
|
792
|
-
const nextIndex = (state.currentTask.currentSubtaskIndex || 0) + 1
|
|
793
|
-
return state.currentTask.subtasks[nextIndex] || null
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
/**
|
|
797
|
-
* Get previous subtask (before current)
|
|
798
|
-
*/
|
|
799
|
-
async getPreviousSubtask(projectId: string): Promise<Subtask | null> {
|
|
800
|
-
const state = await this.read(projectId)
|
|
801
|
-
if (!state.currentTask?.subtasks) return null
|
|
802
|
-
const prevIndex = (state.currentTask.currentSubtaskIndex || 0) - 1
|
|
803
|
-
if (prevIndex < 0) return null
|
|
804
|
-
return state.currentTask.subtasks[prevIndex] || null
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
/**
|
|
808
|
-
* Get handoff from the most recently completed subtask (PRJ-262).
|
|
809
|
-
* Returns the summary.outputForNextAgent and related context,
|
|
810
|
-
* or null if no previous subtask or no handoff data.
|
|
811
|
-
*/
|
|
812
|
-
async getPreviousHandoff(projectId: string): Promise<{
|
|
813
|
-
fromSubtask: string
|
|
814
|
-
outputForNextAgent: string
|
|
815
|
-
filesChanged: Array<{ path: string; action: string }>
|
|
816
|
-
whatWasDone: string[]
|
|
817
|
-
} | null> {
|
|
818
|
-
const prev = await this.getPreviousSubtask(projectId)
|
|
819
|
-
if (!prev?.summary?.outputForNextAgent) return null
|
|
820
|
-
return {
|
|
821
|
-
fromSubtask: prev.description,
|
|
822
|
-
outputForNextAgent: prev.summary.outputForNextAgent,
|
|
823
|
-
filesChanged: prev.summary.filesChanged,
|
|
824
|
-
whatWasDone: prev.summary.whatWasDone,
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
/**
|
|
829
|
-
* Get all subtasks
|
|
830
|
-
*/
|
|
831
|
-
async getSubtasks(projectId: string): Promise<Subtask[]> {
|
|
832
|
-
const state = await this.read(projectId)
|
|
833
|
-
return state.currentTask?.subtasks || []
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
/**
|
|
837
|
-
* Get subtask progress
|
|
838
|
-
*/
|
|
839
|
-
async getSubtaskProgress(
|
|
840
|
-
projectId: string
|
|
841
|
-
): Promise<{ completed: number; total: number; percentage: number } | null> {
|
|
842
|
-
const state = await this.read(projectId)
|
|
843
|
-
return state.currentTask?.subtaskProgress || null
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
/**
|
|
847
|
-
* Check if task has subtasks
|
|
848
|
-
*/
|
|
849
|
-
async hasSubtasks(projectId: string): Promise<boolean> {
|
|
850
|
-
const state = await this.read(projectId)
|
|
851
|
-
return (state.currentTask?.subtasks?.length || 0) > 0
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
/**
|
|
855
|
-
* Check if all subtasks are complete
|
|
856
|
-
*/
|
|
857
|
-
async areAllSubtasksComplete(projectId: string): Promise<boolean> {
|
|
858
|
-
const state = await this.read(projectId)
|
|
859
|
-
if (!state.currentTask?.subtasks) return true
|
|
860
|
-
return state.currentTask.subtasks.every(
|
|
861
|
-
(s) => s.status === 'completed' || s.status === 'failed' || s.status === 'skipped'
|
|
862
|
-
)
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
/**
|
|
866
|
-
* Fail current subtask and advance to next
|
|
867
|
-
* Returns the next subtask (or null if all done/failed)
|
|
868
|
-
*/
|
|
869
|
-
async failSubtask(projectId: string, error: string): Promise<Subtask | null> {
|
|
870
|
-
const state = await this.read(projectId)
|
|
871
|
-
if (!state.currentTask?.subtasks) return null
|
|
872
|
-
|
|
873
|
-
const currentIndex = state.currentTask.currentSubtaskIndex || 0
|
|
874
|
-
const current = state.currentTask.subtasks[currentIndex]
|
|
875
|
-
if (!current) return null
|
|
876
|
-
|
|
877
|
-
const updatedSubtasks = [...state.currentTask.subtasks]
|
|
878
|
-
updatedSubtasks[currentIndex] = {
|
|
879
|
-
...current,
|
|
880
|
-
status: 'failed',
|
|
881
|
-
completedAt: getTimestamp(),
|
|
882
|
-
output: `Failed: ${error}`,
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
// Advance to next subtask if available
|
|
886
|
-
const nextIndex = currentIndex + 1
|
|
887
|
-
const total = updatedSubtasks.length
|
|
888
|
-
if (nextIndex < total) {
|
|
889
|
-
updatedSubtasks[nextIndex] = {
|
|
890
|
-
...updatedSubtasks[nextIndex],
|
|
891
|
-
status: 'in_progress',
|
|
892
|
-
startedAt: getTimestamp(),
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
// Count completed + failed + skipped as "resolved" for progress
|
|
897
|
-
const resolved = updatedSubtasks.filter(
|
|
898
|
-
(s) => s.status === 'completed' || s.status === 'failed' || s.status === 'skipped'
|
|
899
|
-
).length
|
|
900
|
-
const percentage = Math.round((resolved / total) * 100)
|
|
901
|
-
|
|
902
|
-
await this.update(projectId, (s) => ({
|
|
903
|
-
...s,
|
|
904
|
-
currentTask: {
|
|
905
|
-
...s.currentTask!,
|
|
906
|
-
subtasks: updatedSubtasks,
|
|
907
|
-
currentSubtaskIndex: nextIndex < total ? nextIndex : currentIndex,
|
|
908
|
-
subtaskProgress: { completed: resolved, total, percentage },
|
|
909
|
-
},
|
|
910
|
-
lastUpdated: getTimestamp(),
|
|
911
|
-
}))
|
|
912
|
-
|
|
913
|
-
// Publish event
|
|
914
|
-
await this.publishEvent(projectId, 'subtask.failed', {
|
|
915
|
-
taskId: state.currentTask.id,
|
|
916
|
-
subtaskId: current.id,
|
|
917
|
-
description: current.description,
|
|
918
|
-
error,
|
|
919
|
-
})
|
|
920
|
-
|
|
921
|
-
return nextIndex < total ? updatedSubtasks[nextIndex] : null
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
/**
|
|
925
|
-
* Skip current subtask with reason and advance to next
|
|
926
|
-
* Returns the next subtask (or null if all done)
|
|
927
|
-
*/
|
|
928
|
-
async skipSubtask(projectId: string, reason: string): Promise<Subtask | null> {
|
|
929
|
-
const state = await this.read(projectId)
|
|
930
|
-
if (!state.currentTask?.subtasks) return null
|
|
931
|
-
|
|
932
|
-
const currentIndex = state.currentTask.currentSubtaskIndex || 0
|
|
933
|
-
const current = state.currentTask.subtasks[currentIndex]
|
|
934
|
-
if (!current) return null
|
|
935
|
-
|
|
936
|
-
const updatedSubtasks = [...state.currentTask.subtasks]
|
|
937
|
-
updatedSubtasks[currentIndex] = {
|
|
938
|
-
...current,
|
|
939
|
-
status: 'skipped',
|
|
940
|
-
completedAt: getTimestamp(),
|
|
941
|
-
output: `Skipped: ${reason}`,
|
|
942
|
-
skipReason: reason,
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
// Advance to next subtask if available
|
|
946
|
-
const nextIndex = currentIndex + 1
|
|
947
|
-
const total = updatedSubtasks.length
|
|
948
|
-
if (nextIndex < total) {
|
|
949
|
-
updatedSubtasks[nextIndex] = {
|
|
950
|
-
...updatedSubtasks[nextIndex],
|
|
951
|
-
status: 'in_progress',
|
|
952
|
-
startedAt: getTimestamp(),
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
const resolved = updatedSubtasks.filter(
|
|
957
|
-
(s) => s.status === 'completed' || s.status === 'failed' || s.status === 'skipped'
|
|
958
|
-
).length
|
|
959
|
-
const percentage = Math.round((resolved / total) * 100)
|
|
960
|
-
|
|
961
|
-
await this.update(projectId, (s) => ({
|
|
962
|
-
...s,
|
|
963
|
-
currentTask: {
|
|
964
|
-
...s.currentTask!,
|
|
965
|
-
subtasks: updatedSubtasks,
|
|
966
|
-
currentSubtaskIndex: nextIndex < total ? nextIndex : currentIndex,
|
|
967
|
-
subtaskProgress: { completed: resolved, total, percentage },
|
|
968
|
-
},
|
|
969
|
-
lastUpdated: getTimestamp(),
|
|
970
|
-
}))
|
|
971
|
-
|
|
972
|
-
await this.publishEvent(projectId, 'subtask.skipped', {
|
|
973
|
-
taskId: state.currentTask.id,
|
|
974
|
-
subtaskId: current.id,
|
|
975
|
-
description: current.description,
|
|
976
|
-
reason,
|
|
977
|
-
})
|
|
978
|
-
|
|
979
|
-
return nextIndex < total ? updatedSubtasks[nextIndex] : null
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
/**
|
|
983
|
-
* Block current subtask with reason, allow proceeding to next
|
|
984
|
-
* Returns the next subtask (or null if no more)
|
|
985
|
-
*/
|
|
986
|
-
async blockSubtask(projectId: string, blocker: string): Promise<Subtask | null> {
|
|
987
|
-
const state = await this.read(projectId)
|
|
988
|
-
if (!state.currentTask?.subtasks) return null
|
|
989
|
-
|
|
990
|
-
const currentIndex = state.currentTask.currentSubtaskIndex || 0
|
|
991
|
-
const current = state.currentTask.subtasks[currentIndex]
|
|
992
|
-
if (!current) return null
|
|
993
|
-
|
|
994
|
-
const updatedSubtasks = [...state.currentTask.subtasks]
|
|
995
|
-
updatedSubtasks[currentIndex] = {
|
|
996
|
-
...current,
|
|
997
|
-
status: 'blocked',
|
|
998
|
-
output: `Blocked: ${blocker}`,
|
|
999
|
-
blockReason: blocker,
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
// Advance to next subtask if available (blocked doesn't halt)
|
|
1003
|
-
const nextIndex = currentIndex + 1
|
|
1004
|
-
const total = updatedSubtasks.length
|
|
1005
|
-
if (nextIndex < total) {
|
|
1006
|
-
updatedSubtasks[nextIndex] = {
|
|
1007
|
-
...updatedSubtasks[nextIndex],
|
|
1008
|
-
status: 'in_progress',
|
|
1009
|
-
startedAt: getTimestamp(),
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
await this.update(projectId, (s) => ({
|
|
1014
|
-
...s,
|
|
1015
|
-
currentTask: {
|
|
1016
|
-
...s.currentTask!,
|
|
1017
|
-
subtasks: updatedSubtasks,
|
|
1018
|
-
currentSubtaskIndex: nextIndex < total ? nextIndex : currentIndex,
|
|
1019
|
-
},
|
|
1020
|
-
lastUpdated: getTimestamp(),
|
|
1021
|
-
}))
|
|
1022
|
-
|
|
1023
|
-
await this.publishEvent(projectId, 'subtask.blocked', {
|
|
1024
|
-
taskId: state.currentTask.id,
|
|
1025
|
-
subtaskId: current.id,
|
|
1026
|
-
description: current.description,
|
|
1027
|
-
blocker,
|
|
1028
|
-
})
|
|
1029
|
-
|
|
1030
|
-
return nextIndex < total ? updatedSubtasks[nextIndex] : null
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
export const stateStorage = new StateStorage()
|
|
1035
|
-
export default stateStorage
|