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