@kynetic-ai/spec 0.1.2 → 0.3.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/README.md +250 -17
- package/dist/acp/client.d.ts +18 -4
- package/dist/acp/client.d.ts.map +1 -1
- package/dist/acp/client.js +44 -26
- package/dist/acp/client.js.map +1 -1
- package/dist/acp/framing.d.ts +2 -2
- package/dist/acp/framing.d.ts.map +1 -1
- package/dist/acp/framing.js +37 -29
- package/dist/acp/framing.js.map +1 -1
- package/dist/acp/index.d.ts +6 -7
- package/dist/acp/index.d.ts.map +1 -1
- package/dist/acp/index.js +3 -3
- package/dist/acp/index.js.map +1 -1
- package/dist/acp/types.d.ts +5 -5
- package/dist/acp/types.d.ts.map +1 -1
- package/dist/acp/types.js +18 -18
- package/dist/acp/types.js.map +1 -1
- package/dist/agents/adapters.d.ts.map +1 -1
- package/dist/agents/adapters.js +24 -13
- package/dist/agents/adapters.js.map +1 -1
- package/dist/agents/index.d.ts +2 -2
- package/dist/agents/index.js +2 -2
- package/dist/agents/spawner.d.ts +4 -4
- package/dist/agents/spawner.d.ts.map +1 -1
- package/dist/agents/spawner.js +6 -6
- package/dist/agents/spawner.js.map +1 -1
- package/dist/cli/batch-context.d.ts +43 -0
- package/dist/cli/batch-context.d.ts.map +1 -0
- package/dist/cli/batch-context.js +93 -0
- package/dist/cli/batch-context.js.map +1 -0
- package/dist/cli/batch-exec.d.ts +116 -0
- package/dist/cli/batch-exec.d.ts.map +1 -0
- package/dist/cli/batch-exec.js +694 -0
- package/dist/cli/batch-exec.js.map +1 -0
- package/dist/cli/batch.d.ts +4 -2
- package/dist/cli/batch.d.ts.map +1 -1
- package/dist/cli/batch.js +15 -14
- package/dist/cli/batch.js.map +1 -1
- package/dist/cli/command-annotations.d.ts +23 -0
- package/dist/cli/command-annotations.d.ts.map +1 -0
- package/dist/cli/command-annotations.js +27 -0
- package/dist/cli/command-annotations.js.map +1 -0
- package/dist/cli/commands/agents.d.ts +46 -0
- package/dist/cli/commands/agents.d.ts.map +1 -0
- package/dist/cli/commands/agents.js +377 -0
- package/dist/cli/commands/agents.js.map +1 -0
- package/dist/cli/commands/batch.d.ts +20 -0
- package/dist/cli/commands/batch.d.ts.map +1 -0
- package/dist/cli/commands/batch.js +214 -0
- package/dist/cli/commands/batch.js.map +1 -0
- package/dist/cli/commands/clone-for-testing.d.ts +1 -1
- package/dist/cli/commands/clone-for-testing.d.ts.map +1 -1
- package/dist/cli/commands/clone-for-testing.js +37 -47
- package/dist/cli/commands/clone-for-testing.js.map +1 -1
- package/dist/cli/commands/derive.d.ts +1 -1
- package/dist/cli/commands/derive.d.ts.map +1 -1
- package/dist/cli/commands/derive.js +140 -88
- package/dist/cli/commands/derive.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +11 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +152 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/export.d.ts +12 -0
- package/dist/cli/commands/export.d.ts.map +1 -0
- package/dist/cli/commands/export.js +134 -0
- package/dist/cli/commands/export.js.map +1 -0
- package/dist/cli/commands/help.d.ts +1 -1
- package/dist/cli/commands/help.d.ts.map +1 -1
- package/dist/cli/commands/help.js +163 -37
- package/dist/cli/commands/help.js.map +1 -1
- package/dist/cli/commands/inbox.d.ts +1 -1
- package/dist/cli/commands/inbox.d.ts.map +1 -1
- package/dist/cli/commands/inbox.js +178 -56
- package/dist/cli/commands/inbox.js.map +1 -1
- package/dist/cli/commands/index.d.ts +31 -19
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +31 -19
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts +5 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +108 -57
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/item.d.ts +1 -1
- package/dist/cli/commands/item.d.ts.map +1 -1
- package/dist/cli/commands/item.js +557 -274
- package/dist/cli/commands/item.js.map +1 -1
- package/dist/cli/commands/link.d.ts +1 -1
- package/dist/cli/commands/link.d.ts.map +1 -1
- package/dist/cli/commands/link.js +55 -46
- package/dist/cli/commands/link.js.map +1 -1
- package/dist/cli/commands/log.d.ts +1 -1
- package/dist/cli/commands/log.d.ts.map +1 -1
- package/dist/cli/commands/log.js +57 -51
- package/dist/cli/commands/log.js.map +1 -1
- package/dist/cli/commands/merge-driver.d.ts +19 -0
- package/dist/cli/commands/merge-driver.d.ts.map +1 -0
- package/dist/cli/commands/merge-driver.js +398 -0
- package/dist/cli/commands/merge-driver.js.map +1 -0
- package/dist/cli/commands/meta.d.ts +1 -1
- package/dist/cli/commands/meta.d.ts.map +1 -1
- package/dist/cli/commands/meta.js +533 -399
- package/dist/cli/commands/meta.js.map +1 -1
- package/dist/cli/commands/module.d.ts +1 -1
- package/dist/cli/commands/module.d.ts.map +1 -1
- package/dist/cli/commands/module.js +30 -25
- package/dist/cli/commands/module.js.map +1 -1
- package/dist/cli/commands/plan-import.d.ts +11 -0
- package/dist/cli/commands/plan-import.d.ts.map +1 -0
- package/dist/cli/commands/plan-import.js +516 -0
- package/dist/cli/commands/plan-import.js.map +1 -0
- package/dist/cli/commands/plan.d.ts +10 -0
- package/dist/cli/commands/plan.d.ts.map +1 -0
- package/dist/cli/commands/plan.js +421 -0
- package/dist/cli/commands/plan.js.map +1 -0
- package/dist/cli/commands/ralph.d.ts +1 -1
- package/dist/cli/commands/ralph.d.ts.map +1 -1
- package/dist/cli/commands/ralph.js +1097 -169
- package/dist/cli/commands/ralph.js.map +1 -1
- package/dist/cli/commands/refs.d.ts +13 -0
- package/dist/cli/commands/refs.d.ts.map +1 -0
- package/dist/cli/commands/refs.js +283 -0
- package/dist/cli/commands/refs.js.map +1 -0
- package/dist/cli/commands/search.d.ts +1 -1
- package/dist/cli/commands/search.d.ts.map +1 -1
- package/dist/cli/commands/search.js +199 -37
- package/dist/cli/commands/search.js.map +1 -1
- package/dist/cli/commands/serve.d.ts +10 -0
- package/dist/cli/commands/serve.d.ts.map +1 -0
- package/dist/cli/commands/serve.js +491 -0
- package/dist/cli/commands/serve.js.map +1 -0
- package/dist/cli/commands/session.d.ts +25 -6
- package/dist/cli/commands/session.d.ts.map +1 -1
- package/dist/cli/commands/session.js +811 -127
- package/dist/cli/commands/session.js.map +1 -1
- package/dist/cli/commands/setup-seeding.d.ts +81 -0
- package/dist/cli/commands/setup-seeding.d.ts.map +1 -0
- package/dist/cli/commands/setup-seeding.js +292 -0
- package/dist/cli/commands/setup-seeding.js.map +1 -0
- package/dist/cli/commands/setup.d.ts +77 -3
- package/dist/cli/commands/setup.d.ts.map +1 -1
- package/dist/cli/commands/setup.js +1233 -274
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/shadow.d.ts +1 -1
- package/dist/cli/commands/shadow.d.ts.map +1 -1
- package/dist/cli/commands/shadow.js +70 -50
- package/dist/cli/commands/shadow.js.map +1 -1
- package/dist/cli/commands/skill-crud.d.ts +58 -0
- package/dist/cli/commands/skill-crud.d.ts.map +1 -0
- package/dist/cli/commands/skill-crud.js +753 -0
- package/dist/cli/commands/skill-crud.js.map +1 -0
- package/dist/cli/commands/skill-diff.d.ts +27 -0
- package/dist/cli/commands/skill-diff.d.ts.map +1 -0
- package/dist/cli/commands/skill-diff.js +840 -0
- package/dist/cli/commands/skill-diff.js.map +1 -0
- package/dist/cli/commands/skill-install.d.ts +53 -0
- package/dist/cli/commands/skill-install.d.ts.map +1 -0
- package/dist/cli/commands/skill-install.js +452 -0
- package/dist/cli/commands/skill-install.js.map +1 -0
- package/dist/cli/commands/skill.d.ts +20 -0
- package/dist/cli/commands/skill.d.ts.map +1 -0
- package/dist/cli/commands/skill.js +36 -0
- package/dist/cli/commands/skill.js.map +1 -0
- package/dist/cli/commands/task.d.ts +1 -1
- package/dist/cli/commands/task.d.ts.map +1 -1
- package/dist/cli/commands/task.js +569 -346
- package/dist/cli/commands/task.js.map +1 -1
- package/dist/cli/commands/tasks.d.ts +26 -1
- package/dist/cli/commands/tasks.d.ts.map +1 -1
- package/dist/cli/commands/tasks.js +227 -122
- package/dist/cli/commands/tasks.js.map +1 -1
- package/dist/cli/commands/trait.d.ts +1 -1
- package/dist/cli/commands/trait.d.ts.map +1 -1
- package/dist/cli/commands/trait.js +166 -101
- package/dist/cli/commands/trait.js.map +1 -1
- package/dist/cli/commands/triage.d.ts +7 -0
- package/dist/cli/commands/triage.d.ts.map +1 -0
- package/dist/cli/commands/triage.js +569 -0
- package/dist/cli/commands/triage.js.map +1 -0
- package/dist/cli/commands/util.d.ts +7 -0
- package/dist/cli/commands/util.d.ts.map +1 -0
- package/dist/cli/commands/util.js +30 -0
- package/dist/cli/commands/util.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +1 -1
- package/dist/cli/commands/validate.d.ts.map +1 -1
- package/dist/cli/commands/validate.js +264 -83
- package/dist/cli/commands/validate.js.map +1 -1
- package/dist/cli/commands/workflow.d.ts +16 -0
- package/dist/cli/commands/workflow.d.ts.map +1 -0
- package/dist/cli/commands/workflow.js +851 -0
- package/dist/cli/commands/workflow.js.map +1 -0
- package/dist/cli/exit-codes.d.ts +7 -0
- package/dist/cli/exit-codes.d.ts.map +1 -1
- package/dist/cli/exit-codes.js +26 -18
- package/dist/cli/exit-codes.js.map +1 -1
- package/dist/cli/help/content.d.ts.map +1 -1
- package/dist/cli/help/content.js +86 -71
- package/dist/cli/help/content.js.map +1 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +131 -19
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/introspection.d.ts +6 -2
- package/dist/cli/introspection.d.ts.map +1 -1
- package/dist/cli/introspection.js +11 -8
- package/dist/cli/introspection.js.map +1 -1
- package/dist/cli/output.d.ts +64 -4
- package/dist/cli/output.d.ts.map +1 -1
- package/dist/cli/output.js +235 -85
- package/dist/cli/output.js.map +1 -1
- package/dist/cli/parse-utils.d.ts +21 -0
- package/dist/cli/parse-utils.d.ts.map +1 -0
- package/dist/cli/parse-utils.js +32 -0
- package/dist/cli/parse-utils.js.map +1 -0
- package/dist/cli/pid-utils.d.ts +72 -0
- package/dist/cli/pid-utils.d.ts.map +1 -0
- package/dist/cli/pid-utils.js +174 -0
- package/dist/cli/pid-utils.js.map +1 -0
- package/dist/cli/suggest.d.ts.map +1 -1
- package/dist/cli/suggest.js +1 -2
- package/dist/cli/suggest.js.map +1 -1
- package/dist/cli/validators.d.ts +43 -0
- package/dist/cli/validators.d.ts.map +1 -0
- package/dist/cli/validators.js +84 -0
- package/dist/cli/validators.js.map +1 -0
- package/dist/daemon/index.ts +52 -0
- package/dist/daemon/middleware/project-context.ts +126 -0
- package/dist/daemon/pid.ts +179 -0
- package/dist/daemon/project-context.ts +343 -0
- package/dist/daemon/routes/inbox.ts +164 -0
- package/dist/daemon/routes/items.ts +322 -0
- package/dist/daemon/routes/meta.ts +118 -0
- package/dist/daemon/routes/projects.ts +162 -0
- package/dist/daemon/routes/tasks.ts +327 -0
- package/dist/daemon/routes/triage.ts +468 -0
- package/dist/daemon/routes/validation.ts +248 -0
- package/dist/daemon/server.ts +408 -0
- package/dist/daemon/watcher.ts +195 -0
- package/dist/daemon/websocket/handler.ts +138 -0
- package/dist/daemon/websocket/heartbeat.ts +71 -0
- package/dist/daemon/websocket/pubsub.ts +125 -0
- package/dist/daemon/websocket/types.ts +66 -0
- package/dist/export/html.d.ts +19 -0
- package/dist/export/html.d.ts.map +1 -0
- package/dist/export/html.js +239 -0
- package/dist/export/html.js.map +1 -0
- package/dist/export/index.d.ts +10 -0
- package/dist/export/index.d.ts.map +1 -0
- package/dist/export/index.js +10 -0
- package/dist/export/index.js.map +1 -0
- package/dist/export/json.d.ts +24 -0
- package/dist/export/json.d.ts.map +1 -0
- package/dist/export/json.js +198 -0
- package/dist/export/json.js.map +1 -0
- package/dist/export/triage.d.ts +51 -0
- package/dist/export/triage.d.ts.map +1 -0
- package/dist/export/triage.js +83 -0
- package/dist/export/triage.js.map +1 -0
- package/dist/export/types.d.ts +122 -0
- package/dist/export/types.d.ts.map +1 -0
- package/dist/export/types.js +9 -0
- package/dist/export/types.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/lib/claude-plugin-registry.d.ts +66 -0
- package/dist/lib/claude-plugin-registry.d.ts.map +1 -0
- package/dist/lib/claude-plugin-registry.js +318 -0
- package/dist/lib/claude-plugin-registry.js.map +1 -0
- package/dist/merge/arrays.d.ts +87 -0
- package/dist/merge/arrays.d.ts.map +1 -0
- package/dist/merge/arrays.js +164 -0
- package/dist/merge/arrays.js.map +1 -0
- package/dist/merge/file-type.d.ts +32 -0
- package/dist/merge/file-type.d.ts.map +1 -0
- package/dist/merge/file-type.js +70 -0
- package/dist/merge/file-type.js.map +1 -0
- package/dist/merge/index.d.ts +14 -0
- package/dist/merge/index.d.ts.map +1 -0
- package/dist/merge/index.js +11 -0
- package/dist/merge/index.js.map +1 -0
- package/dist/merge/objects.d.ts +46 -0
- package/dist/merge/objects.d.ts.map +1 -0
- package/dist/merge/objects.js +193 -0
- package/dist/merge/objects.js.map +1 -0
- package/dist/merge/parse.d.ts +23 -0
- package/dist/merge/parse.d.ts.map +1 -0
- package/dist/merge/parse.js +78 -0
- package/dist/merge/parse.js.map +1 -0
- package/dist/merge/resolve.d.ts +66 -0
- package/dist/merge/resolve.d.ts.map +1 -0
- package/dist/merge/resolve.js +189 -0
- package/dist/merge/resolve.js.map +1 -0
- package/dist/merge/types.d.ts +82 -0
- package/dist/merge/types.d.ts.map +1 -0
- package/dist/merge/types.js +8 -0
- package/dist/merge/types.js.map +1 -0
- package/dist/parser/agent-data-sections.d.ts +53 -0
- package/dist/parser/agent-data-sections.d.ts.map +1 -0
- package/dist/parser/agent-data-sections.js +118 -0
- package/dist/parser/agent-data-sections.js.map +1 -0
- package/dist/parser/alignment.d.ts +4 -4
- package/dist/parser/alignment.d.ts.map +1 -1
- package/dist/parser/alignment.js +27 -22
- package/dist/parser/alignment.js.map +1 -1
- package/dist/parser/assess.d.ts +5 -5
- package/dist/parser/assess.d.ts.map +1 -1
- package/dist/parser/assess.js +36 -32
- package/dist/parser/assess.js.map +1 -1
- package/dist/parser/config.d.ts +351 -0
- package/dist/parser/config.d.ts.map +1 -0
- package/dist/parser/config.js +326 -0
- package/dist/parser/config.js.map +1 -0
- package/dist/parser/convention-validation.d.ts +1 -1
- package/dist/parser/convention-validation.d.ts.map +1 -1
- package/dist/parser/convention-validation.js +21 -16
- package/dist/parser/convention-validation.js.map +1 -1
- package/dist/parser/coverage-cache.d.ts +49 -0
- package/dist/parser/coverage-cache.d.ts.map +1 -0
- package/dist/parser/coverage-cache.js +123 -0
- package/dist/parser/coverage-cache.js.map +1 -0
- package/dist/parser/daemon-status.d.ts +37 -0
- package/dist/parser/daemon-status.d.ts.map +1 -0
- package/dist/parser/daemon-status.js +67 -0
- package/dist/parser/daemon-status.js.map +1 -0
- package/dist/parser/doctor.d.ts +107 -0
- package/dist/parser/doctor.d.ts.map +1 -0
- package/dist/parser/doctor.js +366 -0
- package/dist/parser/doctor.js.map +1 -0
- package/dist/parser/fix.d.ts +1 -1
- package/dist/parser/fix.d.ts.map +1 -1
- package/dist/parser/fix.js +31 -27
- package/dist/parser/fix.js.map +1 -1
- package/dist/parser/index.d.ts +16 -11
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +16 -11
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/items.d.ts +8 -2
- package/dist/parser/items.d.ts.map +1 -1
- package/dist/parser/items.js +71 -35
- package/dist/parser/items.js.map +1 -1
- package/dist/parser/meta.d.ts +167 -9
- package/dist/parser/meta.d.ts.map +1 -1
- package/dist/parser/meta.js +379 -46
- package/dist/parser/meta.js.map +1 -1
- package/dist/parser/plan-document.d.ts +189 -0
- package/dist/parser/plan-document.d.ts.map +1 -0
- package/dist/parser/plan-document.js +340 -0
- package/dist/parser/plan-document.js.map +1 -0
- package/dist/parser/plans.d.ts +59 -0
- package/dist/parser/plans.d.ts.map +1 -0
- package/dist/parser/plans.js +239 -0
- package/dist/parser/plans.js.map +1 -0
- package/dist/parser/refs.d.ts +22 -9
- package/dist/parser/refs.d.ts.map +1 -1
- package/dist/parser/refs.js +102 -50
- package/dist/parser/refs.js.map +1 -1
- package/dist/parser/setup-status.d.ts +71 -0
- package/dist/parser/setup-status.d.ts.map +1 -0
- package/dist/parser/setup-status.js +269 -0
- package/dist/parser/setup-status.js.map +1 -0
- package/dist/parser/shadow.d.ts +150 -19
- package/dist/parser/shadow.d.ts.map +1 -1
- package/dist/parser/shadow.js +548 -187
- package/dist/parser/shadow.js.map +1 -1
- package/dist/parser/skill-render.d.ts +317 -0
- package/dist/parser/skill-render.d.ts.map +1 -0
- package/dist/parser/skill-render.js +943 -0
- package/dist/parser/skill-render.js.map +1 -0
- package/dist/parser/traits.d.ts +3 -3
- package/dist/parser/traits.d.ts.map +1 -1
- package/dist/parser/traits.js +2 -2
- package/dist/parser/traits.js.map +1 -1
- package/dist/parser/validate-skills.d.ts +32 -0
- package/dist/parser/validate-skills.d.ts.map +1 -0
- package/dist/parser/validate-skills.js +202 -0
- package/dist/parser/validate-skills.js.map +1 -0
- package/dist/parser/validate.d.ts +45 -3
- package/dist/parser/validate.d.ts.map +1 -1
- package/dist/parser/validate.js +622 -105
- package/dist/parser/validate.js.map +1 -1
- package/dist/parser/yaml.d.ts +83 -19
- package/dist/parser/yaml.d.ts.map +1 -1
- package/dist/parser/yaml.js +478 -173
- package/dist/parser/yaml.js.map +1 -1
- package/dist/ralph/cli-renderer.d.ts +8 -1
- package/dist/ralph/cli-renderer.d.ts.map +1 -1
- package/dist/ralph/cli-renderer.js +105 -34
- package/dist/ralph/cli-renderer.js.map +1 -1
- package/dist/ralph/events.d.ts +10 -10
- package/dist/ralph/events.d.ts.map +1 -1
- package/dist/ralph/events.js +277 -98
- package/dist/ralph/events.js.map +1 -1
- package/dist/ralph/index.d.ts +5 -2
- package/dist/ralph/index.d.ts.map +1 -1
- package/dist/ralph/index.js +9 -3
- package/dist/ralph/index.js.map +1 -1
- package/dist/ralph/loop-errors.d.ts +83 -0
- package/dist/ralph/loop-errors.d.ts.map +1 -0
- package/dist/ralph/loop-errors.js +150 -0
- package/dist/ralph/loop-errors.js.map +1 -0
- package/dist/ralph/subagent.d.ts +83 -0
- package/dist/ralph/subagent.d.ts.map +1 -0
- package/dist/ralph/subagent.js +174 -0
- package/dist/ralph/subagent.js.map +1 -0
- package/dist/ralph/wrap-up.d.ts +125 -0
- package/dist/ralph/wrap-up.d.ts.map +1 -0
- package/dist/ralph/wrap-up.js +270 -0
- package/dist/ralph/wrap-up.js.map +1 -0
- package/dist/schema/batch.d.ts +95 -0
- package/dist/schema/batch.d.ts.map +1 -0
- package/dist/schema/batch.js +24 -0
- package/dist/schema/batch.js.map +1 -0
- package/dist/schema/common.d.ts +2 -2
- package/dist/schema/common.d.ts.map +1 -1
- package/dist/schema/common.js +34 -31
- package/dist/schema/common.js.map +1 -1
- package/dist/schema/inbox.d.ts +12 -12
- package/dist/schema/inbox.js +4 -4
- package/dist/schema/inbox.js.map +1 -1
- package/dist/schema/index.d.ts +8 -5
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +8 -5
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/meta.d.ts +1454 -27
- package/dist/schema/meta.d.ts.map +1 -1
- package/dist/schema/meta.js +198 -21
- package/dist/schema/meta.js.map +1 -1
- package/dist/schema/plan.d.ts +285 -0
- package/dist/schema/plan.d.ts.map +1 -0
- package/dist/schema/plan.js +81 -0
- package/dist/schema/plan.js.map +1 -0
- package/dist/schema/spec.d.ts +72 -33
- package/dist/schema/spec.d.ts.map +1 -1
- package/dist/schema/spec.js +22 -9
- package/dist/schema/spec.js.map +1 -1
- package/dist/schema/task.d.ts +172 -161
- package/dist/schema/task.d.ts.map +1 -1
- package/dist/schema/task.js +21 -12
- package/dist/schema/task.js.map +1 -1
- package/dist/schema/triage.d.ts +266 -0
- package/dist/schema/triage.d.ts.map +1 -0
- package/dist/schema/triage.js +134 -0
- package/dist/schema/triage.js.map +1 -0
- package/dist/sessions/index.d.ts +2 -2
- package/dist/sessions/index.d.ts.map +1 -1
- package/dist/sessions/index.js +3 -3
- package/dist/sessions/index.js.map +1 -1
- package/dist/sessions/store.d.ts +233 -1
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +628 -31
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/types.d.ts +10 -10
- package/dist/sessions/types.d.ts.map +1 -1
- package/dist/sessions/types.js +10 -9
- package/dist/sessions/types.js.map +1 -1
- package/dist/strings/errors.d.ts +51 -0
- package/dist/strings/errors.d.ts.map +1 -1
- package/dist/strings/errors.js +136 -106
- package/dist/strings/errors.js.map +1 -1
- package/dist/strings/guidance.d.ts.map +1 -1
- package/dist/strings/guidance.js +16 -16
- package/dist/strings/guidance.js.map +1 -1
- package/dist/strings/index.d.ts +4 -4
- package/dist/strings/index.d.ts.map +1 -1
- package/dist/strings/index.js +4 -4
- package/dist/strings/index.js.map +1 -1
- package/dist/strings/labels.d.ts +4 -0
- package/dist/strings/labels.d.ts.map +1 -1
- package/dist/strings/labels.js +45 -41
- package/dist/strings/labels.js.map +1 -1
- package/dist/strings/validation.d.ts.map +1 -1
- package/dist/strings/validation.js +71 -71
- package/dist/strings/validation.js.map +1 -1
- package/dist/utils/commit.d.ts +1 -1
- package/dist/utils/commit.d.ts.map +1 -1
- package/dist/utils/commit.js +28 -26
- package/dist/utils/commit.js.map +1 -1
- package/dist/utils/git.d.ts +1 -1
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +40 -38
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/grep.js +11 -11
- package/dist/utils/grep.js.map +1 -1
- package/dist/utils/index.d.ts +7 -7
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +4 -4
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/time.d.ts.map +1 -1
- package/dist/utils/time.js +10 -10
- package/dist/utils/time.js.map +1 -1
- package/package.json +28 -5
- package/plugin/.claude-plugin/marketplace.json +17 -0
- package/plugin/.claude-plugin/plugin.json +5 -0
- package/plugin/plugins/kspec/skills/help/SKILL.md +42 -0
- package/plugin/plugins/kspec/skills/triage/SKILL.md +206 -0
- package/plugin/plugins/kspec/skills/triage/docs/automation.md +120 -0
- package/plugin/plugins/kspec/skills/triage/docs/inbox.md +144 -0
- package/plugin/plugins/kspec/skills/triage/docs/observations.md +85 -0
- package/templates/agents-sections/01-quick-start.md +22 -0
- package/templates/agents-sections/02-shadow-branch.md +34 -0
- package/templates/agents-sections/03-task-lifecycle.md +48 -0
- package/templates/agents-sections/04-pr-workflow.md +17 -0
- package/templates/agents-sections/05-commit-convention.md +27 -0
- package/templates/agents-sections/06-ralph-loop.md +45 -0
- package/templates/hooks/pre-commit +34 -0
- package/templates/skills/help/SKILL.md +37 -0
- package/templates/skills/manifest.yaml +15 -0
- package/templates/skills/triage/SKILL.md +199 -0
- package/templates/skills/triage/docs/automation.md +120 -0
- package/templates/skills/triage/docs/inbox.md +144 -0
- package/templates/skills/triage/docs/observations.md +85 -0
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import chalk from
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import { commitIfShadow } from
|
|
5
|
-
import {
|
|
6
|
-
import { formatCommitGuidance, printCommitGuidance } from
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { markMutating } from "../command-annotations.js";
|
|
3
|
+
import { checkSlugUniqueness, createNote, createTask, createTodo, deleteTask, getAuthor, initContext, loadAllItems, loadAllTasks, ReferenceIndex, saveTask, scanTestCoverage, syncSpecImplementationStatus, } from "../../parser/index.js";
|
|
4
|
+
import { commitIfShadow } from "../../parser/shadow.js";
|
|
5
|
+
import { alignmentCheck, errors } from "../../strings/index.js";
|
|
6
|
+
import { formatCommitGuidance, printCommitGuidance, } from "../../utils/commit.js";
|
|
7
|
+
import { executeBatchOperation, formatBatchOutput } from "../batch.js";
|
|
8
|
+
import { EXIT_CODES } from "../exit-codes.js";
|
|
9
|
+
import { parseTagsArray } from "../parse-utils.js";
|
|
10
|
+
import { annotateNotesWithSuperseded, error, formatTaskDetails, info, isJsonMode, output, success, warn, } from "../output.js";
|
|
11
|
+
import { parseIntOption, validateEnumOption, validateSpecRef, } from "../validators.js";
|
|
12
|
+
import { addListOptions, listTasksAction } from "./tasks.js";
|
|
13
|
+
import { findClosestCommand } from "../suggest.js";
|
|
10
14
|
/**
|
|
11
15
|
* Find a task by reference with detailed error reporting.
|
|
12
16
|
* Returns the task or exits with appropriate error.
|
|
@@ -15,18 +19,18 @@ function resolveTaskRef(ref, tasks, index) {
|
|
|
15
19
|
const result = index.resolve(ref);
|
|
16
20
|
if (!result.ok) {
|
|
17
21
|
switch (result.error) {
|
|
18
|
-
case
|
|
22
|
+
case "not_found":
|
|
19
23
|
error(errors.reference.taskNotFound(ref));
|
|
20
24
|
break;
|
|
21
|
-
case
|
|
25
|
+
case "ambiguous":
|
|
22
26
|
error(errors.reference.ambiguous(ref));
|
|
23
27
|
for (const candidate of result.candidates) {
|
|
24
|
-
const task = tasks.find(t => t._ulid === candidate);
|
|
25
|
-
const slug = task?.slugs[0] ||
|
|
26
|
-
console.error(` - ${index.shortUlid(candidate)} ${slug ? `(${slug})` :
|
|
28
|
+
const task = tasks.find((t) => t._ulid === candidate);
|
|
29
|
+
const slug = task?.slugs[0] || "";
|
|
30
|
+
console.error(` - ${index.shortUlid(candidate)} ${slug ? `(${slug})` : ""}`);
|
|
27
31
|
}
|
|
28
32
|
break;
|
|
29
|
-
case
|
|
33
|
+
case "duplicate_slug":
|
|
30
34
|
error(errors.reference.slugMapsToMultiple(ref));
|
|
31
35
|
for (const candidate of result.candidates) {
|
|
32
36
|
console.error(` - ${index.shortUlid(candidate)}`);
|
|
@@ -37,7 +41,7 @@ function resolveTaskRef(ref, tasks, index) {
|
|
|
37
41
|
process.exit(EXIT_CODES.NOT_FOUND);
|
|
38
42
|
}
|
|
39
43
|
// Check if it's actually a task
|
|
40
|
-
const task = tasks.find(t => t._ulid === result.ulid);
|
|
44
|
+
const task = tasks.find((t) => t._ulid === result.ulid);
|
|
41
45
|
if (!task) {
|
|
42
46
|
error(errors.reference.notTask(ref));
|
|
43
47
|
// AC: @cli-exit-codes consistent-usage - NOT_FOUND for missing resources
|
|
@@ -55,20 +59,20 @@ function resolveTaskRefForBatch(ref, tasks, index) {
|
|
|
55
59
|
if (!result.ok) {
|
|
56
60
|
let errorMsg;
|
|
57
61
|
switch (result.error) {
|
|
58
|
-
case
|
|
62
|
+
case "not_found":
|
|
59
63
|
errorMsg = `Reference "${ref}" not found`;
|
|
60
64
|
break;
|
|
61
|
-
case
|
|
65
|
+
case "ambiguous":
|
|
62
66
|
errorMsg = `Reference "${ref}" is ambiguous (matches ${result.candidates.length} items)`;
|
|
63
67
|
break;
|
|
64
|
-
case
|
|
68
|
+
case "duplicate_slug":
|
|
65
69
|
errorMsg = `Slug "${ref}" maps to multiple items`;
|
|
66
70
|
break;
|
|
67
71
|
}
|
|
68
72
|
return { task: null, error: errorMsg };
|
|
69
73
|
}
|
|
70
74
|
// Check if it's actually a task
|
|
71
|
-
const task = tasks.find(t => t._ulid === result.ulid);
|
|
75
|
+
const task = tasks.find((t) => t._ulid === result.ulid);
|
|
72
76
|
if (!task) {
|
|
73
77
|
return { task: null, error: `Reference "${ref}" is not a task` };
|
|
74
78
|
}
|
|
@@ -79,7 +83,7 @@ function resolveTaskRefForBatch(ref, tasks, index) {
|
|
|
79
83
|
* Used by both single-ref and batch modes of task set.
|
|
80
84
|
* AC: @spec-task-set-batch ac-1, ac-2, ac-4, ac-5
|
|
81
85
|
*/
|
|
82
|
-
async function setTaskFields(foundTask, ctx, tasks, items,
|
|
86
|
+
async function setTaskFields(foundTask, ctx, tasks, items, _allMetaItems, index, options) {
|
|
83
87
|
try {
|
|
84
88
|
// Check slug uniqueness if adding a new slug
|
|
85
89
|
if (options.slug) {
|
|
@@ -96,7 +100,7 @@ async function setTaskFields(foundTask, ctx, tasks, items, allMetaItems, index,
|
|
|
96
100
|
const changes = [];
|
|
97
101
|
if (options.title) {
|
|
98
102
|
updatedTask.title = options.title;
|
|
99
|
-
changes.push(
|
|
103
|
+
changes.push("title");
|
|
100
104
|
}
|
|
101
105
|
if (options.specRef) {
|
|
102
106
|
// Validate the spec ref exists and is a spec item
|
|
@@ -108,7 +112,7 @@ async function setTaskFields(foundTask, ctx, tasks, items, allMetaItems, index,
|
|
|
108
112
|
};
|
|
109
113
|
}
|
|
110
114
|
// Check it's not a task
|
|
111
|
-
const isTask = tasks.some(t => t._ulid === specResult.ulid);
|
|
115
|
+
const isTask = tasks.some((t) => t._ulid === specResult.ulid);
|
|
112
116
|
if (isTask) {
|
|
113
117
|
return {
|
|
114
118
|
success: false,
|
|
@@ -116,7 +120,7 @@ async function setTaskFields(foundTask, ctx, tasks, items, allMetaItems, index,
|
|
|
116
120
|
};
|
|
117
121
|
}
|
|
118
122
|
updatedTask.spec_ref = options.specRef;
|
|
119
|
-
changes.push(
|
|
123
|
+
changes.push("spec_ref");
|
|
120
124
|
}
|
|
121
125
|
if (options.metaRef) {
|
|
122
126
|
// Validate the meta ref exists and is a meta item
|
|
@@ -128,8 +132,8 @@ async function setTaskFields(foundTask, ctx, tasks, items, allMetaItems, index,
|
|
|
128
132
|
};
|
|
129
133
|
}
|
|
130
134
|
// Check if the resolved item is a meta item (not a spec item or task)
|
|
131
|
-
const isTask = tasks.some(t => t._ulid === metaRefResult.ulid);
|
|
132
|
-
const isSpecItem = items.some(i => i._ulid === metaRefResult.ulid);
|
|
135
|
+
const isTask = tasks.some((t) => t._ulid === metaRefResult.ulid);
|
|
136
|
+
const isSpecItem = items.some((i) => i._ulid === metaRefResult.ulid);
|
|
133
137
|
if (isTask || isSpecItem) {
|
|
134
138
|
return {
|
|
135
139
|
success: false,
|
|
@@ -137,30 +141,68 @@ async function setTaskFields(foundTask, ctx, tasks, items, allMetaItems, index,
|
|
|
137
141
|
};
|
|
138
142
|
}
|
|
139
143
|
updatedTask.meta_ref = options.metaRef;
|
|
140
|
-
changes.push(
|
|
144
|
+
changes.push("meta_ref");
|
|
145
|
+
}
|
|
146
|
+
if (options.planRef !== undefined) {
|
|
147
|
+
// Handle 'null' string to clear plan_ref
|
|
148
|
+
if (options.planRef === "null") {
|
|
149
|
+
updatedTask.plan_ref = null;
|
|
150
|
+
changes.push("plan_ref: cleared");
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// First check if it's a task or spec item (wrong type)
|
|
154
|
+
const cleanRef = options.planRef.startsWith("@")
|
|
155
|
+
? options.planRef.slice(1)
|
|
156
|
+
: options.planRef;
|
|
157
|
+
const isTask = tasks.some((t) => t.slugs.includes(cleanRef) ||
|
|
158
|
+
t._ulid === cleanRef ||
|
|
159
|
+
t._ulid.toLowerCase().startsWith(cleanRef.toLowerCase()));
|
|
160
|
+
const isSpecItem = items.some((i) => i.slugs.includes(cleanRef) ||
|
|
161
|
+
i._ulid === cleanRef ||
|
|
162
|
+
i._ulid.toLowerCase().startsWith(cleanRef.toLowerCase()));
|
|
163
|
+
if (isTask || isSpecItem) {
|
|
164
|
+
return {
|
|
165
|
+
success: false,
|
|
166
|
+
error: `Reference "${options.planRef}" is not a plan`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
// Now check if the plan exists
|
|
170
|
+
const { findPlanByRef } = await import("../../parser/plans.js");
|
|
171
|
+
const plan = await findPlanByRef(ctx, options.planRef);
|
|
172
|
+
if (!plan) {
|
|
173
|
+
return {
|
|
174
|
+
success: false,
|
|
175
|
+
error: `Plan reference not found: ${options.planRef}`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
updatedTask.plan_ref = options.planRef;
|
|
179
|
+
changes.push("plan_ref");
|
|
180
|
+
}
|
|
141
181
|
}
|
|
142
182
|
if (options.priority) {
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
183
|
+
const priorityResult = parseIntOption(options.priority, {
|
|
184
|
+
min: 1,
|
|
185
|
+
max: 5,
|
|
186
|
+
name: "Priority",
|
|
187
|
+
});
|
|
188
|
+
if (!priorityResult.ok) {
|
|
189
|
+
return { success: false, error: priorityResult.error };
|
|
149
190
|
}
|
|
150
|
-
updatedTask.priority =
|
|
151
|
-
changes.push(
|
|
191
|
+
updatedTask.priority = priorityResult.value;
|
|
192
|
+
changes.push("priority");
|
|
152
193
|
}
|
|
153
194
|
if (options.slug) {
|
|
154
195
|
if (!updatedTask.slugs.includes(options.slug)) {
|
|
155
196
|
updatedTask.slugs = [...updatedTask.slugs, options.slug];
|
|
156
|
-
changes.push(
|
|
197
|
+
changes.push("slug");
|
|
157
198
|
}
|
|
158
199
|
}
|
|
159
200
|
if (options.tag) {
|
|
160
|
-
const
|
|
201
|
+
const parsedTags = parseTagsArray(options.tag);
|
|
202
|
+
const newTags = parsedTags.filter((t) => !updatedTask.tags.includes(t));
|
|
161
203
|
if (newTags.length > 0) {
|
|
162
204
|
updatedTask.tags = [...updatedTask.tags, ...newTags];
|
|
163
|
-
changes.push(
|
|
205
|
+
changes.push("tags");
|
|
164
206
|
}
|
|
165
207
|
}
|
|
166
208
|
if (options.dependsOn) {
|
|
@@ -173,9 +215,17 @@ async function setTaskFields(foundTask, ctx, tasks, items, allMetaItems, index,
|
|
|
173
215
|
error: errors.reference.depNotFound(depRef),
|
|
174
216
|
};
|
|
175
217
|
}
|
|
218
|
+
// Ensure the dependency is a task, not a spec item
|
|
219
|
+
const isTask = tasks.some((t) => t._ulid === depResult.ulid);
|
|
220
|
+
if (!isTask) {
|
|
221
|
+
return {
|
|
222
|
+
success: false,
|
|
223
|
+
error: `Reference "${depRef}" is not a task`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
176
226
|
}
|
|
177
|
-
updatedTask.depends_on = options.dependsOn;
|
|
178
|
-
changes.push(
|
|
227
|
+
updatedTask.depends_on = options.dependsOn.map((ref) => ref.startsWith("@") ? ref : `@${ref}`);
|
|
228
|
+
changes.push("depends_on");
|
|
179
229
|
}
|
|
180
230
|
// AC: @spec-task-clear-deps ac-1, ac-2 - Clear all dependencies
|
|
181
231
|
if (options.clearDeps) {
|
|
@@ -183,14 +233,14 @@ async function setTaskFields(foundTask, ctx, tasks, items, allMetaItems, index,
|
|
|
183
233
|
// AC: @spec-task-clear-deps ac-2 - No changes needed
|
|
184
234
|
return {
|
|
185
235
|
success: true,
|
|
186
|
-
message:
|
|
236
|
+
message: "No changes: task has no dependencies to clear",
|
|
187
237
|
};
|
|
188
238
|
}
|
|
189
239
|
updatedTask.depends_on = [];
|
|
190
|
-
changes.push(
|
|
240
|
+
changes.push("depends_on");
|
|
191
241
|
// Add note documenting the change
|
|
192
242
|
// AC: @task-set ac-author
|
|
193
|
-
const note = createNote(`Dependencies cleared (was: ${foundTask.depends_on.join(
|
|
243
|
+
const note = createNote(`Dependencies cleared (was: ${foundTask.depends_on.join(", ")})`, getAuthor(ctx.config?.identity?.author));
|
|
194
244
|
updatedTask.notes = [...updatedTask.notes, note];
|
|
195
245
|
}
|
|
196
246
|
// AC: @task-automation-eligibility ac-5, ac-11, ac-12, ac-18
|
|
@@ -199,46 +249,43 @@ async function setTaskFields(foundTask, ctx, tasks, items, allMetaItems, index,
|
|
|
199
249
|
if (options.automation === false) {
|
|
200
250
|
// --no-automation flag clears the automation status (AC: ac-12)
|
|
201
251
|
delete updatedTask.automation;
|
|
202
|
-
changes.push(
|
|
252
|
+
changes.push("automation");
|
|
203
253
|
}
|
|
204
254
|
else if (options.automation !== undefined) {
|
|
205
|
-
const
|
|
206
|
-
if (!
|
|
207
|
-
return {
|
|
208
|
-
success: false,
|
|
209
|
-
error: `Invalid automation status: ${options.automation}. Must be one of: ${validStatuses.join(', ')}`,
|
|
210
|
-
};
|
|
255
|
+
const automationResult = validateEnumOption(options.automation, ["eligible", "needs_review", "manual_only"], "automation status");
|
|
256
|
+
if (!automationResult.ok) {
|
|
257
|
+
return { success: false, error: automationResult.error };
|
|
211
258
|
}
|
|
212
259
|
// AC: @task-automation-eligibility ac-18 - require reason for needs_review
|
|
213
|
-
if (options.automation ===
|
|
260
|
+
if (options.automation === "needs_review" && !options.reason) {
|
|
214
261
|
return {
|
|
215
262
|
success: false,
|
|
216
|
-
error:
|
|
263
|
+
error: "Setting automation to needs_review requires --reason flag explaining why",
|
|
217
264
|
};
|
|
218
265
|
}
|
|
219
|
-
updatedTask.automation =
|
|
220
|
-
changes.push(
|
|
266
|
+
updatedTask.automation = automationResult.value;
|
|
267
|
+
changes.push("automation");
|
|
221
268
|
// If reason provided, add a note documenting the change
|
|
222
269
|
// AC: @task-set ac-author
|
|
223
270
|
if (options.reason) {
|
|
224
|
-
const note = createNote(`Automation status set to ${
|
|
271
|
+
const note = createNote(`Automation status set to ${automationResult.value}: ${options.reason}`, getAuthor(ctx.config?.identity?.author));
|
|
225
272
|
updatedTask.notes = [...updatedTask.notes, note];
|
|
226
|
-
changes.push(
|
|
273
|
+
changes.push("note");
|
|
227
274
|
}
|
|
228
275
|
}
|
|
229
276
|
// AC: @spec-task-set-batch ac-4 - Warn on no changes, don't fail
|
|
230
277
|
if (changes.length === 0) {
|
|
231
278
|
return {
|
|
232
279
|
success: true,
|
|
233
|
-
message:
|
|
280
|
+
message: "No changes specified",
|
|
234
281
|
data: { task: updatedTask },
|
|
235
282
|
};
|
|
236
283
|
}
|
|
237
284
|
await saveTask(ctx, updatedTask);
|
|
238
|
-
await commitIfShadow(ctx.shadow,
|
|
285
|
+
await commitIfShadow(ctx.shadow, "task-set", foundTask.slugs[0] || index.shortUlid(foundTask._ulid), changes.join(", "));
|
|
239
286
|
return {
|
|
240
287
|
success: true,
|
|
241
|
-
message: `Updated task: ${index.shortUlid(updatedTask._ulid)} (${changes.join(
|
|
288
|
+
message: `Updated task: ${index.shortUlid(updatedTask._ulid)} (${changes.join(", ")})`,
|
|
242
289
|
data: { task: updatedTask },
|
|
243
290
|
};
|
|
244
291
|
}
|
|
@@ -254,20 +301,68 @@ async function setTaskFields(foundTask, ctx, tasks, items, allMetaItems, index,
|
|
|
254
301
|
*/
|
|
255
302
|
export function registerTaskCommands(program) {
|
|
256
303
|
const task = program
|
|
257
|
-
.command(
|
|
258
|
-
.description(
|
|
304
|
+
.command("task")
|
|
305
|
+
.description("Operations on individual tasks")
|
|
306
|
+
.allowUnknownOption()
|
|
307
|
+
.allowExcessArguments();
|
|
308
|
+
// AC: @command-group-default-actions ac-bare-task, ac-unknown-subcommand
|
|
309
|
+
// Default action when no subcommand is given (e.g. `kspec task` or `kspec task --status pending`)
|
|
310
|
+
task.action(async (_options, cmd) => {
|
|
311
|
+
const { Command: Cmd } = await import("commander");
|
|
312
|
+
const listCmd = addListOptions(new Cmd("_list"));
|
|
313
|
+
listCmd.exitOverride();
|
|
314
|
+
try {
|
|
315
|
+
listCmd.parse(cmd.args, { from: "user" });
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
console.error(chalk.gray(`Run 'kspec task --help' to see available subcommands`));
|
|
319
|
+
process.exit(EXIT_CODES.ERROR);
|
|
320
|
+
}
|
|
321
|
+
// AC: @command-group-default-actions ac-unknown-subcommand
|
|
322
|
+
if (listCmd.args.length > 0) {
|
|
323
|
+
const unknownCmd = listCmd.args[0];
|
|
324
|
+
const subcommandNames = cmd.commands.map((c) => c.name());
|
|
325
|
+
const suggestion = findClosestCommand(unknownCmd, subcommandNames);
|
|
326
|
+
console.error(chalk.red(`error: unknown command 'task ${unknownCmd}'`));
|
|
327
|
+
if (suggestion) {
|
|
328
|
+
console.error(chalk.yellow(`Did you mean: kspec task ${suggestion}?`));
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
console.error(chalk.gray(`Run 'kspec task --help' to see available subcommands`));
|
|
332
|
+
}
|
|
333
|
+
process.exit(EXIT_CODES.ERROR);
|
|
334
|
+
}
|
|
335
|
+
// AC: @command-group-default-actions ac-bare-with-options
|
|
336
|
+
await listTasksAction(listCmd.opts());
|
|
337
|
+
});
|
|
338
|
+
// kspec task list - alias for 'kspec tasks list'
|
|
339
|
+
task
|
|
340
|
+
.command("list")
|
|
341
|
+
.description("List all tasks (alias for 'kspec tasks list')")
|
|
342
|
+
.option("-s, --status <status>", "Filter by status")
|
|
343
|
+
.option("-t, --type <type>", "Filter by type")
|
|
344
|
+
.option("--tag <tag>", "Filter by tag")
|
|
345
|
+
.option("--meta-ref <ref>", "Filter by meta reference")
|
|
346
|
+
.option("-g, --grep <pattern>", "Search content with regex pattern")
|
|
347
|
+
.option("-v, --verbose", "Show more details")
|
|
348
|
+
.option("--full", "Show full details (notes, todos, timestamps)")
|
|
349
|
+
.option("--count", "Show only the count of matching tasks")
|
|
350
|
+
.action(async (options) => {
|
|
351
|
+
await listTasksAction(options);
|
|
352
|
+
});
|
|
259
353
|
// kspec task get <ref>
|
|
260
354
|
task
|
|
261
|
-
.command(
|
|
262
|
-
.description(
|
|
263
|
-
.
|
|
355
|
+
.command("get <ref>")
|
|
356
|
+
.description("Get task details")
|
|
357
|
+
.option("--all", "Show all notes including superseded ones")
|
|
358
|
+
.action(async (ref, options) => {
|
|
264
359
|
try {
|
|
265
360
|
const ctx = await initContext();
|
|
266
361
|
const tasks = await loadAllTasks(ctx);
|
|
267
|
-
const
|
|
362
|
+
const _items = await loadAllItems(ctx);
|
|
268
363
|
// Build all indexes including TraitIndex
|
|
269
364
|
const { refIndex: index, traitIndex } = await (async () => {
|
|
270
|
-
const { buildIndexes } = await import(
|
|
365
|
+
const { buildIndexes } = await import("../../parser/index.js");
|
|
271
366
|
return buildIndexes(ctx);
|
|
272
367
|
})();
|
|
273
368
|
const foundTask = resolveTaskRef(ref, tasks, index);
|
|
@@ -284,14 +379,16 @@ export function registerTaskCommands(program) {
|
|
|
284
379
|
if (!traitsByTrait.has(trait.ulid)) {
|
|
285
380
|
traitsByTrait.set(trait.ulid, { trait, acs: [] });
|
|
286
381
|
}
|
|
287
|
-
traitsByTrait.get(trait.ulid)
|
|
382
|
+
traitsByTrait.get(trait.ulid)?.acs.push(ac);
|
|
288
383
|
}
|
|
289
384
|
inheritedTraits = Array.from(traitsByTrait.values());
|
|
290
385
|
}
|
|
291
386
|
}
|
|
292
387
|
// Build JSON output with inherited traits (AC: @trait-display ac-2)
|
|
388
|
+
// Always include all notes in JSON output with superseded computed field
|
|
293
389
|
const jsonOutput = {
|
|
294
390
|
...foundTask,
|
|
391
|
+
notes: annotateNotesWithSuperseded(foundTask.notes),
|
|
295
392
|
...(inheritedTraits.length > 0 && {
|
|
296
393
|
inherited_traits: inheritedTraits.map(({ trait, acs }) => ({
|
|
297
394
|
ref: `@${trait.slug}`,
|
|
@@ -301,13 +398,14 @@ export function registerTaskCommands(program) {
|
|
|
301
398
|
}),
|
|
302
399
|
};
|
|
303
400
|
output(jsonOutput, () => {
|
|
304
|
-
formatTaskDetails(foundTask, index);
|
|
401
|
+
formatTaskDetails(foundTask, index, { showAllNotes: options.all });
|
|
305
402
|
// AC: @trait-display ac-3, ac-4, ac-5 - Show inherited AC per trait in labeled sections
|
|
306
403
|
if (inheritedTraits.length > 0) {
|
|
307
404
|
for (const { trait, acs } of inheritedTraits) {
|
|
308
405
|
console.log(chalk.gray(`\n─── Inherited from @${trait.slug} ───`));
|
|
309
406
|
for (const ac of acs) {
|
|
310
|
-
console.log(chalk.cyan(` [${ac.id}]`) +
|
|
407
|
+
console.log(chalk.cyan(` [${ac.id}]`) +
|
|
408
|
+
chalk.gray(` (from @${trait.slug})`));
|
|
311
409
|
if (ac.given)
|
|
312
410
|
console.log(` Given: ${ac.given}`);
|
|
313
411
|
if (ac.when)
|
|
@@ -325,25 +423,31 @@ export function registerTaskCommands(program) {
|
|
|
325
423
|
}
|
|
326
424
|
});
|
|
327
425
|
// kspec task add
|
|
328
|
-
task
|
|
329
|
-
.
|
|
330
|
-
.
|
|
331
|
-
.
|
|
332
|
-
.option(
|
|
333
|
-
.option(
|
|
334
|
-
.option(
|
|
335
|
-
.option(
|
|
336
|
-
.option(
|
|
337
|
-
.option(
|
|
338
|
-
.option(
|
|
339
|
-
.option(
|
|
426
|
+
markMutating(task.command("add"))
|
|
427
|
+
.description("Create a new task")
|
|
428
|
+
.requiredOption("--title <title>", "Task title")
|
|
429
|
+
.option("--description <description>", "Task description")
|
|
430
|
+
.option("--type <type>", "Task type (task, epic, bug, spike, infra)", "task")
|
|
431
|
+
.option("--spec-ref <ref>", "Reference to spec item")
|
|
432
|
+
.option("--meta-ref <ref>", "Reference to meta item (workflow, agent, or convention)")
|
|
433
|
+
.option("--plan-ref <ref>", "Reference to plan this task is derived from")
|
|
434
|
+
.option("--priority <n>", "Priority (1-5)", "3")
|
|
435
|
+
.option("--slug <slug>", "Human-friendly slug")
|
|
436
|
+
.option("--tag <tag...>", "Tags")
|
|
437
|
+
.option("--depends-on <refs...>", "Set task dependencies")
|
|
438
|
+
.option("--automation <status>", "Automation eligibility (eligible, needs_review, manual_only)")
|
|
439
|
+
.addHelpText("after", `
|
|
440
|
+
Examples:
|
|
441
|
+
$ kspec task add --title "Implement feature" --spec-ref @feature-spec
|
|
442
|
+
$ kspec task add --title "Fix bug" --type bug --priority 1
|
|
443
|
+
$ kspec task add --title "Multi-tag task" --tag cli urgent`)
|
|
340
444
|
.action(async (options) => {
|
|
341
445
|
try {
|
|
342
446
|
const ctx = await initContext();
|
|
343
447
|
const tasks = await loadAllTasks(ctx);
|
|
344
448
|
const items = await loadAllItems(ctx);
|
|
345
449
|
// Load meta items for validation
|
|
346
|
-
const { loadMetaContext } = await import(
|
|
450
|
+
const { loadMetaContext } = await import("../../parser/meta.js");
|
|
347
451
|
const metaContext = await loadMetaContext(ctx);
|
|
348
452
|
const allMetaItems = [
|
|
349
453
|
...metaContext.agents,
|
|
@@ -369,25 +473,83 @@ export function registerTaskCommands(program) {
|
|
|
369
473
|
process.exit(EXIT_CODES.NOT_FOUND);
|
|
370
474
|
}
|
|
371
475
|
// Check if the resolved item is a meta item (not a spec item or task)
|
|
372
|
-
const isTask = tasks.some(t => t._ulid === metaRefResult.ulid);
|
|
373
|
-
const isSpecItem = items.some(i => i._ulid === metaRefResult.ulid);
|
|
476
|
+
const isTask = tasks.some((t) => t._ulid === metaRefResult.ulid);
|
|
477
|
+
const isSpecItem = items.some((i) => i._ulid === metaRefResult.ulid);
|
|
374
478
|
if (isTask || isSpecItem) {
|
|
375
479
|
error(errors.reference.metaRefPointsToSpec(options.metaRef));
|
|
376
480
|
process.exit(EXIT_CODES.NOT_FOUND);
|
|
377
481
|
}
|
|
378
482
|
}
|
|
483
|
+
// Validate spec_ref if provided — must point to a spec item, not a task or meta item
|
|
484
|
+
if (options.specRef) {
|
|
485
|
+
const specRefResult = validateSpecRef(options.specRef, refIndex, tasks, items);
|
|
486
|
+
if (!specRefResult.ok) {
|
|
487
|
+
error(specRefResult.error);
|
|
488
|
+
process.exit(EXIT_CODES.NOT_FOUND);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
379
491
|
// AC: @task-automation-eligibility ac-13 - validate automation if provided
|
|
380
492
|
let automationValue;
|
|
381
493
|
if (options.automation) {
|
|
382
|
-
const
|
|
383
|
-
if (!
|
|
384
|
-
error(
|
|
494
|
+
const automationResult = validateEnumOption(options.automation, ["eligible", "needs_review", "manual_only"], "automation status");
|
|
495
|
+
if (!automationResult.ok) {
|
|
496
|
+
error(automationResult.error);
|
|
385
497
|
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
386
498
|
}
|
|
387
|
-
automationValue =
|
|
499
|
+
automationValue = automationResult.value;
|
|
500
|
+
}
|
|
501
|
+
// Validate plan_ref if provided (AC: @plan-derive ac-5, ac-6)
|
|
502
|
+
if (options.planRef) {
|
|
503
|
+
// First check if it's a task or spec item (wrong type)
|
|
504
|
+
const cleanRef = options.planRef.startsWith("@")
|
|
505
|
+
? options.planRef.slice(1)
|
|
506
|
+
: options.planRef;
|
|
507
|
+
const isTask = tasks.some((t) => t.slugs.includes(cleanRef) ||
|
|
508
|
+
t._ulid === cleanRef ||
|
|
509
|
+
t._ulid.toLowerCase().startsWith(cleanRef.toLowerCase()));
|
|
510
|
+
const isSpecItem = items.some((i) => i.slugs.includes(cleanRef) ||
|
|
511
|
+
i._ulid === cleanRef ||
|
|
512
|
+
i._ulid.toLowerCase().startsWith(cleanRef.toLowerCase()));
|
|
513
|
+
if (isTask || isSpecItem) {
|
|
514
|
+
error(`Reference "${options.planRef}" is not a plan`);
|
|
515
|
+
process.exit(EXIT_CODES.NOT_FOUND);
|
|
516
|
+
}
|
|
517
|
+
// Now check if the plan exists
|
|
518
|
+
const { findPlanByRef } = await import("../../parser/plans.js");
|
|
519
|
+
const plan = await findPlanByRef(ctx, options.planRef);
|
|
520
|
+
if (!plan) {
|
|
521
|
+
error(`Plan reference not found: ${options.planRef}`);
|
|
522
|
+
process.exit(EXIT_CODES.NOT_FOUND);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
// AC: @task-add-depends-on ac-2 - Validate dependency refs
|
|
526
|
+
if (options.dependsOn) {
|
|
527
|
+
for (const depRef of options.dependsOn) {
|
|
528
|
+
const depResult = refIndex.resolve(depRef);
|
|
529
|
+
if (!depResult.ok) {
|
|
530
|
+
error(errors.reference.depNotFound(depRef));
|
|
531
|
+
process.exit(EXIT_CODES.NOT_FOUND);
|
|
532
|
+
}
|
|
533
|
+
// Ensure the dependency is a task, not a spec item
|
|
534
|
+
const isTask = tasks.some((t) => t._ulid === depResult.ulid);
|
|
535
|
+
if (!isTask) {
|
|
536
|
+
error(`Reference "${depRef}" is not a task`);
|
|
537
|
+
process.exit(EXIT_CODES.NOT_FOUND);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
// Validate priority
|
|
542
|
+
const priorityResult = parseIntOption(options.priority, {
|
|
543
|
+
min: 1,
|
|
544
|
+
max: 5,
|
|
545
|
+
name: "Priority",
|
|
546
|
+
});
|
|
547
|
+
if (!priorityResult.ok) {
|
|
548
|
+
error(priorityResult.error);
|
|
549
|
+
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
388
550
|
}
|
|
389
551
|
// AC: @spec-task-add-description ac-6 - Omit description if empty string
|
|
390
|
-
const descriptionValue = options.description && options.description.trim() !==
|
|
552
|
+
const descriptionValue = options.description && options.description.trim() !== ""
|
|
391
553
|
? options.description
|
|
392
554
|
: undefined;
|
|
393
555
|
const input = {
|
|
@@ -396,17 +558,21 @@ export function registerTaskCommands(program) {
|
|
|
396
558
|
type: options.type,
|
|
397
559
|
spec_ref: options.specRef || null,
|
|
398
560
|
meta_ref: options.metaRef || null,
|
|
399
|
-
|
|
561
|
+
plan_ref: options.planRef || null,
|
|
562
|
+
priority: priorityResult.value,
|
|
400
563
|
slugs: options.slug ? [options.slug] : [],
|
|
401
|
-
tags: options.tag
|
|
564
|
+
tags: parseTagsArray(options.tag),
|
|
565
|
+
depends_on: (options.dependsOn || []).map((ref) => ref.startsWith("@") ? ref : `@${ref}`),
|
|
402
566
|
automation: automationValue,
|
|
403
567
|
};
|
|
404
568
|
const newTask = createTask(input);
|
|
405
569
|
await saveTask(ctx, newTask);
|
|
406
|
-
await commitIfShadow(ctx.shadow,
|
|
570
|
+
await commitIfShadow(ctx.shadow, "task-add", newTask.slugs[0] || newTask._ulid.slice(0, 8), newTask.title);
|
|
407
571
|
// Build index including the new task for accurate short ULID
|
|
408
572
|
const index = new ReferenceIndex([...tasks, newTask], items, allMetaItems);
|
|
409
|
-
success(`Created task: ${index.shortUlid(newTask._ulid)}`, {
|
|
573
|
+
success(`Created task: ${index.shortUlid(newTask._ulid)}`, {
|
|
574
|
+
task: newTask,
|
|
575
|
+
});
|
|
410
576
|
}
|
|
411
577
|
catch (err) {
|
|
412
578
|
error(errors.failures.createTask, err);
|
|
@@ -414,39 +580,45 @@ export function registerTaskCommands(program) {
|
|
|
414
580
|
}
|
|
415
581
|
});
|
|
416
582
|
// kspec task set <ref>
|
|
417
|
-
task
|
|
418
|
-
.
|
|
419
|
-
.
|
|
420
|
-
.option(
|
|
421
|
-
.option(
|
|
422
|
-
.option(
|
|
423
|
-
.option(
|
|
424
|
-
.option(
|
|
425
|
-
.option(
|
|
426
|
-
.option(
|
|
427
|
-
.option(
|
|
428
|
-
.option(
|
|
429
|
-
.option(
|
|
430
|
-
.option(
|
|
431
|
-
.option(
|
|
432
|
-
.option(
|
|
583
|
+
markMutating(task.command("set [ref]"))
|
|
584
|
+
.description("Update task fields")
|
|
585
|
+
.option("--refs <refs...>", "Update multiple tasks (AC: @spec-task-set-batch ac-1)")
|
|
586
|
+
.option("--title <title>", "Update task title")
|
|
587
|
+
.option("--spec-ref <ref>", "Link to spec item")
|
|
588
|
+
.option("--meta-ref <ref>", "Link to meta item (workflow, agent, or convention)")
|
|
589
|
+
.option("--plan-ref <ref>", "Link to plan (use 'null' to clear)")
|
|
590
|
+
.option("--priority <n>", "Set priority (1-5)")
|
|
591
|
+
.option("--slug <slug>", "Add a slug alias")
|
|
592
|
+
.option("--tag <tag...>", "Add tags")
|
|
593
|
+
.option("--depends-on <refs...>", "Set dependencies (replaces existing)")
|
|
594
|
+
.option("--clear-deps", "Clear all dependencies")
|
|
595
|
+
.option("--automation <status>", "Set automation eligibility (eligible, needs_review, manual_only)")
|
|
596
|
+
.option("--no-automation", "Clear automation status (return to unassessed)")
|
|
597
|
+
.option("--reason <reason>", "Reason for status change (required when setting needs_review)")
|
|
598
|
+
.option("--status <status>", "Reject with error - use state transition commands instead")
|
|
599
|
+
.addHelpText("after", `
|
|
600
|
+
Examples:
|
|
601
|
+
$ kspec task set @task-slug --priority 2
|
|
602
|
+
$ kspec task set @task-slug --depends-on @dep1 @dep2
|
|
603
|
+
$ kspec task set @task-slug --tag cli urgent
|
|
604
|
+
$ kspec task set --refs @task1 @task2 --priority 3`)
|
|
433
605
|
.action(async (ref, options) => {
|
|
434
606
|
try {
|
|
435
607
|
// AC: @spec-task-set-batch ac-3 - Reject --status flag
|
|
436
608
|
if (options.status !== undefined) {
|
|
437
|
-
error(
|
|
609
|
+
error("Use state transition commands (start, complete, block, etc.) to change status");
|
|
438
610
|
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
439
611
|
}
|
|
440
612
|
// AC: @spec-task-clear-deps ac-3 - Mutual exclusivity check
|
|
441
613
|
if (options.clearDeps && options.dependsOn) {
|
|
442
|
-
error(
|
|
614
|
+
error("Cannot use --clear-deps and --depends-on together");
|
|
443
615
|
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
444
616
|
}
|
|
445
617
|
const ctx = await initContext();
|
|
446
618
|
const tasks = await loadAllTasks(ctx);
|
|
447
619
|
const items = await loadAllItems(ctx);
|
|
448
620
|
// Load meta items for validation
|
|
449
|
-
const { loadMetaContext } = await import(
|
|
621
|
+
const { loadMetaContext } = await import("../../parser/meta.js");
|
|
450
622
|
const metaContext = await loadMetaContext(ctx);
|
|
451
623
|
const allMetaItems = [
|
|
452
624
|
...metaContext.agents,
|
|
@@ -456,7 +628,9 @@ export function registerTaskCommands(program) {
|
|
|
456
628
|
];
|
|
457
629
|
const index = new ReferenceIndex(tasks, items, allMetaItems);
|
|
458
630
|
// AC: @trait-multi-ref-batch ac-8 - Deduplicate refs
|
|
459
|
-
const refsFlag = options.refs
|
|
631
|
+
const refsFlag = options.refs
|
|
632
|
+
? [...new Set(options.refs)]
|
|
633
|
+
: undefined;
|
|
460
634
|
// Batch mode or single mode?
|
|
461
635
|
if (refsFlag && refsFlag.length > 0) {
|
|
462
636
|
// Batch mode - AC: @spec-task-set-batch ac-1, ac-2, ac-5
|
|
@@ -475,23 +649,23 @@ export function registerTaskCommands(program) {
|
|
|
475
649
|
},
|
|
476
650
|
getUlid: (task) => task._ulid,
|
|
477
651
|
});
|
|
478
|
-
formatBatchOutput(result,
|
|
652
|
+
formatBatchOutput(result, "Set");
|
|
479
653
|
}
|
|
480
654
|
else {
|
|
481
655
|
// Single mode - existing behavior
|
|
482
656
|
if (!ref) {
|
|
483
|
-
error(
|
|
657
|
+
error("Either provide a positional ref or use --refs flag");
|
|
484
658
|
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
485
659
|
}
|
|
486
660
|
const foundTask = resolveTaskRef(ref, tasks, index);
|
|
487
661
|
const result = await setTaskFields(foundTask, ctx, tasks, items, allMetaItems, index, options);
|
|
488
662
|
if (!result.success) {
|
|
489
|
-
error(result.error ||
|
|
663
|
+
error(result.error || "Failed to update task");
|
|
490
664
|
process.exit(EXIT_CODES.ERROR);
|
|
491
665
|
}
|
|
492
666
|
if (result.message) {
|
|
493
667
|
// AC: @spec-task-set-batch ac-4 - Warn on no changes
|
|
494
|
-
if (result.message.includes(
|
|
668
|
+
if (result.message.includes("No changes")) {
|
|
495
669
|
if (isJsonMode()) {
|
|
496
670
|
output({ success: true, message: result.message });
|
|
497
671
|
}
|
|
@@ -511,19 +685,18 @@ export function registerTaskCommands(program) {
|
|
|
511
685
|
}
|
|
512
686
|
});
|
|
513
687
|
// kspec task patch <ref>
|
|
514
|
-
task
|
|
515
|
-
.
|
|
516
|
-
.
|
|
517
|
-
.option(
|
|
518
|
-
.option(
|
|
519
|
-
.option('--allow-unknown', 'Allow unknown fields (for extending format)')
|
|
688
|
+
markMutating(task.command("patch <ref>"))
|
|
689
|
+
.description("Update task with JSON data")
|
|
690
|
+
.option("--data <json>", "JSON object with fields to update")
|
|
691
|
+
.option("--dry-run", "Show what would change without writing")
|
|
692
|
+
.option("--allow-unknown", "Allow unknown fields (for extending format)")
|
|
520
693
|
.action(async (ref, options) => {
|
|
521
694
|
try {
|
|
522
695
|
const ctx = await initContext();
|
|
523
696
|
const tasks = await loadAllTasks(ctx);
|
|
524
697
|
const items = await loadAllItems(ctx);
|
|
525
698
|
// Load meta items for validation
|
|
526
|
-
const { loadMetaContext } = await import(
|
|
699
|
+
const { loadMetaContext } = await import("../../parser/meta.js");
|
|
527
700
|
const metaContext = await loadMetaContext(ctx);
|
|
528
701
|
const allMetaItems = [
|
|
529
702
|
...metaContext.agents,
|
|
@@ -544,7 +717,7 @@ export function registerTaskCommands(program) {
|
|
|
544
717
|
for await (const chunk of process.stdin) {
|
|
545
718
|
chunks.push(chunk);
|
|
546
719
|
}
|
|
547
|
-
jsonData = Buffer.concat(chunks).toString(
|
|
720
|
+
jsonData = Buffer.concat(chunks).toString("utf-8");
|
|
548
721
|
}
|
|
549
722
|
// Parse JSON
|
|
550
723
|
let patchData;
|
|
@@ -556,7 +729,7 @@ export function registerTaskCommands(program) {
|
|
|
556
729
|
process.exit(EXIT_CODES.ERROR);
|
|
557
730
|
}
|
|
558
731
|
// Validate against TaskInputSchema (partial)
|
|
559
|
-
const { TaskInputSchema } = await import(
|
|
732
|
+
const { TaskInputSchema } = await import("../../schema/index.js");
|
|
560
733
|
// Create a partial schema for validation
|
|
561
734
|
const partialSchema = options.allowUnknown
|
|
562
735
|
? TaskInputSchema.partial().passthrough()
|
|
@@ -573,7 +746,7 @@ export function registerTaskCommands(program) {
|
|
|
573
746
|
if (!options.allowUnknown) {
|
|
574
747
|
const knownFields = Object.keys(TaskInputSchema.shape);
|
|
575
748
|
const providedFields = Object.keys(patchData);
|
|
576
|
-
const unknownFields = providedFields.filter(f => !knownFields.includes(f));
|
|
749
|
+
const unknownFields = providedFields.filter((f) => !knownFields.includes(f));
|
|
577
750
|
if (unknownFields.length > 0) {
|
|
578
751
|
error(errors.validation.unknownFields(unknownFields));
|
|
579
752
|
process.exit(EXIT_CODES.ERROR);
|
|
@@ -584,17 +757,17 @@ export function registerTaskCommands(program) {
|
|
|
584
757
|
// Track changes for output
|
|
585
758
|
const changes = Object.keys(validatedPatch);
|
|
586
759
|
if (options.dryRun) {
|
|
587
|
-
info(
|
|
588
|
-
info(`Would update: ${changes.join(
|
|
760
|
+
info("Dry run - no changes will be written");
|
|
761
|
+
info(`Would update: ${changes.join(", ")}`);
|
|
589
762
|
output({ changes, updated: updatedTask }, () => {
|
|
590
|
-
console.log(`\nChanges: ${changes.join(
|
|
763
|
+
console.log(`\nChanges: ${changes.join(", ")}\n`);
|
|
591
764
|
return formatTaskDetails(updatedTask, index);
|
|
592
765
|
});
|
|
593
766
|
return;
|
|
594
767
|
}
|
|
595
768
|
await saveTask(ctx, updatedTask);
|
|
596
|
-
await commitIfShadow(ctx.shadow,
|
|
597
|
-
success(`Patched task: ${index.shortUlid(updatedTask._ulid)} (${changes.join(
|
|
769
|
+
await commitIfShadow(ctx.shadow, "task-patch", foundTask.slugs[0] || index.shortUlid(foundTask._ulid), changes.join(", "));
|
|
770
|
+
success(`Patched task: ${index.shortUlid(updatedTask._ulid)} (${changes.join(", ")})`, { task: updatedTask });
|
|
598
771
|
}
|
|
599
772
|
catch (err) {
|
|
600
773
|
error(errors.failures.patchTask, err);
|
|
@@ -602,10 +775,9 @@ export function registerTaskCommands(program) {
|
|
|
602
775
|
}
|
|
603
776
|
});
|
|
604
777
|
// kspec task start <ref>
|
|
605
|
-
task
|
|
606
|
-
.
|
|
607
|
-
.
|
|
608
|
-
.option('--no-sync', 'Skip syncing spec implementation status')
|
|
778
|
+
markMutating(task.command("start <ref>"))
|
|
779
|
+
.description("Start working on a task (pending|needs_work -> in_progress)")
|
|
780
|
+
.option("--no-sync", "Skip syncing spec implementation status")
|
|
609
781
|
.action(async (ref, options) => {
|
|
610
782
|
try {
|
|
611
783
|
const ctx = await initContext();
|
|
@@ -613,37 +785,40 @@ export function registerTaskCommands(program) {
|
|
|
613
785
|
const items = await loadAllItems(ctx);
|
|
614
786
|
const index = new ReferenceIndex(tasks, items);
|
|
615
787
|
const foundTask = resolveTaskRef(ref, tasks, index);
|
|
616
|
-
if (foundTask.status ===
|
|
617
|
-
warn(
|
|
788
|
+
if (foundTask.status === "in_progress") {
|
|
789
|
+
warn("Task is already in progress");
|
|
618
790
|
output(foundTask, () => formatTaskDetails(foundTask));
|
|
619
791
|
return;
|
|
620
792
|
}
|
|
621
|
-
if (foundTask.status !==
|
|
793
|
+
if (foundTask.status !== "pending" && foundTask.status !== "needs_work") {
|
|
622
794
|
error(errors.status.cannotStart(foundTask.status));
|
|
623
795
|
process.exit(EXIT_CODES.VALIDATION_FAILED); // Exit code 4 = invalid state
|
|
624
796
|
}
|
|
625
797
|
// Update status
|
|
626
798
|
const updatedTask = {
|
|
627
799
|
...foundTask,
|
|
628
|
-
status:
|
|
800
|
+
status: "in_progress",
|
|
629
801
|
started_at: new Date().toISOString(),
|
|
630
802
|
};
|
|
631
803
|
await saveTask(ctx, updatedTask);
|
|
632
|
-
await commitIfShadow(ctx.shadow,
|
|
633
|
-
success(`Started task: ${index.shortUlid(updatedTask._ulid)}`, {
|
|
804
|
+
await commitIfShadow(ctx.shadow, "task-start", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
|
|
805
|
+
success(`Started task: ${index.shortUlid(updatedTask._ulid)}`, {
|
|
806
|
+
task: updatedTask,
|
|
807
|
+
});
|
|
634
808
|
// Show spec context and AC guidance (suppressed in JSON mode)
|
|
635
809
|
if (!isJsonMode() && foundTask.spec_ref) {
|
|
636
810
|
const specResult = index.resolve(foundTask.spec_ref);
|
|
637
811
|
if (specResult.ok) {
|
|
638
|
-
const specItem = items.find(i => i._ulid === specResult.ulid);
|
|
812
|
+
const specItem = items.find((i) => i._ulid === specResult.ulid);
|
|
639
813
|
if (specItem) {
|
|
640
|
-
console.log(
|
|
641
|
-
console.log(
|
|
814
|
+
console.log("");
|
|
815
|
+
console.log("--- Spec Context ---");
|
|
642
816
|
console.log(`Implementing: ${specItem.title}`);
|
|
643
817
|
if (specItem.description) {
|
|
644
818
|
console.log(`\n${specItem.description}`);
|
|
645
819
|
}
|
|
646
|
-
if (specItem.acceptance_criteria &&
|
|
820
|
+
if (specItem.acceptance_criteria &&
|
|
821
|
+
specItem.acceptance_criteria.length > 0) {
|
|
647
822
|
console.log(`\nAcceptance Criteria (${specItem.acceptance_criteria.length}):`);
|
|
648
823
|
for (const ac of specItem.acceptance_criteria) {
|
|
649
824
|
console.log(` [${ac.id}]`);
|
|
@@ -651,21 +826,21 @@ export function registerTaskCommands(program) {
|
|
|
651
826
|
console.log(` When: ${ac.when}`);
|
|
652
827
|
console.log(` Then: ${ac.then}`);
|
|
653
828
|
}
|
|
654
|
-
console.log(
|
|
655
|
-
console.log(
|
|
829
|
+
console.log("");
|
|
830
|
+
console.log("Remember: Add test coverage for each AC and mark tests with // AC: @spec-ref ac-N");
|
|
656
831
|
}
|
|
657
|
-
console.log(
|
|
832
|
+
console.log("");
|
|
658
833
|
}
|
|
659
834
|
}
|
|
660
835
|
}
|
|
661
836
|
// Sync spec implementation status (unless --no-sync)
|
|
662
837
|
if (options.sync !== false && foundTask.spec_ref) {
|
|
663
|
-
const updatedTasks = tasks.map(t => t._ulid === updatedTask._ulid ? { ...t, ...updatedTask } : t);
|
|
838
|
+
const updatedTasks = tasks.map((t) => t._ulid === updatedTask._ulid ? { ...t, ...updatedTask } : t);
|
|
664
839
|
const syncResult = await syncSpecImplementationStatus(ctx, updatedTask, updatedTasks, items, index);
|
|
665
840
|
if (syncResult) {
|
|
666
841
|
info(`Synced spec "${syncResult.specTitle}" implementation: ${syncResult.previousStatus} -> ${syncResult.newStatus}`);
|
|
667
842
|
// Commit the spec status change
|
|
668
|
-
await commitIfShadow(ctx.shadow,
|
|
843
|
+
await commitIfShadow(ctx.shadow, "spec-sync", syncResult.specUlid.slice(0, 8), `${syncResult.previousStatus} -> ${syncResult.newStatus}`);
|
|
669
844
|
}
|
|
670
845
|
}
|
|
671
846
|
}
|
|
@@ -677,13 +852,17 @@ export function registerTaskCommands(program) {
|
|
|
677
852
|
// kspec task complete <ref> | --refs <refs...>
|
|
678
853
|
// AC: @multi-ref-batch ac-1 - Basic multi-ref syntax
|
|
679
854
|
// AC: @multi-ref-batch ac-2 - Backward compatibility
|
|
680
|
-
task
|
|
681
|
-
.
|
|
682
|
-
.
|
|
683
|
-
.option(
|
|
684
|
-
.option(
|
|
685
|
-
.option(
|
|
686
|
-
.option(
|
|
855
|
+
markMutating(task.command("complete [ref]"))
|
|
856
|
+
.description("Complete a task (pending_review -> completed)")
|
|
857
|
+
.option("--refs <refs...>", "Complete multiple tasks by ref")
|
|
858
|
+
.option("--reason <reason>", "Completion reason/notes")
|
|
859
|
+
.option("--skip-review", "Skip review requirement (requires --reason)")
|
|
860
|
+
.option("--force", "Force completion even if blocked (AC: @task-commands ac-1)")
|
|
861
|
+
.option("--no-sync", "Skip syncing spec implementation status")
|
|
862
|
+
.addHelpText("after", `
|
|
863
|
+
Examples:
|
|
864
|
+
$ kspec task complete @task-slug --reason "Merged in PR #123"
|
|
865
|
+
$ kspec task complete --refs @task1 @task2 --reason "Batch completion"`)
|
|
687
866
|
.action(async (ref, options) => {
|
|
688
867
|
try {
|
|
689
868
|
const ctx = await initContext();
|
|
@@ -709,44 +888,47 @@ export function registerTaskCommands(program) {
|
|
|
709
888
|
executeOperation: async (foundTask, { ctx, tasks, items, index, options }) => {
|
|
710
889
|
try {
|
|
711
890
|
// AC: @spec-completion-enforcement ac-6
|
|
712
|
-
if (foundTask.status ===
|
|
891
|
+
if (foundTask.status === "completed") {
|
|
713
892
|
return {
|
|
714
893
|
success: false,
|
|
715
894
|
error: errors.status.completeAlreadyCompleted,
|
|
716
895
|
};
|
|
717
896
|
}
|
|
897
|
+
// AC: @task-commands ac-1 - Allow --force to bypass blocked state
|
|
898
|
+
// Handle blocked task with --force before other checks
|
|
899
|
+
const forcingBlockedTask = foundTask.status === "blocked" && options.force;
|
|
718
900
|
// AC: @spec-completion-enforcement ac-7 - Allow skip-review bypass
|
|
719
|
-
if (!options.skipReview) {
|
|
901
|
+
if (!options.skipReview && !forcingBlockedTask) {
|
|
720
902
|
// AC: @spec-completion-enforcement ac-2
|
|
721
|
-
if (foundTask.status ===
|
|
903
|
+
if (foundTask.status === "in_progress") {
|
|
722
904
|
return {
|
|
723
905
|
success: false,
|
|
724
906
|
error: errors.status.completeRequiresReview,
|
|
725
907
|
};
|
|
726
908
|
}
|
|
727
909
|
// AC: @spec-completion-enforcement ac-3
|
|
728
|
-
if (foundTask.status ===
|
|
910
|
+
if (foundTask.status === "pending") {
|
|
729
911
|
return {
|
|
730
912
|
success: false,
|
|
731
913
|
error: errors.status.completeRequiresStart,
|
|
732
914
|
};
|
|
733
915
|
}
|
|
734
916
|
// AC: @spec-completion-enforcement ac-4
|
|
735
|
-
if (foundTask.status ===
|
|
917
|
+
if (foundTask.status === "blocked") {
|
|
736
918
|
return {
|
|
737
919
|
success: false,
|
|
738
920
|
error: errors.status.completeBlockedTask,
|
|
739
921
|
};
|
|
740
922
|
}
|
|
741
923
|
// AC: @spec-completion-enforcement ac-5
|
|
742
|
-
if (foundTask.status ===
|
|
924
|
+
if (foundTask.status === "cancelled") {
|
|
743
925
|
return {
|
|
744
926
|
success: false,
|
|
745
927
|
error: errors.status.completeCancelledTask,
|
|
746
928
|
};
|
|
747
929
|
}
|
|
748
930
|
// AC: @spec-completion-enforcement ac-1 - Only pending_review allowed
|
|
749
|
-
if (foundTask.status !==
|
|
931
|
+
if (foundTask.status !== "pending_review") {
|
|
750
932
|
return {
|
|
751
933
|
success: false,
|
|
752
934
|
error: errors.status.cannotComplete(foundTask.status),
|
|
@@ -758,44 +940,61 @@ export function registerTaskCommands(program) {
|
|
|
758
940
|
// AC: @spec-completion-enforcement ac-author
|
|
759
941
|
let taskNotes = foundTask.notes;
|
|
760
942
|
if (options.skipReview && options.reason) {
|
|
761
|
-
const skipNote = createNote(`Completed with --skip-review: ${options.reason}`, getAuthor());
|
|
943
|
+
const skipNote = createNote(`Completed with --skip-review: ${options.reason}`, getAuthor(ctx.config?.identity?.author));
|
|
762
944
|
taskNotes = [...taskNotes, skipNote];
|
|
763
945
|
}
|
|
946
|
+
// AC: @task-commands ac-1 - Document force completion of blocked task
|
|
947
|
+
if (forcingBlockedTask) {
|
|
948
|
+
const blockedBy = foundTask.blocked_by.join("; ");
|
|
949
|
+
const forceNote = createNote(`Completed with --force despite blocked state. Was blocked by: ${blockedBy || "(dependency-blocked)"}${options.reason ? `. Reason: ${options.reason}` : ""}`, getAuthor(ctx.config?.identity?.author));
|
|
950
|
+
taskNotes = [...taskNotes, forceNote];
|
|
951
|
+
}
|
|
764
952
|
// Update status
|
|
765
953
|
const updatedTask = {
|
|
766
954
|
...foundTask,
|
|
767
|
-
status:
|
|
955
|
+
status: "completed",
|
|
768
956
|
completed_at: now,
|
|
769
957
|
closed_reason: options.reason || null,
|
|
770
958
|
started_at: foundTask.started_at || now,
|
|
771
959
|
notes: taskNotes,
|
|
772
960
|
};
|
|
773
961
|
await saveTask(ctx, updatedTask);
|
|
774
|
-
await commitIfShadow(ctx.shadow,
|
|
962
|
+
await commitIfShadow(ctx.shadow, "task-complete", foundTask.slugs[0] || index.shortUlid(foundTask._ulid), options.reason);
|
|
775
963
|
// Sync spec implementation status (unless --no-sync)
|
|
776
964
|
if (options.sync !== false && foundTask.spec_ref) {
|
|
777
|
-
const updatedTasks = tasks.map(t => t._ulid === updatedTask._ulid ? { ...t, ...updatedTask } : t);
|
|
965
|
+
const updatedTasks = tasks.map((t) => t._ulid === updatedTask._ulid ? { ...t, ...updatedTask } : t);
|
|
778
966
|
const syncResult = await syncSpecImplementationStatus(ctx, updatedTask, updatedTasks, items, index);
|
|
779
967
|
if (syncResult && !isJsonMode()) {
|
|
780
968
|
info(`Synced spec "${syncResult.specTitle}" implementation: ${syncResult.previousStatus} -> ${syncResult.newStatus}`);
|
|
781
|
-
await commitIfShadow(ctx.shadow,
|
|
969
|
+
await commitIfShadow(ctx.shadow, "spec-sync", syncResult.specUlid.slice(0, 8), `${syncResult.previousStatus} -> ${syncResult.newStatus}`);
|
|
782
970
|
}
|
|
783
971
|
}
|
|
784
972
|
// Show AC reminder for single-ref mode only (not in batch)
|
|
785
973
|
if (!options.refs && foundTask.spec_ref && !isJsonMode()) {
|
|
786
974
|
const specResult = index.resolve(foundTask.spec_ref);
|
|
787
975
|
if (specResult.ok && specResult.item) {
|
|
788
|
-
const specItem = items.find(i => i._ulid === specResult.ulid);
|
|
789
|
-
if (specItem
|
|
976
|
+
const specItem = items.find((i) => i._ulid === specResult.ulid);
|
|
977
|
+
if (specItem?.acceptance_criteria &&
|
|
978
|
+
specItem.acceptance_criteria.length > 0) {
|
|
790
979
|
const count = specItem.acceptance_criteria.length;
|
|
791
|
-
console.log(`\n⚠ Linked spec ${foundTask.spec_ref} has ${count} acceptance criteri${count === 1 ?
|
|
980
|
+
console.log(`\n⚠ Linked spec ${foundTask.spec_ref} has ${count} acceptance criteri${count === 1 ? "on" : "a"} - verify they are covered\n`);
|
|
792
981
|
}
|
|
793
982
|
}
|
|
794
983
|
}
|
|
984
|
+
// AC: @task-commands ac-1 - Show warning when force-completing blocked task
|
|
985
|
+
let warningMsg;
|
|
986
|
+
if (forcingBlockedTask) {
|
|
987
|
+
const blockedBy = foundTask.blocked_by.join("; ");
|
|
988
|
+
warningMsg = `Task was blocked by: ${blockedBy || "(dependency-blocked)"}`;
|
|
989
|
+
if (!isJsonMode()) {
|
|
990
|
+
warn(warningMsg);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
795
993
|
return {
|
|
796
994
|
success: true,
|
|
797
995
|
message: `Completed task: ${index.shortUlid(updatedTask._ulid)}`,
|
|
798
996
|
data: updatedTask,
|
|
997
|
+
...(forcingBlockedTask && { warning: warningMsg }),
|
|
799
998
|
};
|
|
800
999
|
}
|
|
801
1000
|
catch (err) {
|
|
@@ -808,9 +1007,12 @@ export function registerTaskCommands(program) {
|
|
|
808
1007
|
getUlid: (task) => task._ulid,
|
|
809
1008
|
});
|
|
810
1009
|
// AC: @multi-ref-batch ac-5, ac-6
|
|
811
|
-
formatBatchOutput(result,
|
|
1010
|
+
formatBatchOutput(result, "Complete");
|
|
812
1011
|
// Show commit guidance for single-ref mode only
|
|
813
|
-
if (!options.refs &&
|
|
1012
|
+
if (!options.refs &&
|
|
1013
|
+
result.success &&
|
|
1014
|
+
result.results.length === 1 &&
|
|
1015
|
+
!isJsonMode()) {
|
|
814
1016
|
const taskData = result.results[0].data;
|
|
815
1017
|
if (taskData) {
|
|
816
1018
|
const guidance = formatCommitGuidance(taskData);
|
|
@@ -825,9 +1027,8 @@ export function registerTaskCommands(program) {
|
|
|
825
1027
|
});
|
|
826
1028
|
// kspec task submit <ref>
|
|
827
1029
|
// Transitions in_progress → pending_review (code done, awaiting merge)
|
|
828
|
-
task
|
|
829
|
-
.
|
|
830
|
-
.description('Submit task for review (transitions to pending_review)')
|
|
1030
|
+
markMutating(task.command("submit <ref>"))
|
|
1031
|
+
.description("Submit task for review (transitions to pending_review)")
|
|
831
1032
|
.action(async (ref) => {
|
|
832
1033
|
try {
|
|
833
1034
|
const ctx = await initContext();
|
|
@@ -835,16 +1036,16 @@ export function registerTaskCommands(program) {
|
|
|
835
1036
|
const items = await loadAllItems(ctx);
|
|
836
1037
|
const index = new ReferenceIndex(tasks, items);
|
|
837
1038
|
const foundTask = resolveTaskRef(ref, tasks, index);
|
|
838
|
-
if (foundTask.status !==
|
|
1039
|
+
if (foundTask.status !== "in_progress") {
|
|
839
1040
|
error(`Cannot submit task with status: ${foundTask.status}. Task must be in_progress.`);
|
|
840
1041
|
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
841
1042
|
}
|
|
842
1043
|
const updatedTask = {
|
|
843
1044
|
...foundTask,
|
|
844
|
-
status:
|
|
1045
|
+
status: "pending_review",
|
|
845
1046
|
};
|
|
846
1047
|
await saveTask(ctx, updatedTask);
|
|
847
|
-
await commitIfShadow(ctx.shadow,
|
|
1048
|
+
await commitIfShadow(ctx.shadow, "task-submit", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
|
|
848
1049
|
success(`Submitted task for review: ${index.shortUlid(updatedTask._ulid)}`, { task: updatedTask });
|
|
849
1050
|
}
|
|
850
1051
|
catch (err) {
|
|
@@ -852,11 +1053,44 @@ export function registerTaskCommands(program) {
|
|
|
852
1053
|
process.exit(EXIT_CODES.ERROR);
|
|
853
1054
|
}
|
|
854
1055
|
});
|
|
1056
|
+
// kspec task needs-work <ref>
|
|
1057
|
+
// Reviewer kicks back a task for worker to fix
|
|
1058
|
+
markMutating(task.command("needs-work <ref>"))
|
|
1059
|
+
.description("Kick task back to worker for fixes (pending_review -> needs_work)")
|
|
1060
|
+
.requiredOption("--reason <reason>", "Description of issues found")
|
|
1061
|
+
.action(async (ref, options) => {
|
|
1062
|
+
try {
|
|
1063
|
+
const ctx = await initContext();
|
|
1064
|
+
const tasks = await loadAllTasks(ctx);
|
|
1065
|
+
const items = await loadAllItems(ctx);
|
|
1066
|
+
const index = new ReferenceIndex(tasks, items);
|
|
1067
|
+
const foundTask = resolveTaskRef(ref, tasks, index);
|
|
1068
|
+
if (foundTask.status !== "pending_review") {
|
|
1069
|
+
error(`Cannot transition task to needs_work from status: ${foundTask.status}. Task must be pending_review.`);
|
|
1070
|
+
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
1071
|
+
}
|
|
1072
|
+
// Track fix cycle count from existing kickback notes
|
|
1073
|
+
const existingKickbacks = foundTask.notes.filter((n) => n.content.includes("[FIX_CYCLE:")).length;
|
|
1074
|
+
const cycleNumber = existingKickbacks + 1;
|
|
1075
|
+
const note = createNote(`[FIX_CYCLE: ${cycleNumber}] Review findings: ${options.reason}`, getAuthor(ctx.config?.identity?.author));
|
|
1076
|
+
const updatedTask = {
|
|
1077
|
+
...foundTask,
|
|
1078
|
+
status: "needs_work",
|
|
1079
|
+
notes: [...foundTask.notes, note],
|
|
1080
|
+
};
|
|
1081
|
+
await saveTask(ctx, updatedTask);
|
|
1082
|
+
await commitIfShadow(ctx.shadow, "task-needs-work", foundTask.slugs[0] || index.shortUlid(foundTask._ulid), `cycle ${cycleNumber}`);
|
|
1083
|
+
success(`Kicked back task: ${index.shortUlid(updatedTask._ulid)} (fix cycle ${cycleNumber})`, { task: updatedTask });
|
|
1084
|
+
}
|
|
1085
|
+
catch (err) {
|
|
1086
|
+
error("Failed to transition task to needs_work", err);
|
|
1087
|
+
process.exit(EXIT_CODES.ERROR);
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
855
1090
|
// kspec task block <ref>
|
|
856
|
-
task
|
|
857
|
-
.
|
|
858
|
-
.
|
|
859
|
-
.requiredOption('--reason <reason>', 'Reason for blocking')
|
|
1091
|
+
markMutating(task.command("block <ref>"))
|
|
1092
|
+
.description("Block a task")
|
|
1093
|
+
.requiredOption("--reason <reason>", "Reason for blocking")
|
|
860
1094
|
.action(async (ref, options) => {
|
|
861
1095
|
try {
|
|
862
1096
|
const ctx = await initContext();
|
|
@@ -864,18 +1098,21 @@ export function registerTaskCommands(program) {
|
|
|
864
1098
|
const items = await loadAllItems(ctx);
|
|
865
1099
|
const index = new ReferenceIndex(tasks, items);
|
|
866
1100
|
const foundTask = resolveTaskRef(ref, tasks, index);
|
|
867
|
-
if (foundTask.status ===
|
|
1101
|
+
if (foundTask.status === "completed" ||
|
|
1102
|
+
foundTask.status === "cancelled") {
|
|
868
1103
|
error(errors.status.cannotBlock(foundTask.status));
|
|
869
1104
|
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
870
1105
|
}
|
|
871
1106
|
const updatedTask = {
|
|
872
1107
|
...foundTask,
|
|
873
|
-
status:
|
|
1108
|
+
status: "blocked",
|
|
874
1109
|
blocked_by: [...foundTask.blocked_by, options.reason],
|
|
875
1110
|
};
|
|
876
1111
|
await saveTask(ctx, updatedTask);
|
|
877
|
-
await commitIfShadow(ctx.shadow,
|
|
878
|
-
success(`Blocked task: ${index.shortUlid(updatedTask._ulid)}`, {
|
|
1112
|
+
await commitIfShadow(ctx.shadow, "task-block", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
|
|
1113
|
+
success(`Blocked task: ${index.shortUlid(updatedTask._ulid)}`, {
|
|
1114
|
+
task: updatedTask,
|
|
1115
|
+
});
|
|
879
1116
|
}
|
|
880
1117
|
catch (err) {
|
|
881
1118
|
error(errors.failures.blockTask, err);
|
|
@@ -883,9 +1120,8 @@ export function registerTaskCommands(program) {
|
|
|
883
1120
|
}
|
|
884
1121
|
});
|
|
885
1122
|
// kspec task unblock <ref>
|
|
886
|
-
task
|
|
887
|
-
.
|
|
888
|
-
.description('Unblock a task')
|
|
1123
|
+
markMutating(task.command("unblock <ref>"))
|
|
1124
|
+
.description("Unblock a task")
|
|
889
1125
|
.action(async (ref) => {
|
|
890
1126
|
try {
|
|
891
1127
|
const ctx = await initContext();
|
|
@@ -893,18 +1129,20 @@ export function registerTaskCommands(program) {
|
|
|
893
1129
|
const items = await loadAllItems(ctx);
|
|
894
1130
|
const index = new ReferenceIndex(tasks, items);
|
|
895
1131
|
const foundTask = resolveTaskRef(ref, tasks, index);
|
|
896
|
-
if (foundTask.status !==
|
|
897
|
-
warn(
|
|
1132
|
+
if (foundTask.status !== "blocked") {
|
|
1133
|
+
warn("Task is not blocked");
|
|
898
1134
|
return;
|
|
899
1135
|
}
|
|
900
1136
|
const updatedTask = {
|
|
901
1137
|
...foundTask,
|
|
902
|
-
status:
|
|
1138
|
+
status: "pending",
|
|
903
1139
|
blocked_by: [],
|
|
904
1140
|
};
|
|
905
1141
|
await saveTask(ctx, updatedTask);
|
|
906
|
-
await commitIfShadow(ctx.shadow,
|
|
907
|
-
success(`Unblocked task: ${index.shortUlid(updatedTask._ulid)}`, {
|
|
1142
|
+
await commitIfShadow(ctx.shadow, "task-unblock", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
|
|
1143
|
+
success(`Unblocked task: ${index.shortUlid(updatedTask._ulid)}`, {
|
|
1144
|
+
task: updatedTask,
|
|
1145
|
+
});
|
|
908
1146
|
}
|
|
909
1147
|
catch (err) {
|
|
910
1148
|
error(errors.failures.unblockTask, err);
|
|
@@ -913,11 +1151,14 @@ export function registerTaskCommands(program) {
|
|
|
913
1151
|
});
|
|
914
1152
|
// kspec task cancel <ref> | --refs <refs...>
|
|
915
1153
|
// AC: @multi-ref-batch ac-1, ac-2
|
|
916
|
-
task
|
|
917
|
-
.
|
|
918
|
-
.
|
|
919
|
-
.option(
|
|
920
|
-
.
|
|
1154
|
+
markMutating(task.command("cancel [ref]"))
|
|
1155
|
+
.description("Cancel a task")
|
|
1156
|
+
.option("--refs <refs...>", "Cancel multiple tasks by ref")
|
|
1157
|
+
.option("--reason <reason>", "Cancellation reason")
|
|
1158
|
+
.addHelpText("after", `
|
|
1159
|
+
Examples:
|
|
1160
|
+
$ kspec task cancel @task-slug --reason "No longer needed"
|
|
1161
|
+
$ kspec task cancel --refs @task1 @task2 --reason "Superseded by @new-task"`)
|
|
921
1162
|
.action(async (ref, options) => {
|
|
922
1163
|
try {
|
|
923
1164
|
const ctx = await initContext();
|
|
@@ -936,7 +1177,8 @@ export function registerTaskCommands(program) {
|
|
|
936
1177
|
},
|
|
937
1178
|
executeOperation: async (foundTask, { ctx, index, options }) => {
|
|
938
1179
|
try {
|
|
939
|
-
if (foundTask.status ===
|
|
1180
|
+
if (foundTask.status === "completed" ||
|
|
1181
|
+
foundTask.status === "cancelled") {
|
|
940
1182
|
return {
|
|
941
1183
|
success: false,
|
|
942
1184
|
error: `Task is already ${foundTask.status}`,
|
|
@@ -944,11 +1186,11 @@ export function registerTaskCommands(program) {
|
|
|
944
1186
|
}
|
|
945
1187
|
const updatedTask = {
|
|
946
1188
|
...foundTask,
|
|
947
|
-
status:
|
|
1189
|
+
status: "cancelled",
|
|
948
1190
|
closed_reason: options.reason || null,
|
|
949
1191
|
};
|
|
950
1192
|
await saveTask(ctx, updatedTask);
|
|
951
|
-
await commitIfShadow(ctx.shadow,
|
|
1193
|
+
await commitIfShadow(ctx.shadow, "task-cancel", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
|
|
952
1194
|
return {
|
|
953
1195
|
success: true,
|
|
954
1196
|
message: `Cancelled task: ${index.shortUlid(updatedTask._ulid)}`,
|
|
@@ -964,7 +1206,7 @@ export function registerTaskCommands(program) {
|
|
|
964
1206
|
},
|
|
965
1207
|
getUlid: (task) => task._ulid,
|
|
966
1208
|
});
|
|
967
|
-
formatBatchOutput(result,
|
|
1209
|
+
formatBatchOutput(result, "Cancel");
|
|
968
1210
|
}
|
|
969
1211
|
catch (err) {
|
|
970
1212
|
error(errors.failures.cancelTask, err);
|
|
@@ -973,9 +1215,8 @@ export function registerTaskCommands(program) {
|
|
|
973
1215
|
});
|
|
974
1216
|
// kspec task reset <ref>
|
|
975
1217
|
// AC: @spec-task-reset ac-1, ac-2, ac-3, ac-4, ac-5, ac-6
|
|
976
|
-
task
|
|
977
|
-
.
|
|
978
|
-
.description('Reset a task to pending state')
|
|
1218
|
+
markMutating(task.command("reset <ref>"))
|
|
1219
|
+
.description("Reset a task to pending state")
|
|
979
1220
|
.action(async (ref) => {
|
|
980
1221
|
try {
|
|
981
1222
|
const ctx = await initContext();
|
|
@@ -984,72 +1225,81 @@ export function registerTaskCommands(program) {
|
|
|
984
1225
|
const index = new ReferenceIndex(tasks, items);
|
|
985
1226
|
const foundTask = resolveTaskRef(ref, tasks, index);
|
|
986
1227
|
// AC: @spec-task-reset ac-2 - Error if already pending
|
|
987
|
-
if (foundTask.status ===
|
|
988
|
-
error(
|
|
1228
|
+
if (foundTask.status === "pending") {
|
|
1229
|
+
error("Task is already pending");
|
|
989
1230
|
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
990
1231
|
}
|
|
991
1232
|
// Track previous status and reason for note (AC-4)
|
|
992
1233
|
const previousStatus = foundTask.status;
|
|
993
|
-
const hadCancelReason = foundTask.closed_reason && foundTask.status ===
|
|
994
|
-
const cancelReasonText = hadCancelReason
|
|
1234
|
+
const hadCancelReason = foundTask.closed_reason && foundTask.status === "cancelled";
|
|
1235
|
+
const cancelReasonText = hadCancelReason
|
|
1236
|
+
? ` (was cancelled: ${foundTask.closed_reason})`
|
|
1237
|
+
: "";
|
|
995
1238
|
// AC: @spec-task-reset ac-1 - Reset to pending, clear completion-related fields
|
|
996
1239
|
const clearedFields = [];
|
|
997
1240
|
const updatedTask = {
|
|
998
1241
|
...foundTask,
|
|
999
|
-
status:
|
|
1242
|
+
status: "pending",
|
|
1000
1243
|
};
|
|
1001
1244
|
// Clear timestamps and reasons based on previous status
|
|
1002
|
-
if (foundTask.completed_at !== undefined &&
|
|
1245
|
+
if (foundTask.completed_at !== undefined &&
|
|
1246
|
+
foundTask.completed_at !== null) {
|
|
1003
1247
|
updatedTask.completed_at = null;
|
|
1004
|
-
clearedFields.push(
|
|
1248
|
+
clearedFields.push("completed_at");
|
|
1005
1249
|
}
|
|
1006
|
-
if (foundTask.started_at !== undefined &&
|
|
1250
|
+
if (foundTask.started_at !== undefined &&
|
|
1251
|
+
foundTask.started_at !== null) {
|
|
1007
1252
|
updatedTask.started_at = null;
|
|
1008
|
-
clearedFields.push(
|
|
1253
|
+
clearedFields.push("started_at");
|
|
1009
1254
|
}
|
|
1010
|
-
if (foundTask.closed_reason !== undefined &&
|
|
1255
|
+
if (foundTask.closed_reason !== undefined &&
|
|
1256
|
+
foundTask.closed_reason !== null) {
|
|
1011
1257
|
updatedTask.closed_reason = null;
|
|
1012
|
-
clearedFields.push(
|
|
1258
|
+
clearedFields.push("closed_reason");
|
|
1013
1259
|
}
|
|
1014
1260
|
if (foundTask.blocked_by.length > 0) {
|
|
1015
1261
|
updatedTask.blocked_by = [];
|
|
1016
|
-
clearedFields.push(
|
|
1262
|
+
clearedFields.push("blocked_by");
|
|
1017
1263
|
}
|
|
1018
1264
|
// AC: @spec-task-reset ac-4 - Add note documenting the reset
|
|
1019
1265
|
// AC: @spec-task-reset ac-author
|
|
1020
1266
|
const noteContent = `Reset from ${previousStatus} to pending${cancelReasonText}`;
|
|
1021
|
-
const note = createNote(noteContent, getAuthor());
|
|
1267
|
+
const note = createNote(noteContent, getAuthor(ctx.config?.identity?.author));
|
|
1022
1268
|
updatedTask.notes = [...updatedTask.notes, note];
|
|
1023
1269
|
await saveTask(ctx, updatedTask);
|
|
1024
1270
|
// AC: @spec-task-reset ac-3 - Shadow commit with message task-reset
|
|
1025
|
-
await commitIfShadow(ctx.shadow,
|
|
1271
|
+
await commitIfShadow(ctx.shadow, "task-reset", foundTask.slugs[0] || index.shortUlid(foundTask._ulid), `from ${previousStatus}`);
|
|
1026
1272
|
// AC: @spec-task-reset ac-6 - JSON output includes previous_status, new_status, cleared_fields
|
|
1027
1273
|
const jsonOutput = {
|
|
1028
1274
|
task: updatedTask,
|
|
1029
1275
|
previous_status: previousStatus,
|
|
1030
|
-
new_status:
|
|
1276
|
+
new_status: "pending",
|
|
1031
1277
|
cleared_fields: clearedFields,
|
|
1032
1278
|
};
|
|
1033
1279
|
output(jsonOutput, () => {
|
|
1034
1280
|
success(`Reset task: ${index.shortUlid(updatedTask._ulid)} (${previousStatus} → pending)`, undefined);
|
|
1035
1281
|
if (clearedFields.length > 0) {
|
|
1036
|
-
info(`Cleared fields: ${clearedFields.join(
|
|
1282
|
+
info(`Cleared fields: ${clearedFields.join(", ")}`);
|
|
1037
1283
|
}
|
|
1038
1284
|
});
|
|
1039
1285
|
}
|
|
1040
1286
|
catch (err) {
|
|
1041
|
-
error(
|
|
1287
|
+
error("Failed to reset task", err);
|
|
1042
1288
|
process.exit(EXIT_CODES.ERROR);
|
|
1043
1289
|
}
|
|
1044
1290
|
});
|
|
1045
1291
|
// kspec task delete <ref> | --refs <refs...>
|
|
1046
1292
|
// AC: @multi-ref-batch ac-1, ac-2
|
|
1047
|
-
task
|
|
1048
|
-
.
|
|
1049
|
-
.
|
|
1050
|
-
.option(
|
|
1051
|
-
.option(
|
|
1052
|
-
.
|
|
1293
|
+
markMutating(task.command("delete [ref]"))
|
|
1294
|
+
.description("Delete a task permanently")
|
|
1295
|
+
.option("--refs <refs...>", "Delete multiple tasks by ref")
|
|
1296
|
+
.option("--force", "Skip confirmation (required for --refs)")
|
|
1297
|
+
.option("--dry-run", "Show what would be deleted without deleting")
|
|
1298
|
+
.addHelpText("after", `
|
|
1299
|
+
Examples:
|
|
1300
|
+
$ kspec task delete @task-slug
|
|
1301
|
+
$ kspec task delete --refs @task1 @task2 --force
|
|
1302
|
+
$ kspec task delete --refs @task1 @task2 --dry-run`)
|
|
1053
1303
|
.action(async (ref, options) => {
|
|
1054
1304
|
try {
|
|
1055
1305
|
const ctx = await initContext();
|
|
@@ -1057,8 +1307,11 @@ export function registerTaskCommands(program) {
|
|
|
1057
1307
|
const items = await loadAllItems(ctx);
|
|
1058
1308
|
const index = new ReferenceIndex(tasks, items);
|
|
1059
1309
|
// For batch mode (--refs), require --force
|
|
1060
|
-
if (options.refs &&
|
|
1061
|
-
|
|
1310
|
+
if (options.refs &&
|
|
1311
|
+
options.refs.length > 0 &&
|
|
1312
|
+
!options.force &&
|
|
1313
|
+
!options.dryRun) {
|
|
1314
|
+
error("Batch delete requires --force flag");
|
|
1062
1315
|
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
1063
1316
|
}
|
|
1064
1317
|
const result = await executeBatchOperation({
|
|
@@ -1082,7 +1335,7 @@ export function registerTaskCommands(program) {
|
|
|
1082
1335
|
}
|
|
1083
1336
|
// For single-ref mode (not --refs), prompt for confirmation unless --force
|
|
1084
1337
|
if (!options.refs && !options.force) {
|
|
1085
|
-
const readline = await import(
|
|
1338
|
+
const readline = await import("node:readline");
|
|
1086
1339
|
const rl = readline.createInterface({
|
|
1087
1340
|
input: process.stdin,
|
|
1088
1341
|
output: process.stdout,
|
|
@@ -1091,15 +1344,15 @@ export function registerTaskCommands(program) {
|
|
|
1091
1344
|
rl.question(`Delete task "${taskDisplay}"? [y/N] `, resolve);
|
|
1092
1345
|
});
|
|
1093
1346
|
rl.close();
|
|
1094
|
-
if (answer.toLowerCase() !==
|
|
1347
|
+
if (answer.toLowerCase() !== "y") {
|
|
1095
1348
|
return {
|
|
1096
1349
|
success: false,
|
|
1097
|
-
error:
|
|
1350
|
+
error: "Deletion cancelled by user",
|
|
1098
1351
|
};
|
|
1099
1352
|
}
|
|
1100
1353
|
}
|
|
1101
1354
|
await deleteTask(ctx, foundTask);
|
|
1102
|
-
await commitIfShadow(ctx.shadow,
|
|
1355
|
+
await commitIfShadow(ctx.shadow, "task-delete", foundTask.slugs[0] || index.shortUlid(foundTask._ulid), foundTask.title);
|
|
1103
1356
|
return {
|
|
1104
1357
|
success: true,
|
|
1105
1358
|
message: `Deleted task: ${taskDisplay}`,
|
|
@@ -1114,7 +1367,7 @@ export function registerTaskCommands(program) {
|
|
|
1114
1367
|
},
|
|
1115
1368
|
getUlid: (task) => task._ulid,
|
|
1116
1369
|
});
|
|
1117
|
-
formatBatchOutput(result,
|
|
1370
|
+
formatBatchOutput(result, "Delete");
|
|
1118
1371
|
}
|
|
1119
1372
|
catch (err) {
|
|
1120
1373
|
error(errors.failures.deleteTask, err);
|
|
@@ -1122,11 +1375,10 @@ export function registerTaskCommands(program) {
|
|
|
1122
1375
|
}
|
|
1123
1376
|
});
|
|
1124
1377
|
// kspec task note <ref> <message>
|
|
1125
|
-
task
|
|
1126
|
-
.
|
|
1127
|
-
.
|
|
1128
|
-
.option(
|
|
1129
|
-
.option('--supersedes <ulid>', 'ULID of note this supersedes')
|
|
1378
|
+
markMutating(task.command("note <ref> <message>"))
|
|
1379
|
+
.description("Add a note to a task")
|
|
1380
|
+
.option("--author <author>", "Note author")
|
|
1381
|
+
.option("--supersedes <ulid>", "ULID of note this supersedes")
|
|
1130
1382
|
.action(async (ref, message, options) => {
|
|
1131
1383
|
try {
|
|
1132
1384
|
const ctx = await initContext();
|
|
@@ -1140,11 +1392,13 @@ export function registerTaskCommands(program) {
|
|
|
1140
1392
|
notes: [...foundTask.notes, note],
|
|
1141
1393
|
};
|
|
1142
1394
|
await saveTask(ctx, updatedTask);
|
|
1143
|
-
await commitIfShadow(ctx.shadow,
|
|
1144
|
-
success(`Added note to task: ${index.shortUlid(updatedTask._ulid)}`, {
|
|
1395
|
+
await commitIfShadow(ctx.shadow, "task-note", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
|
|
1396
|
+
success(`Added note to task: ${index.shortUlid(updatedTask._ulid)}`, {
|
|
1397
|
+
note,
|
|
1398
|
+
});
|
|
1145
1399
|
// Proactive alignment guidance for tasks with spec_ref
|
|
1146
1400
|
if (foundTask.spec_ref) {
|
|
1147
|
-
console.log(
|
|
1401
|
+
console.log("");
|
|
1148
1402
|
console.log(alignmentCheck.header);
|
|
1149
1403
|
console.log(alignmentCheck.beyondSpec);
|
|
1150
1404
|
console.log(alignmentCheck.updateSpec(foundTask.spec_ref));
|
|
@@ -1153,8 +1407,9 @@ export function registerTaskCommands(program) {
|
|
|
1153
1407
|
const specResult = index.resolve(foundTask.spec_ref);
|
|
1154
1408
|
if (specResult.ok && specResult.item) {
|
|
1155
1409
|
const specItem = specResult.item;
|
|
1156
|
-
if (specItem.acceptance_criteria &&
|
|
1157
|
-
|
|
1410
|
+
if (specItem.acceptance_criteria &&
|
|
1411
|
+
specItem.acceptance_criteria.length > 0) {
|
|
1412
|
+
console.log("");
|
|
1158
1413
|
console.log(alignmentCheck.testCoverage(specItem.acceptance_criteria.length));
|
|
1159
1414
|
}
|
|
1160
1415
|
}
|
|
@@ -1167,8 +1422,8 @@ export function registerTaskCommands(program) {
|
|
|
1167
1422
|
});
|
|
1168
1423
|
// kspec task notes <ref>
|
|
1169
1424
|
task
|
|
1170
|
-
.command(
|
|
1171
|
-
.description(
|
|
1425
|
+
.command("notes <ref>")
|
|
1426
|
+
.description("Show notes for a task")
|
|
1172
1427
|
.action(async (ref) => {
|
|
1173
1428
|
try {
|
|
1174
1429
|
const ctx = await initContext();
|
|
@@ -1178,14 +1433,14 @@ export function registerTaskCommands(program) {
|
|
|
1178
1433
|
const foundTask = resolveTaskRef(ref, tasks, index);
|
|
1179
1434
|
output(foundTask.notes, () => {
|
|
1180
1435
|
if (foundTask.notes.length === 0) {
|
|
1181
|
-
console.log(
|
|
1436
|
+
console.log("No notes");
|
|
1182
1437
|
}
|
|
1183
1438
|
else {
|
|
1184
1439
|
for (const note of foundTask.notes) {
|
|
1185
|
-
const author = note.author ||
|
|
1440
|
+
const author = note.author || "unknown";
|
|
1186
1441
|
console.log(`[${note.created_at}] ${author}:`);
|
|
1187
1442
|
console.log(note.content);
|
|
1188
|
-
console.log(
|
|
1443
|
+
console.log("");
|
|
1189
1444
|
}
|
|
1190
1445
|
}
|
|
1191
1446
|
});
|
|
@@ -1197,8 +1452,8 @@ export function registerTaskCommands(program) {
|
|
|
1197
1452
|
});
|
|
1198
1453
|
// kspec task review <ref>
|
|
1199
1454
|
task
|
|
1200
|
-
.command(
|
|
1201
|
-
.description(
|
|
1455
|
+
.command("review <ref>")
|
|
1456
|
+
.description("Get task context for review (task details, spec, ACs, git diff)")
|
|
1202
1457
|
.action(async (ref) => {
|
|
1203
1458
|
try {
|
|
1204
1459
|
const ctx = await initContext();
|
|
@@ -1207,42 +1462,7 @@ export function registerTaskCommands(program) {
|
|
|
1207
1462
|
const index = new ReferenceIndex(tasks, items);
|
|
1208
1463
|
const foundTask = resolveTaskRef(ref, tasks, index);
|
|
1209
1464
|
// Import getDiffSince from utils
|
|
1210
|
-
const { getDiffSince } = await import(
|
|
1211
|
-
// Import scanTestCoverage (we'll need to export it from validate.ts)
|
|
1212
|
-
// For now, duplicate the logic here
|
|
1213
|
-
const scanTestCoverage = async (rootDir) => {
|
|
1214
|
-
const coveredACs = new Set();
|
|
1215
|
-
const testsDir = path.join(rootDir, 'tests');
|
|
1216
|
-
const fs = await import('node:fs/promises');
|
|
1217
|
-
try {
|
|
1218
|
-
await fs.access(testsDir);
|
|
1219
|
-
const files = await fs.readdir(testsDir);
|
|
1220
|
-
const testFiles = files.filter(f => f.endsWith('.test.ts') || f.endsWith('.test.js'));
|
|
1221
|
-
for (const file of testFiles) {
|
|
1222
|
-
const filePath = path.join(testsDir, file);
|
|
1223
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
1224
|
-
const acPattern = /\/\/\s*AC:\s*(@[\w-]+)(?:\s+(ac-\d+(?:\s*,\s*ac-\d+)*))?/g;
|
|
1225
|
-
let match;
|
|
1226
|
-
while ((match = acPattern.exec(content)) !== null) {
|
|
1227
|
-
const specRef = match[1];
|
|
1228
|
-
const acList = match[2];
|
|
1229
|
-
if (acList) {
|
|
1230
|
-
const acs = acList.split(',').map(ac => ac.trim());
|
|
1231
|
-
for (const ac of acs) {
|
|
1232
|
-
coveredACs.add(`${specRef} ${ac}`);
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
else {
|
|
1236
|
-
coveredACs.add(specRef);
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
catch (err) {
|
|
1242
|
-
// Tests directory doesn't exist or can't be read
|
|
1243
|
-
}
|
|
1244
|
-
return coveredACs;
|
|
1245
|
-
};
|
|
1465
|
+
const { getDiffSince } = await import("../../utils/index.js");
|
|
1246
1466
|
// Gather review context
|
|
1247
1467
|
const reviewContext = {
|
|
1248
1468
|
task: foundTask,
|
|
@@ -1254,10 +1474,11 @@ export function registerTaskCommands(program) {
|
|
|
1254
1474
|
if (foundTask.spec_ref) {
|
|
1255
1475
|
const specResult = index.resolve(foundTask.spec_ref);
|
|
1256
1476
|
if (specResult.ok) {
|
|
1257
|
-
const specItem = items.find(i => i._ulid === specResult.ulid);
|
|
1477
|
+
const specItem = items.find((i) => i._ulid === specResult.ulid);
|
|
1258
1478
|
reviewContext.spec = specItem || null;
|
|
1259
1479
|
// Check test coverage for ACs if spec has them
|
|
1260
|
-
if (specItem
|
|
1480
|
+
if (specItem?.acceptance_criteria &&
|
|
1481
|
+
specItem.acceptance_criteria.length > 0) {
|
|
1261
1482
|
const coveredACs = await scanTestCoverage(ctx.rootDir);
|
|
1262
1483
|
const covered = [];
|
|
1263
1484
|
const uncovered = [];
|
|
@@ -1270,7 +1491,7 @@ export function registerTaskCommands(program) {
|
|
|
1270
1491
|
}
|
|
1271
1492
|
possibleRefs.push(`@${specItem._ulid.slice(0, 8)} ${ac.id}`);
|
|
1272
1493
|
possibleRefs.push(`@${specItem._ulid.slice(0, 8)}`);
|
|
1273
|
-
const isCovered = possibleRefs.some(ref => coveredACs.has(ref));
|
|
1494
|
+
const isCovered = possibleRefs.some((ref) => coveredACs.has(ref));
|
|
1274
1495
|
if (isCovered) {
|
|
1275
1496
|
covered.push(ac.id);
|
|
1276
1497
|
}
|
|
@@ -1288,29 +1509,32 @@ export function registerTaskCommands(program) {
|
|
|
1288
1509
|
reviewContext.diff = getDiffSince(startedDate, ctx.rootDir);
|
|
1289
1510
|
}
|
|
1290
1511
|
output(reviewContext, () => {
|
|
1291
|
-
console.log(
|
|
1292
|
-
console.log(
|
|
1293
|
-
console.log(
|
|
1512
|
+
console.log("=".repeat(60));
|
|
1513
|
+
console.log("Task Review Context");
|
|
1514
|
+
console.log("=".repeat(60));
|
|
1294
1515
|
console.log();
|
|
1295
1516
|
// Task details
|
|
1296
|
-
console.log(
|
|
1297
|
-
console.log(
|
|
1517
|
+
console.log("TASK DETAILS");
|
|
1518
|
+
console.log("-".repeat(60));
|
|
1298
1519
|
console.log(formatTaskDetails(foundTask, index));
|
|
1299
1520
|
console.log();
|
|
1300
1521
|
// Spec details
|
|
1301
1522
|
if (reviewContext.spec) {
|
|
1302
|
-
console.log(
|
|
1303
|
-
console.log(
|
|
1523
|
+
console.log("LINKED SPEC");
|
|
1524
|
+
console.log("-".repeat(60));
|
|
1304
1525
|
console.log(`Title: ${reviewContext.spec.title}`);
|
|
1305
1526
|
console.log(`Type: ${reviewContext.spec.type}`);
|
|
1306
1527
|
if (reviewContext.spec.description) {
|
|
1307
1528
|
console.log(`\nDescription:\n${reviewContext.spec.description}`);
|
|
1308
1529
|
}
|
|
1309
|
-
if (reviewContext.spec.acceptance_criteria &&
|
|
1530
|
+
if (reviewContext.spec.acceptance_criteria &&
|
|
1531
|
+
reviewContext.spec.acceptance_criteria.length > 0) {
|
|
1310
1532
|
console.log(`\nAcceptance Criteria (${reviewContext.spec.acceptance_criteria.length}):`);
|
|
1311
1533
|
for (const ac of reviewContext.spec.acceptance_criteria) {
|
|
1312
1534
|
const isCovered = reviewContext.testCoverage?.covered.includes(ac.id);
|
|
1313
|
-
const coverageMarker = isCovered
|
|
1535
|
+
const coverageMarker = isCovered
|
|
1536
|
+
? chalk.green("✓")
|
|
1537
|
+
: chalk.yellow("○");
|
|
1314
1538
|
console.log(` ${coverageMarker} [${ac.id}]`);
|
|
1315
1539
|
console.log(` Given: ${ac.given}`);
|
|
1316
1540
|
console.log(` When: ${ac.when}`);
|
|
@@ -1325,7 +1549,7 @@ export function registerTaskCommands(program) {
|
|
|
1325
1549
|
}
|
|
1326
1550
|
else {
|
|
1327
1551
|
console.log(chalk.yellow(` Test coverage: ${covered.length}/${covered.length + uncovered.length} ACs covered`));
|
|
1328
|
-
console.log(chalk.yellow(` Missing coverage for: ${uncovered.join(
|
|
1552
|
+
console.log(chalk.yellow(` Missing coverage for: ${uncovered.join(", ")}`));
|
|
1329
1553
|
}
|
|
1330
1554
|
}
|
|
1331
1555
|
}
|
|
@@ -1333,40 +1557,40 @@ export function registerTaskCommands(program) {
|
|
|
1333
1557
|
}
|
|
1334
1558
|
// Git diff
|
|
1335
1559
|
if (reviewContext.diff) {
|
|
1336
|
-
console.log(
|
|
1337
|
-
console.log(
|
|
1560
|
+
console.log("CHANGES SINCE TASK STARTED");
|
|
1561
|
+
console.log("-".repeat(60));
|
|
1338
1562
|
console.log(`Started at: ${foundTask.started_at}`);
|
|
1339
1563
|
console.log();
|
|
1340
1564
|
console.log(reviewContext.diff);
|
|
1341
1565
|
console.log();
|
|
1342
1566
|
}
|
|
1343
1567
|
else if (foundTask.started_at) {
|
|
1344
|
-
console.log(
|
|
1345
|
-
console.log(
|
|
1568
|
+
console.log("CHANGES SINCE TASK STARTED");
|
|
1569
|
+
console.log("-".repeat(60));
|
|
1346
1570
|
console.log(`Started at: ${foundTask.started_at}`);
|
|
1347
|
-
console.log(
|
|
1571
|
+
console.log("No changes detected");
|
|
1348
1572
|
console.log();
|
|
1349
1573
|
}
|
|
1350
|
-
console.log(
|
|
1351
|
-
console.log(
|
|
1352
|
-
console.log(
|
|
1574
|
+
console.log("=".repeat(60));
|
|
1575
|
+
console.log("Review Checklist:");
|
|
1576
|
+
console.log("- Does the implementation match the task description?");
|
|
1353
1577
|
if (reviewContext.spec) {
|
|
1354
|
-
console.log(
|
|
1355
|
-
console.log(
|
|
1578
|
+
console.log("- Are all acceptance criteria covered?");
|
|
1579
|
+
console.log("- Is test coverage adequate?");
|
|
1356
1580
|
}
|
|
1357
|
-
console.log(
|
|
1358
|
-
console.log(
|
|
1581
|
+
console.log("- Are there any gaps or issues?");
|
|
1582
|
+
console.log("=".repeat(60));
|
|
1359
1583
|
});
|
|
1360
1584
|
}
|
|
1361
1585
|
catch (err) {
|
|
1362
|
-
error(
|
|
1586
|
+
error("Failed to generate review context", err);
|
|
1363
1587
|
process.exit(EXIT_CODES.ERROR);
|
|
1364
1588
|
}
|
|
1365
1589
|
});
|
|
1366
1590
|
// kspec task todos <ref>
|
|
1367
1591
|
task
|
|
1368
|
-
.command(
|
|
1369
|
-
.description(
|
|
1592
|
+
.command("todos <ref>")
|
|
1593
|
+
.description("Show todos for a task")
|
|
1370
1594
|
.action(async (ref) => {
|
|
1371
1595
|
try {
|
|
1372
1596
|
const ctx = await initContext();
|
|
@@ -1376,12 +1600,12 @@ export function registerTaskCommands(program) {
|
|
|
1376
1600
|
const foundTask = resolveTaskRef(ref, tasks, index);
|
|
1377
1601
|
output(foundTask.todos, () => {
|
|
1378
1602
|
if (foundTask.todos.length === 0) {
|
|
1379
|
-
console.log(
|
|
1603
|
+
console.log("No todos");
|
|
1380
1604
|
}
|
|
1381
1605
|
else {
|
|
1382
1606
|
for (const todo of foundTask.todos) {
|
|
1383
|
-
const status = todo.done ?
|
|
1384
|
-
const doneInfo = todo.done && todo.done_at ? ` (done ${todo.done_at})` :
|
|
1607
|
+
const status = todo.done ? "[x]" : "[ ]";
|
|
1608
|
+
const doneInfo = todo.done && todo.done_at ? ` (done ${todo.done_at})` : "";
|
|
1385
1609
|
console.log(`${status} ${todo.id}. ${todo.text}${doneInfo}`);
|
|
1386
1610
|
}
|
|
1387
1611
|
}
|
|
@@ -1393,14 +1617,11 @@ export function registerTaskCommands(program) {
|
|
|
1393
1617
|
}
|
|
1394
1618
|
});
|
|
1395
1619
|
// Create subcommand group for todo operations
|
|
1396
|
-
const todoCmd = task
|
|
1397
|
-
.command('todo')
|
|
1398
|
-
.description('Manage task todos');
|
|
1620
|
+
const todoCmd = task.command("todo").description("Manage task todos");
|
|
1399
1621
|
// kspec task todo add <ref> <text>
|
|
1400
|
-
todoCmd
|
|
1401
|
-
.
|
|
1402
|
-
.
|
|
1403
|
-
.option('--author <author>', 'Todo author')
|
|
1622
|
+
markMutating(todoCmd.command("add <ref> <text>"))
|
|
1623
|
+
.description("Add a todo to a task")
|
|
1624
|
+
.option("--author <author>", "Todo author")
|
|
1404
1625
|
.action(async (ref, text, options) => {
|
|
1405
1626
|
try {
|
|
1406
1627
|
const ctx = await initContext();
|
|
@@ -1410,7 +1631,7 @@ export function registerTaskCommands(program) {
|
|
|
1410
1631
|
const foundTask = resolveTaskRef(ref, tasks, index);
|
|
1411
1632
|
// Calculate next ID (max existing + 1, or 1 if none)
|
|
1412
1633
|
const nextId = foundTask.todos.length > 0
|
|
1413
|
-
? Math.max(...foundTask.todos.map(t => t.id)) + 1
|
|
1634
|
+
? Math.max(...foundTask.todos.map((t) => t.id)) + 1
|
|
1414
1635
|
: 1;
|
|
1415
1636
|
const todo = createTodo(nextId, text, options.author);
|
|
1416
1637
|
const updatedTask = {
|
|
@@ -1418,7 +1639,7 @@ export function registerTaskCommands(program) {
|
|
|
1418
1639
|
todos: [...foundTask.todos, todo],
|
|
1419
1640
|
};
|
|
1420
1641
|
await saveTask(ctx, updatedTask);
|
|
1421
|
-
await commitIfShadow(ctx.shadow,
|
|
1642
|
+
await commitIfShadow(ctx.shadow, "task-note", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
|
|
1422
1643
|
success(`Added todo #${todo.id} to task: ${index.shortUlid(updatedTask._ulid)}`, { todo });
|
|
1423
1644
|
}
|
|
1424
1645
|
catch (err) {
|
|
@@ -1427,9 +1648,8 @@ export function registerTaskCommands(program) {
|
|
|
1427
1648
|
}
|
|
1428
1649
|
});
|
|
1429
1650
|
// kspec task todo done <ref> <id>
|
|
1430
|
-
todoCmd
|
|
1431
|
-
.
|
|
1432
|
-
.description('Mark a todo as done')
|
|
1651
|
+
markMutating(todoCmd.command("done <ref> <id>"))
|
|
1652
|
+
.description("Mark a todo as done")
|
|
1433
1653
|
.action(async (ref, idStr) => {
|
|
1434
1654
|
try {
|
|
1435
1655
|
const ctx = await initContext();
|
|
@@ -1438,11 +1658,11 @@ export function registerTaskCommands(program) {
|
|
|
1438
1658
|
const index = new ReferenceIndex(tasks, items);
|
|
1439
1659
|
const foundTask = resolveTaskRef(ref, tasks, index);
|
|
1440
1660
|
const id = parseInt(idStr, 10);
|
|
1441
|
-
if (isNaN(id)) {
|
|
1661
|
+
if (Number.isNaN(id)) {
|
|
1442
1662
|
error(errors.todo.invalidId(idStr));
|
|
1443
1663
|
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
1444
1664
|
}
|
|
1445
|
-
const todoIndex = foundTask.todos.findIndex(t => t.id === id);
|
|
1665
|
+
const todoIndex = foundTask.todos.findIndex((t) => t.id === id);
|
|
1446
1666
|
if (todoIndex === -1) {
|
|
1447
1667
|
error(errors.todo.notFound(id));
|
|
1448
1668
|
process.exit(EXIT_CODES.NOT_FOUND);
|
|
@@ -1462,8 +1682,10 @@ export function registerTaskCommands(program) {
|
|
|
1462
1682
|
todos: updatedTodos,
|
|
1463
1683
|
};
|
|
1464
1684
|
await saveTask(ctx, updatedTask);
|
|
1465
|
-
await commitIfShadow(ctx.shadow,
|
|
1466
|
-
success(`Marked todo #${id} as done`, {
|
|
1685
|
+
await commitIfShadow(ctx.shadow, "task-note", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
|
|
1686
|
+
success(`Marked todo #${id} as done`, {
|
|
1687
|
+
todo: updatedTodos[todoIndex],
|
|
1688
|
+
});
|
|
1467
1689
|
}
|
|
1468
1690
|
catch (err) {
|
|
1469
1691
|
error(errors.failures.markTodoDone, err);
|
|
@@ -1471,9 +1693,8 @@ export function registerTaskCommands(program) {
|
|
|
1471
1693
|
}
|
|
1472
1694
|
});
|
|
1473
1695
|
// kspec task todo undone <ref> <id>
|
|
1474
|
-
todoCmd
|
|
1475
|
-
.
|
|
1476
|
-
.description('Mark a todo as not done')
|
|
1696
|
+
markMutating(todoCmd.command("undone <ref> <id>"))
|
|
1697
|
+
.description("Mark a todo as not done")
|
|
1477
1698
|
.action(async (ref, idStr) => {
|
|
1478
1699
|
try {
|
|
1479
1700
|
const ctx = await initContext();
|
|
@@ -1482,11 +1703,11 @@ export function registerTaskCommands(program) {
|
|
|
1482
1703
|
const index = new ReferenceIndex(tasks, items);
|
|
1483
1704
|
const foundTask = resolveTaskRef(ref, tasks, index);
|
|
1484
1705
|
const id = parseInt(idStr, 10);
|
|
1485
|
-
if (isNaN(id)) {
|
|
1706
|
+
if (Number.isNaN(id)) {
|
|
1486
1707
|
error(errors.todo.invalidId(idStr));
|
|
1487
1708
|
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
1488
1709
|
}
|
|
1489
|
-
const todoIndex = foundTask.todos.findIndex(t => t.id === id);
|
|
1710
|
+
const todoIndex = foundTask.todos.findIndex((t) => t.id === id);
|
|
1490
1711
|
if (todoIndex === -1) {
|
|
1491
1712
|
error(errors.todo.notFound(id));
|
|
1492
1713
|
process.exit(EXIT_CODES.NOT_FOUND);
|
|
@@ -1506,8 +1727,10 @@ export function registerTaskCommands(program) {
|
|
|
1506
1727
|
todos: updatedTodos,
|
|
1507
1728
|
};
|
|
1508
1729
|
await saveTask(ctx, updatedTask);
|
|
1509
|
-
await commitIfShadow(ctx.shadow,
|
|
1510
|
-
success(`Marked todo #${id} as not done`, {
|
|
1730
|
+
await commitIfShadow(ctx.shadow, "task-note", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
|
|
1731
|
+
success(`Marked todo #${id} as not done`, {
|
|
1732
|
+
todo: updatedTodos[todoIndex],
|
|
1733
|
+
});
|
|
1511
1734
|
}
|
|
1512
1735
|
catch (err) {
|
|
1513
1736
|
error(errors.failures.markTodoNotDone, err);
|