@kynetic-ai/spec 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +250 -17
- package/dist/acp/client.d.ts +18 -4
- package/dist/acp/client.d.ts.map +1 -1
- package/dist/acp/client.js +44 -26
- package/dist/acp/client.js.map +1 -1
- package/dist/acp/framing.d.ts +2 -2
- package/dist/acp/framing.d.ts.map +1 -1
- package/dist/acp/framing.js +37 -29
- package/dist/acp/framing.js.map +1 -1
- package/dist/acp/index.d.ts +6 -7
- package/dist/acp/index.d.ts.map +1 -1
- package/dist/acp/index.js +3 -3
- package/dist/acp/index.js.map +1 -1
- package/dist/acp/types.d.ts +5 -5
- package/dist/acp/types.d.ts.map +1 -1
- package/dist/acp/types.js +18 -18
- package/dist/acp/types.js.map +1 -1
- package/dist/agents/adapters.d.ts.map +1 -1
- package/dist/agents/adapters.js +24 -13
- package/dist/agents/adapters.js.map +1 -1
- package/dist/agents/index.d.ts +2 -2
- package/dist/agents/index.js +2 -2
- package/dist/agents/spawner.d.ts +4 -4
- package/dist/agents/spawner.d.ts.map +1 -1
- package/dist/agents/spawner.js +6 -6
- package/dist/agents/spawner.js.map +1 -1
- package/dist/cli/batch-context.d.ts +43 -0
- package/dist/cli/batch-context.d.ts.map +1 -0
- package/dist/cli/batch-context.js +93 -0
- package/dist/cli/batch-context.js.map +1 -0
- package/dist/cli/batch-exec.d.ts +116 -0
- package/dist/cli/batch-exec.d.ts.map +1 -0
- package/dist/cli/batch-exec.js +694 -0
- package/dist/cli/batch-exec.js.map +1 -0
- package/dist/cli/batch.d.ts +4 -2
- package/dist/cli/batch.d.ts.map +1 -1
- package/dist/cli/batch.js +15 -14
- package/dist/cli/batch.js.map +1 -1
- package/dist/cli/command-annotations.d.ts +23 -0
- package/dist/cli/command-annotations.d.ts.map +1 -0
- package/dist/cli/command-annotations.js +27 -0
- package/dist/cli/command-annotations.js.map +1 -0
- package/dist/cli/commands/agents.d.ts +46 -0
- package/dist/cli/commands/agents.d.ts.map +1 -0
- package/dist/cli/commands/agents.js +377 -0
- package/dist/cli/commands/agents.js.map +1 -0
- package/dist/cli/commands/batch.d.ts +20 -0
- package/dist/cli/commands/batch.d.ts.map +1 -0
- package/dist/cli/commands/batch.js +214 -0
- package/dist/cli/commands/batch.js.map +1 -0
- package/dist/cli/commands/clone-for-testing.d.ts +1 -1
- package/dist/cli/commands/clone-for-testing.d.ts.map +1 -1
- package/dist/cli/commands/clone-for-testing.js +37 -47
- package/dist/cli/commands/clone-for-testing.js.map +1 -1
- package/dist/cli/commands/derive.d.ts +1 -1
- package/dist/cli/commands/derive.d.ts.map +1 -1
- package/dist/cli/commands/derive.js +140 -88
- package/dist/cli/commands/derive.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +11 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +152 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/export.d.ts +12 -0
- package/dist/cli/commands/export.d.ts.map +1 -0
- package/dist/cli/commands/export.js +134 -0
- package/dist/cli/commands/export.js.map +1 -0
- package/dist/cli/commands/help.d.ts +1 -1
- package/dist/cli/commands/help.d.ts.map +1 -1
- package/dist/cli/commands/help.js +163 -37
- package/dist/cli/commands/help.js.map +1 -1
- package/dist/cli/commands/inbox.d.ts +1 -1
- package/dist/cli/commands/inbox.d.ts.map +1 -1
- package/dist/cli/commands/inbox.js +178 -56
- package/dist/cli/commands/inbox.js.map +1 -1
- package/dist/cli/commands/index.d.ts +31 -19
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +31 -19
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts +5 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +108 -57
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/item.d.ts +1 -1
- package/dist/cli/commands/item.d.ts.map +1 -1
- package/dist/cli/commands/item.js +557 -274
- package/dist/cli/commands/item.js.map +1 -1
- package/dist/cli/commands/link.d.ts +1 -1
- package/dist/cli/commands/link.d.ts.map +1 -1
- package/dist/cli/commands/link.js +55 -46
- package/dist/cli/commands/link.js.map +1 -1
- package/dist/cli/commands/log.d.ts +1 -1
- package/dist/cli/commands/log.d.ts.map +1 -1
- package/dist/cli/commands/log.js +57 -51
- package/dist/cli/commands/log.js.map +1 -1
- package/dist/cli/commands/merge-driver.d.ts +19 -0
- package/dist/cli/commands/merge-driver.d.ts.map +1 -0
- package/dist/cli/commands/merge-driver.js +398 -0
- package/dist/cli/commands/merge-driver.js.map +1 -0
- package/dist/cli/commands/meta.d.ts +1 -1
- package/dist/cli/commands/meta.d.ts.map +1 -1
- package/dist/cli/commands/meta.js +533 -399
- package/dist/cli/commands/meta.js.map +1 -1
- package/dist/cli/commands/module.d.ts +1 -1
- package/dist/cli/commands/module.d.ts.map +1 -1
- package/dist/cli/commands/module.js +30 -25
- package/dist/cli/commands/module.js.map +1 -1
- package/dist/cli/commands/plan-import.d.ts +11 -0
- package/dist/cli/commands/plan-import.d.ts.map +1 -0
- package/dist/cli/commands/plan-import.js +516 -0
- package/dist/cli/commands/plan-import.js.map +1 -0
- package/dist/cli/commands/plan.d.ts +10 -0
- package/dist/cli/commands/plan.d.ts.map +1 -0
- package/dist/cli/commands/plan.js +421 -0
- package/dist/cli/commands/plan.js.map +1 -0
- package/dist/cli/commands/ralph.d.ts +1 -1
- package/dist/cli/commands/ralph.d.ts.map +1 -1
- package/dist/cli/commands/ralph.js +1097 -169
- package/dist/cli/commands/ralph.js.map +1 -1
- package/dist/cli/commands/refs.d.ts +13 -0
- package/dist/cli/commands/refs.d.ts.map +1 -0
- package/dist/cli/commands/refs.js +283 -0
- package/dist/cli/commands/refs.js.map +1 -0
- package/dist/cli/commands/search.d.ts +1 -1
- package/dist/cli/commands/search.d.ts.map +1 -1
- package/dist/cli/commands/search.js +199 -37
- package/dist/cli/commands/search.js.map +1 -1
- package/dist/cli/commands/serve.d.ts +10 -0
- package/dist/cli/commands/serve.d.ts.map +1 -0
- package/dist/cli/commands/serve.js +491 -0
- package/dist/cli/commands/serve.js.map +1 -0
- package/dist/cli/commands/session.d.ts +25 -6
- package/dist/cli/commands/session.d.ts.map +1 -1
- package/dist/cli/commands/session.js +811 -127
- package/dist/cli/commands/session.js.map +1 -1
- package/dist/cli/commands/setup-seeding.d.ts +81 -0
- package/dist/cli/commands/setup-seeding.d.ts.map +1 -0
- package/dist/cli/commands/setup-seeding.js +292 -0
- package/dist/cli/commands/setup-seeding.js.map +1 -0
- package/dist/cli/commands/setup.d.ts +77 -3
- package/dist/cli/commands/setup.d.ts.map +1 -1
- package/dist/cli/commands/setup.js +1233 -274
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/shadow.d.ts +1 -1
- package/dist/cli/commands/shadow.d.ts.map +1 -1
- package/dist/cli/commands/shadow.js +70 -50
- package/dist/cli/commands/shadow.js.map +1 -1
- package/dist/cli/commands/skill-crud.d.ts +58 -0
- package/dist/cli/commands/skill-crud.d.ts.map +1 -0
- package/dist/cli/commands/skill-crud.js +753 -0
- package/dist/cli/commands/skill-crud.js.map +1 -0
- package/dist/cli/commands/skill-diff.d.ts +27 -0
- package/dist/cli/commands/skill-diff.d.ts.map +1 -0
- package/dist/cli/commands/skill-diff.js +840 -0
- package/dist/cli/commands/skill-diff.js.map +1 -0
- package/dist/cli/commands/skill-install.d.ts +53 -0
- package/dist/cli/commands/skill-install.d.ts.map +1 -0
- package/dist/cli/commands/skill-install.js +452 -0
- package/dist/cli/commands/skill-install.js.map +1 -0
- package/dist/cli/commands/skill.d.ts +20 -0
- package/dist/cli/commands/skill.d.ts.map +1 -0
- package/dist/cli/commands/skill.js +36 -0
- package/dist/cli/commands/skill.js.map +1 -0
- package/dist/cli/commands/task.d.ts +1 -1
- package/dist/cli/commands/task.d.ts.map +1 -1
- package/dist/cli/commands/task.js +569 -346
- package/dist/cli/commands/task.js.map +1 -1
- package/dist/cli/commands/tasks.d.ts +26 -1
- package/dist/cli/commands/tasks.d.ts.map +1 -1
- package/dist/cli/commands/tasks.js +227 -122
- package/dist/cli/commands/tasks.js.map +1 -1
- package/dist/cli/commands/trait.d.ts +1 -1
- package/dist/cli/commands/trait.d.ts.map +1 -1
- package/dist/cli/commands/trait.js +166 -101
- package/dist/cli/commands/trait.js.map +1 -1
- package/dist/cli/commands/triage.d.ts +7 -0
- package/dist/cli/commands/triage.d.ts.map +1 -0
- package/dist/cli/commands/triage.js +569 -0
- package/dist/cli/commands/triage.js.map +1 -0
- package/dist/cli/commands/util.d.ts +7 -0
- package/dist/cli/commands/util.d.ts.map +1 -0
- package/dist/cli/commands/util.js +30 -0
- package/dist/cli/commands/util.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +1 -1
- package/dist/cli/commands/validate.d.ts.map +1 -1
- package/dist/cli/commands/validate.js +264 -83
- package/dist/cli/commands/validate.js.map +1 -1
- package/dist/cli/commands/workflow.d.ts +16 -0
- package/dist/cli/commands/workflow.d.ts.map +1 -0
- package/dist/cli/commands/workflow.js +851 -0
- package/dist/cli/commands/workflow.js.map +1 -0
- package/dist/cli/exit-codes.d.ts +7 -0
- package/dist/cli/exit-codes.d.ts.map +1 -1
- package/dist/cli/exit-codes.js +26 -18
- package/dist/cli/exit-codes.js.map +1 -1
- package/dist/cli/help/content.d.ts.map +1 -1
- package/dist/cli/help/content.js +86 -71
- package/dist/cli/help/content.js.map +1 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +131 -19
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/introspection.d.ts +6 -2
- package/dist/cli/introspection.d.ts.map +1 -1
- package/dist/cli/introspection.js +11 -8
- package/dist/cli/introspection.js.map +1 -1
- package/dist/cli/output.d.ts +64 -4
- package/dist/cli/output.d.ts.map +1 -1
- package/dist/cli/output.js +235 -85
- package/dist/cli/output.js.map +1 -1
- package/dist/cli/parse-utils.d.ts +21 -0
- package/dist/cli/parse-utils.d.ts.map +1 -0
- package/dist/cli/parse-utils.js +32 -0
- package/dist/cli/parse-utils.js.map +1 -0
- package/dist/cli/pid-utils.d.ts +72 -0
- package/dist/cli/pid-utils.d.ts.map +1 -0
- package/dist/cli/pid-utils.js +174 -0
- package/dist/cli/pid-utils.js.map +1 -0
- package/dist/cli/suggest.d.ts.map +1 -1
- package/dist/cli/suggest.js +1 -2
- package/dist/cli/suggest.js.map +1 -1
- package/dist/cli/validators.d.ts +43 -0
- package/dist/cli/validators.d.ts.map +1 -0
- package/dist/cli/validators.js +84 -0
- package/dist/cli/validators.js.map +1 -0
- package/dist/daemon/index.ts +52 -0
- package/dist/daemon/middleware/project-context.ts +126 -0
- package/dist/daemon/pid.ts +179 -0
- package/dist/daemon/project-context.ts +343 -0
- package/dist/daemon/routes/inbox.ts +164 -0
- package/dist/daemon/routes/items.ts +322 -0
- package/dist/daemon/routes/meta.ts +118 -0
- package/dist/daemon/routes/projects.ts +162 -0
- package/dist/daemon/routes/tasks.ts +327 -0
- package/dist/daemon/routes/triage.ts +468 -0
- package/dist/daemon/routes/validation.ts +248 -0
- package/dist/daemon/server.ts +408 -0
- package/dist/daemon/watcher.ts +195 -0
- package/dist/daemon/websocket/handler.ts +138 -0
- package/dist/daemon/websocket/heartbeat.ts +71 -0
- package/dist/daemon/websocket/pubsub.ts +125 -0
- package/dist/daemon/websocket/types.ts +66 -0
- package/dist/export/html.d.ts +19 -0
- package/dist/export/html.d.ts.map +1 -0
- package/dist/export/html.js +239 -0
- package/dist/export/html.js.map +1 -0
- package/dist/export/index.d.ts +10 -0
- package/dist/export/index.d.ts.map +1 -0
- package/dist/export/index.js +10 -0
- package/dist/export/index.js.map +1 -0
- package/dist/export/json.d.ts +24 -0
- package/dist/export/json.d.ts.map +1 -0
- package/dist/export/json.js +198 -0
- package/dist/export/json.js.map +1 -0
- package/dist/export/triage.d.ts +51 -0
- package/dist/export/triage.d.ts.map +1 -0
- package/dist/export/triage.js +83 -0
- package/dist/export/triage.js.map +1 -0
- package/dist/export/types.d.ts +122 -0
- package/dist/export/types.d.ts.map +1 -0
- package/dist/export/types.js +9 -0
- package/dist/export/types.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/lib/claude-plugin-registry.d.ts +66 -0
- package/dist/lib/claude-plugin-registry.d.ts.map +1 -0
- package/dist/lib/claude-plugin-registry.js +318 -0
- package/dist/lib/claude-plugin-registry.js.map +1 -0
- package/dist/merge/arrays.d.ts +87 -0
- package/dist/merge/arrays.d.ts.map +1 -0
- package/dist/merge/arrays.js +164 -0
- package/dist/merge/arrays.js.map +1 -0
- package/dist/merge/file-type.d.ts +32 -0
- package/dist/merge/file-type.d.ts.map +1 -0
- package/dist/merge/file-type.js +70 -0
- package/dist/merge/file-type.js.map +1 -0
- package/dist/merge/index.d.ts +14 -0
- package/dist/merge/index.d.ts.map +1 -0
- package/dist/merge/index.js +11 -0
- package/dist/merge/index.js.map +1 -0
- package/dist/merge/objects.d.ts +46 -0
- package/dist/merge/objects.d.ts.map +1 -0
- package/dist/merge/objects.js +193 -0
- package/dist/merge/objects.js.map +1 -0
- package/dist/merge/parse.d.ts +23 -0
- package/dist/merge/parse.d.ts.map +1 -0
- package/dist/merge/parse.js +78 -0
- package/dist/merge/parse.js.map +1 -0
- package/dist/merge/resolve.d.ts +66 -0
- package/dist/merge/resolve.d.ts.map +1 -0
- package/dist/merge/resolve.js +189 -0
- package/dist/merge/resolve.js.map +1 -0
- package/dist/merge/types.d.ts +82 -0
- package/dist/merge/types.d.ts.map +1 -0
- package/dist/merge/types.js +8 -0
- package/dist/merge/types.js.map +1 -0
- package/dist/parser/agent-data-sections.d.ts +53 -0
- package/dist/parser/agent-data-sections.d.ts.map +1 -0
- package/dist/parser/agent-data-sections.js +118 -0
- package/dist/parser/agent-data-sections.js.map +1 -0
- package/dist/parser/alignment.d.ts +4 -4
- package/dist/parser/alignment.d.ts.map +1 -1
- package/dist/parser/alignment.js +27 -22
- package/dist/parser/alignment.js.map +1 -1
- package/dist/parser/assess.d.ts +5 -5
- package/dist/parser/assess.d.ts.map +1 -1
- package/dist/parser/assess.js +36 -32
- package/dist/parser/assess.js.map +1 -1
- package/dist/parser/config.d.ts +351 -0
- package/dist/parser/config.d.ts.map +1 -0
- package/dist/parser/config.js +326 -0
- package/dist/parser/config.js.map +1 -0
- package/dist/parser/convention-validation.d.ts +1 -1
- package/dist/parser/convention-validation.d.ts.map +1 -1
- package/dist/parser/convention-validation.js +21 -16
- package/dist/parser/convention-validation.js.map +1 -1
- package/dist/parser/coverage-cache.d.ts +49 -0
- package/dist/parser/coverage-cache.d.ts.map +1 -0
- package/dist/parser/coverage-cache.js +123 -0
- package/dist/parser/coverage-cache.js.map +1 -0
- package/dist/parser/daemon-status.d.ts +37 -0
- package/dist/parser/daemon-status.d.ts.map +1 -0
- package/dist/parser/daemon-status.js +67 -0
- package/dist/parser/daemon-status.js.map +1 -0
- package/dist/parser/doctor.d.ts +107 -0
- package/dist/parser/doctor.d.ts.map +1 -0
- package/dist/parser/doctor.js +366 -0
- package/dist/parser/doctor.js.map +1 -0
- package/dist/parser/fix.d.ts +1 -1
- package/dist/parser/fix.d.ts.map +1 -1
- package/dist/parser/fix.js +31 -27
- package/dist/parser/fix.js.map +1 -1
- package/dist/parser/index.d.ts +16 -11
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +16 -11
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/items.d.ts +8 -2
- package/dist/parser/items.d.ts.map +1 -1
- package/dist/parser/items.js +71 -35
- package/dist/parser/items.js.map +1 -1
- package/dist/parser/meta.d.ts +167 -9
- package/dist/parser/meta.d.ts.map +1 -1
- package/dist/parser/meta.js +379 -46
- package/dist/parser/meta.js.map +1 -1
- package/dist/parser/plan-document.d.ts +189 -0
- package/dist/parser/plan-document.d.ts.map +1 -0
- package/dist/parser/plan-document.js +340 -0
- package/dist/parser/plan-document.js.map +1 -0
- package/dist/parser/plans.d.ts +59 -0
- package/dist/parser/plans.d.ts.map +1 -0
- package/dist/parser/plans.js +239 -0
- package/dist/parser/plans.js.map +1 -0
- package/dist/parser/refs.d.ts +22 -9
- package/dist/parser/refs.d.ts.map +1 -1
- package/dist/parser/refs.js +102 -50
- package/dist/parser/refs.js.map +1 -1
- package/dist/parser/setup-status.d.ts +71 -0
- package/dist/parser/setup-status.d.ts.map +1 -0
- package/dist/parser/setup-status.js +269 -0
- package/dist/parser/setup-status.js.map +1 -0
- package/dist/parser/shadow.d.ts +150 -19
- package/dist/parser/shadow.d.ts.map +1 -1
- package/dist/parser/shadow.js +548 -187
- package/dist/parser/shadow.js.map +1 -1
- package/dist/parser/skill-render.d.ts +317 -0
- package/dist/parser/skill-render.d.ts.map +1 -0
- package/dist/parser/skill-render.js +943 -0
- package/dist/parser/skill-render.js.map +1 -0
- package/dist/parser/traits.d.ts +3 -3
- package/dist/parser/traits.d.ts.map +1 -1
- package/dist/parser/traits.js +2 -2
- package/dist/parser/traits.js.map +1 -1
- package/dist/parser/validate-skills.d.ts +32 -0
- package/dist/parser/validate-skills.d.ts.map +1 -0
- package/dist/parser/validate-skills.js +202 -0
- package/dist/parser/validate-skills.js.map +1 -0
- package/dist/parser/validate.d.ts +45 -3
- package/dist/parser/validate.d.ts.map +1 -1
- package/dist/parser/validate.js +622 -105
- package/dist/parser/validate.js.map +1 -1
- package/dist/parser/yaml.d.ts +83 -19
- package/dist/parser/yaml.d.ts.map +1 -1
- package/dist/parser/yaml.js +478 -173
- package/dist/parser/yaml.js.map +1 -1
- package/dist/ralph/cli-renderer.d.ts +8 -1
- package/dist/ralph/cli-renderer.d.ts.map +1 -1
- package/dist/ralph/cli-renderer.js +105 -34
- package/dist/ralph/cli-renderer.js.map +1 -1
- package/dist/ralph/events.d.ts +10 -10
- package/dist/ralph/events.d.ts.map +1 -1
- package/dist/ralph/events.js +277 -98
- package/dist/ralph/events.js.map +1 -1
- package/dist/ralph/index.d.ts +5 -2
- package/dist/ralph/index.d.ts.map +1 -1
- package/dist/ralph/index.js +9 -3
- package/dist/ralph/index.js.map +1 -1
- package/dist/ralph/loop-errors.d.ts +83 -0
- package/dist/ralph/loop-errors.d.ts.map +1 -0
- package/dist/ralph/loop-errors.js +150 -0
- package/dist/ralph/loop-errors.js.map +1 -0
- package/dist/ralph/subagent.d.ts +83 -0
- package/dist/ralph/subagent.d.ts.map +1 -0
- package/dist/ralph/subagent.js +174 -0
- package/dist/ralph/subagent.js.map +1 -0
- package/dist/ralph/wrap-up.d.ts +125 -0
- package/dist/ralph/wrap-up.d.ts.map +1 -0
- package/dist/ralph/wrap-up.js +270 -0
- package/dist/ralph/wrap-up.js.map +1 -0
- package/dist/schema/batch.d.ts +95 -0
- package/dist/schema/batch.d.ts.map +1 -0
- package/dist/schema/batch.js +24 -0
- package/dist/schema/batch.js.map +1 -0
- package/dist/schema/common.d.ts +2 -2
- package/dist/schema/common.d.ts.map +1 -1
- package/dist/schema/common.js +34 -31
- package/dist/schema/common.js.map +1 -1
- package/dist/schema/inbox.d.ts +12 -12
- package/dist/schema/inbox.js +4 -4
- package/dist/schema/inbox.js.map +1 -1
- package/dist/schema/index.d.ts +8 -5
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +8 -5
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/meta.d.ts +1454 -27
- package/dist/schema/meta.d.ts.map +1 -1
- package/dist/schema/meta.js +198 -21
- package/dist/schema/meta.js.map +1 -1
- package/dist/schema/plan.d.ts +285 -0
- package/dist/schema/plan.d.ts.map +1 -0
- package/dist/schema/plan.js +81 -0
- package/dist/schema/plan.js.map +1 -0
- package/dist/schema/spec.d.ts +72 -33
- package/dist/schema/spec.d.ts.map +1 -1
- package/dist/schema/spec.js +22 -9
- package/dist/schema/spec.js.map +1 -1
- package/dist/schema/task.d.ts +172 -161
- package/dist/schema/task.d.ts.map +1 -1
- package/dist/schema/task.js +21 -12
- package/dist/schema/task.js.map +1 -1
- package/dist/schema/triage.d.ts +266 -0
- package/dist/schema/triage.d.ts.map +1 -0
- package/dist/schema/triage.js +134 -0
- package/dist/schema/triage.js.map +1 -0
- package/dist/sessions/index.d.ts +2 -2
- package/dist/sessions/index.d.ts.map +1 -1
- package/dist/sessions/index.js +3 -3
- package/dist/sessions/index.js.map +1 -1
- package/dist/sessions/store.d.ts +233 -1
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +628 -31
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/types.d.ts +10 -10
- package/dist/sessions/types.d.ts.map +1 -1
- package/dist/sessions/types.js +10 -9
- package/dist/sessions/types.js.map +1 -1
- package/dist/strings/errors.d.ts +51 -0
- package/dist/strings/errors.d.ts.map +1 -1
- package/dist/strings/errors.js +136 -106
- package/dist/strings/errors.js.map +1 -1
- package/dist/strings/guidance.d.ts.map +1 -1
- package/dist/strings/guidance.js +16 -16
- package/dist/strings/guidance.js.map +1 -1
- package/dist/strings/index.d.ts +4 -4
- package/dist/strings/index.d.ts.map +1 -1
- package/dist/strings/index.js +4 -4
- package/dist/strings/index.js.map +1 -1
- package/dist/strings/labels.d.ts +4 -0
- package/dist/strings/labels.d.ts.map +1 -1
- package/dist/strings/labels.js +45 -41
- package/dist/strings/labels.js.map +1 -1
- package/dist/strings/validation.d.ts.map +1 -1
- package/dist/strings/validation.js +71 -71
- package/dist/strings/validation.js.map +1 -1
- package/dist/utils/commit.d.ts +1 -1
- package/dist/utils/commit.d.ts.map +1 -1
- package/dist/utils/commit.js +28 -26
- package/dist/utils/commit.js.map +1 -1
- package/dist/utils/git.d.ts +1 -1
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +40 -38
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/grep.js +11 -11
- package/dist/utils/grep.js.map +1 -1
- package/dist/utils/index.d.ts +7 -7
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +4 -4
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/time.d.ts.map +1 -1
- package/dist/utils/time.js +10 -10
- package/dist/utils/time.js.map +1 -1
- package/package.json +28 -5
- package/plugin/.claude-plugin/marketplace.json +17 -0
- package/plugin/.claude-plugin/plugin.json +5 -0
- package/plugin/plugins/kspec/skills/help/SKILL.md +42 -0
- package/plugin/plugins/kspec/skills/triage/SKILL.md +206 -0
- package/plugin/plugins/kspec/skills/triage/docs/automation.md +120 -0
- package/plugin/plugins/kspec/skills/triage/docs/inbox.md +144 -0
- package/plugin/plugins/kspec/skills/triage/docs/observations.md +85 -0
- package/templates/agents-sections/01-quick-start.md +22 -0
- package/templates/agents-sections/02-shadow-branch.md +34 -0
- package/templates/agents-sections/03-task-lifecycle.md +48 -0
- package/templates/agents-sections/04-pr-workflow.md +17 -0
- package/templates/agents-sections/05-commit-convention.md +27 -0
- package/templates/agents-sections/06-ralph-loop.md +45 -0
- package/templates/hooks/pre-commit +34 -0
- package/templates/skills/help/SKILL.md +37 -0
- package/templates/skills/manifest.yaml +15 -0
- package/templates/skills/triage/SKILL.md +199 -0
- package/templates/skills/triage/docs/automation.md +120 -0
- package/templates/skills/triage/docs/inbox.md +144 -0
- package/templates/skills/triage/docs/observations.md +85 -0
|
@@ -1,26 +1,31 @@
|
|
|
1
|
-
import chalk from
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { EXIT_CODES } from
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { markMutating } from "../command-annotations.js";
|
|
3
|
+
import { AlignmentIndex, addChildItem, buildIndexes, checkSlugUniqueness, createNote, createSpecItem, deleteSpecItem, findChildItems, findDescendantItems, findTraitImplementors, initContext, loadAllItems, loadAllTasks, patchSpecItems, ReferenceIndex, updateSpecItem, } from "../../parser/index.js";
|
|
4
|
+
import { commitIfShadow } from "../../parser/shadow.js";
|
|
5
|
+
import { SpecItemPatchSchema } from "../../schema/index.js";
|
|
6
|
+
import { errors } from "../../strings/errors.js";
|
|
7
|
+
import { fieldLabels, sectionHeaders } from "../../strings/labels.js";
|
|
8
|
+
import { formatMatchedFields, grepItem } from "../../utils/grep.js";
|
|
9
|
+
import { EXIT_CODES } from "../exit-codes.js";
|
|
10
|
+
import { error, isJsonMode, output, success, warn } from "../output.js";
|
|
11
|
+
import { parseTagsArray } from "../parse-utils.js";
|
|
10
12
|
/**
|
|
11
13
|
* Format a spec item for display
|
|
12
14
|
*/
|
|
13
15
|
function formatItem(item, verbose = false, grepPattern) {
|
|
14
16
|
const shortId = item._ulid.slice(0, 8);
|
|
15
|
-
const slugStr = item.slugs.length > 0 ? chalk.cyan(`@${item.slugs[0]}`) :
|
|
17
|
+
const slugStr = item.slugs.length > 0 ? chalk.cyan(`@${item.slugs[0]}`) : "";
|
|
16
18
|
const typeStr = chalk.gray(`[${item.type}]`);
|
|
17
|
-
let status =
|
|
18
|
-
if (item.status && typeof item.status ===
|
|
19
|
+
let status = "";
|
|
20
|
+
if (item.status && typeof item.status === "object") {
|
|
19
21
|
const s = item.status;
|
|
20
22
|
if (s.implementation) {
|
|
21
|
-
const implColor = s.implementation ===
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
const implColor = s.implementation === "verified"
|
|
24
|
+
? chalk.green
|
|
25
|
+
: s.implementation === "implemented"
|
|
26
|
+
? chalk.cyan
|
|
27
|
+
: s.implementation === "in_progress"
|
|
28
|
+
? chalk.yellow
|
|
24
29
|
: chalk.gray;
|
|
25
30
|
status = implColor(s.implementation);
|
|
26
31
|
}
|
|
@@ -34,16 +39,18 @@ function formatItem(item, verbose = false, grepPattern) {
|
|
|
34
39
|
if (status)
|
|
35
40
|
line += ` ${status}`;
|
|
36
41
|
if (verbose) {
|
|
37
|
-
const tags =
|
|
42
|
+
const tags = "tags" in item && Array.isArray(item.tags) ? item.tags : [];
|
|
38
43
|
if (tags.length > 0) {
|
|
39
|
-
line += chalk.blue(` #${tags.join(
|
|
44
|
+
line += chalk.blue(` #${tags.join(" #")}`);
|
|
40
45
|
}
|
|
41
46
|
}
|
|
42
47
|
// Show matched fields if grep pattern provided
|
|
43
48
|
if (grepPattern) {
|
|
44
49
|
const match = grepItem(item, grepPattern);
|
|
45
50
|
if (match && match.matchedFields.length > 0) {
|
|
46
|
-
line +=
|
|
51
|
+
line +=
|
|
52
|
+
"\n " +
|
|
53
|
+
chalk.gray(`matched: ${formatMatchedFields(match.matchedFields)}`);
|
|
47
54
|
}
|
|
48
55
|
}
|
|
49
56
|
return line;
|
|
@@ -53,7 +60,7 @@ function formatItem(item, verbose = false, grepPattern) {
|
|
|
53
60
|
*/
|
|
54
61
|
function formatItemList(items, verbose = false, grepPattern) {
|
|
55
62
|
if (items.length === 0) {
|
|
56
|
-
console.log(chalk.gray(
|
|
63
|
+
console.log(chalk.gray("No items found"));
|
|
57
64
|
return;
|
|
58
65
|
}
|
|
59
66
|
for (const item of items) {
|
|
@@ -66,37 +73,37 @@ function formatItemList(items, verbose = false, grepPattern) {
|
|
|
66
73
|
*/
|
|
67
74
|
function formatItemTree(items, verbose = false, grepPattern) {
|
|
68
75
|
if (items.length === 0) {
|
|
69
|
-
console.log(chalk.gray(
|
|
76
|
+
console.log(chalk.gray("No items found"));
|
|
70
77
|
return;
|
|
71
78
|
}
|
|
72
79
|
// Build parent-child map
|
|
73
80
|
const childrenMap = new Map();
|
|
74
81
|
const rootItems = [];
|
|
75
82
|
for (const item of items) {
|
|
76
|
-
const path = item._path ||
|
|
83
|
+
const path = item._path || "";
|
|
77
84
|
// Determine parent path
|
|
78
|
-
let parentPath =
|
|
85
|
+
let parentPath = "";
|
|
79
86
|
if (path) {
|
|
80
87
|
// Extract parent path from current path
|
|
81
88
|
// e.g., "features[0].requirements[1]" -> "features[0]"
|
|
82
|
-
const lastDotIndex = path.lastIndexOf(
|
|
89
|
+
const lastDotIndex = path.lastIndexOf(".");
|
|
83
90
|
if (lastDotIndex !== -1) {
|
|
84
91
|
parentPath = path.substring(0, lastDotIndex);
|
|
85
92
|
}
|
|
86
93
|
}
|
|
87
|
-
if (parentPath ===
|
|
94
|
+
if (parentPath === "") {
|
|
88
95
|
// Root level item
|
|
89
96
|
rootItems.push(item);
|
|
90
97
|
}
|
|
91
98
|
else {
|
|
92
99
|
// Find parent by path
|
|
93
|
-
const parent = items.find(i => i._path === parentPath);
|
|
100
|
+
const parent = items.find((i) => i._path === parentPath);
|
|
94
101
|
if (parent) {
|
|
95
102
|
const parentUlid = parent._ulid;
|
|
96
103
|
if (!childrenMap.has(parentUlid)) {
|
|
97
104
|
childrenMap.set(parentUlid, []);
|
|
98
105
|
}
|
|
99
|
-
childrenMap.get(parentUlid)
|
|
106
|
+
childrenMap.get(parentUlid)?.push(item);
|
|
100
107
|
}
|
|
101
108
|
else {
|
|
102
109
|
// Parent not in filtered list, show at root
|
|
@@ -105,14 +112,14 @@ function formatItemTree(items, verbose = false, grepPattern) {
|
|
|
105
112
|
}
|
|
106
113
|
}
|
|
107
114
|
// Recursive function to print tree
|
|
108
|
-
function printTree(item, prefix =
|
|
115
|
+
function printTree(item, prefix = "", isLast = true) {
|
|
109
116
|
// Print current item with tree prefix
|
|
110
|
-
const connector = isLast ?
|
|
117
|
+
const connector = isLast ? "└── " : "├── ";
|
|
111
118
|
const itemLine = formatItem(item, verbose, grepPattern);
|
|
112
119
|
console.log(prefix + connector + itemLine);
|
|
113
120
|
// Print children
|
|
114
121
|
const children = childrenMap.get(item._ulid) || [];
|
|
115
|
-
const childPrefix = prefix + (isLast ?
|
|
122
|
+
const childPrefix = prefix + (isLast ? " " : "│ ");
|
|
116
123
|
children.forEach((child, index) => {
|
|
117
124
|
const isLastChild = index === children.length - 1;
|
|
118
125
|
printTree(child, childPrefix, isLastChild);
|
|
@@ -121,7 +128,7 @@ function formatItemTree(items, verbose = false, grepPattern) {
|
|
|
121
128
|
// Print all root items
|
|
122
129
|
rootItems.forEach((item, index) => {
|
|
123
130
|
const isLast = index === rootItems.length - 1;
|
|
124
|
-
printTree(item,
|
|
131
|
+
printTree(item, "", isLast);
|
|
125
132
|
});
|
|
126
133
|
console.log(chalk.gray(`\n${items.length} item(s)`));
|
|
127
134
|
}
|
|
@@ -140,7 +147,7 @@ async function handleStatusCascade(ctx, parent, newStatus, allItems, refIndex) {
|
|
|
140
147
|
return [];
|
|
141
148
|
}
|
|
142
149
|
// Prompt user for cascade
|
|
143
|
-
const readline = await import(
|
|
150
|
+
const readline = await import("node:readline");
|
|
144
151
|
const rl = readline.createInterface({
|
|
145
152
|
input: process.stdin,
|
|
146
153
|
output: process.stdout,
|
|
@@ -149,18 +156,21 @@ async function handleStatusCascade(ctx, parent, newStatus, allItems, refIndex) {
|
|
|
149
156
|
rl.question(`Update ${children.length} child item(s) to ${newStatus}? [y/n] `, resolve);
|
|
150
157
|
});
|
|
151
158
|
rl.close();
|
|
152
|
-
if (answer.toLowerCase() !==
|
|
159
|
+
if (answer.toLowerCase() !== "y") {
|
|
153
160
|
return [];
|
|
154
161
|
}
|
|
155
162
|
// Update children
|
|
156
163
|
const updatedChildren = [];
|
|
157
164
|
for (const child of children) {
|
|
158
|
-
const currentStatus = child.status && typeof child.status ===
|
|
165
|
+
const currentStatus = child.status && typeof child.status === "object"
|
|
159
166
|
? child.status
|
|
160
|
-
: {
|
|
167
|
+
: {
|
|
168
|
+
maturity: "draft",
|
|
169
|
+
implementation: "not_started",
|
|
170
|
+
};
|
|
161
171
|
const updates = {
|
|
162
172
|
status: {
|
|
163
|
-
maturity: currentStatus.maturity ||
|
|
173
|
+
maturity: currentStatus.maturity || "draft",
|
|
164
174
|
implementation: newStatus,
|
|
165
175
|
},
|
|
166
176
|
};
|
|
@@ -176,27 +186,27 @@ async function handleStatusCascade(ctx, parent, newStatus, allItems, refIndex) {
|
|
|
176
186
|
* Register item commands
|
|
177
187
|
*/
|
|
178
188
|
export function registerItemCommands(program) {
|
|
179
|
-
const item = program
|
|
180
|
-
.command('item')
|
|
181
|
-
.description('Spec item commands');
|
|
189
|
+
const item = program.command("item").description("Spec item commands");
|
|
182
190
|
// kspec item list
|
|
183
191
|
item
|
|
184
|
-
.command(
|
|
185
|
-
.description(
|
|
186
|
-
.option(
|
|
187
|
-
.option(
|
|
188
|
-
.option(
|
|
189
|
-
.option(
|
|
190
|
-
.option(
|
|
191
|
-
.option(
|
|
192
|
-
.option(
|
|
193
|
-
.option(
|
|
194
|
-
.option(
|
|
195
|
-
.option(
|
|
192
|
+
.command("list")
|
|
193
|
+
.description("List spec items with optional filters")
|
|
194
|
+
.option("-t, --type <type>", "Filter by item type (module, feature, requirement, constraint, decision)")
|
|
195
|
+
.option("-s, --status <status>", "Filter by implementation status (not_started, in_progress, implemented, verified)")
|
|
196
|
+
.option("-m, --maturity <maturity>", "Filter by maturity (draft, proposed, stable, deferred, deprecated)")
|
|
197
|
+
.option("--tag <tag>", "Filter by tag (can specify multiple)", (val, prev) => [...prev, val], [])
|
|
198
|
+
.option("--has <field>", "Filter items that have field present", (val, prev) => [...prev, val], [])
|
|
199
|
+
.option("-q, --search <text>", "Search in title")
|
|
200
|
+
.option("-g, --grep <pattern>", "Search content with regex pattern")
|
|
201
|
+
.option("-v, --verbose", "Show more details")
|
|
202
|
+
.option("--tree", "Show parent/child hierarchy")
|
|
203
|
+
.option("--under <ref>", "Scope to descendants of a module or parent item")
|
|
204
|
+
.option("--limit <n>", "Limit results", "50")
|
|
205
|
+
.option("--count", "Show only the count of matching items")
|
|
196
206
|
.action(async (options) => {
|
|
197
207
|
try {
|
|
198
208
|
const ctx = await initContext();
|
|
199
|
-
const { itemIndex, items } = await buildIndexes(ctx);
|
|
209
|
+
const { itemIndex, items, refIndex } = await buildIndexes(ctx);
|
|
200
210
|
// Build filter from options
|
|
201
211
|
const filter = {
|
|
202
212
|
specItemsOnly: true, // Only spec items, not tasks
|
|
@@ -204,14 +214,20 @@ export function registerItemCommands(program) {
|
|
|
204
214
|
if (options.type) {
|
|
205
215
|
filter.type = options.type;
|
|
206
216
|
}
|
|
217
|
+
// AC: @multi-value-status-filter ac-item-list-parity, ac-invalid-item-status
|
|
207
218
|
if (options.status) {
|
|
208
|
-
|
|
219
|
+
const { parseMultiStatus } = await import("./tasks.js");
|
|
220
|
+
const { ImplementationStatusSchema } = await import("../../schema/common.js");
|
|
221
|
+
const statuses = parseMultiStatus(options.status, ImplementationStatusSchema.options, "implementation status");
|
|
222
|
+
if (statuses) {
|
|
223
|
+
filter.implementation = statuses.length === 1 ? statuses[0] : statuses;
|
|
224
|
+
}
|
|
209
225
|
}
|
|
210
226
|
if (options.maturity) {
|
|
211
227
|
filter.maturity = options.maturity;
|
|
212
228
|
}
|
|
213
229
|
if (options.tag && options.tag.length > 0) {
|
|
214
|
-
filter.tags = options.tag;
|
|
230
|
+
filter.tags = parseTagsArray(options.tag);
|
|
215
231
|
}
|
|
216
232
|
if (options.has && options.has.length > 0) {
|
|
217
233
|
filter.hasFields = options.has;
|
|
@@ -222,18 +238,67 @@ export function registerItemCommands(program) {
|
|
|
222
238
|
if (options.grep) {
|
|
223
239
|
filter.grepSearch = options.grep;
|
|
224
240
|
}
|
|
241
|
+
// AC: @module-scoped-item-listing ac-under-filter, ac-under-invalid-ref
|
|
242
|
+
// Handle --under: scope to descendants of a module or parent item
|
|
243
|
+
let underRoot;
|
|
244
|
+
let underDescendantUlids;
|
|
245
|
+
if (options.under) {
|
|
246
|
+
const underResult = refIndex.resolve(options.under);
|
|
247
|
+
if (!underResult.ok) {
|
|
248
|
+
// AC: @module-scoped-item-listing ac-under-invalid-ref
|
|
249
|
+
error(`Reference not found: ${options.under}. Check with: kspec item get ${options.under}`);
|
|
250
|
+
process.exit(EXIT_CODES.NOT_FOUND);
|
|
251
|
+
}
|
|
252
|
+
underRoot = underResult.item;
|
|
253
|
+
// Check it's not a task
|
|
254
|
+
if ("status" in underRoot && typeof underRoot.status === "string") {
|
|
255
|
+
error(`Reference ${options.under} is a task, not a spec item`);
|
|
256
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
257
|
+
}
|
|
258
|
+
// AC: @module-scoped-item-listing ac-nested-descendants
|
|
259
|
+
// Find all descendants based on _path and _sourceFile
|
|
260
|
+
const descendants = findDescendantItems(underRoot, items);
|
|
261
|
+
underDescendantUlids = new Set([underRoot._ulid, ...descendants.map(d => d._ulid)]);
|
|
262
|
+
}
|
|
225
263
|
const limit = parseInt(options.limit, 10) || 50;
|
|
226
|
-
|
|
227
|
-
//
|
|
228
|
-
|
|
264
|
+
// When --under is used, we need to get all items first, then filter by scope,
|
|
265
|
+
// because pagination before scoping could miss items
|
|
266
|
+
let specItems;
|
|
267
|
+
let effectiveTotal;
|
|
268
|
+
if (underDescendantUlids) {
|
|
269
|
+
// AC: @module-scoped-item-listing ac-under-filter, ac-under-with-other-filters
|
|
270
|
+
// Get all items matching filters, then scope to descendants
|
|
271
|
+
const allResults = itemIndex.query(filter);
|
|
272
|
+
const allSpecItems = allResults.filter((item) => !("status" in item && typeof item.status === "string"));
|
|
273
|
+
// Apply --under filtering (AND logic with other filters)
|
|
274
|
+
const scopedItems = allSpecItems.filter(item => underDescendantUlids.has(item._ulid));
|
|
275
|
+
effectiveTotal = scopedItems.length;
|
|
276
|
+
specItems = scopedItems.slice(0, limit);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
const result = itemIndex.queryPaginated(filter, 0, limit);
|
|
280
|
+
// Filter to only LoadedSpecItem (not tasks)
|
|
281
|
+
specItems = result.items.filter((item) => !("status" in item && typeof item.status === "string"));
|
|
282
|
+
effectiveTotal = result.total;
|
|
283
|
+
}
|
|
284
|
+
// AC: @module-scoped-item-listing ac-count-with-under
|
|
285
|
+
// AC: @trait-filterable-list ac-8
|
|
286
|
+
if (options.count) {
|
|
287
|
+
output({ count: effectiveTotal }, () => {
|
|
288
|
+
console.log(effectiveTotal);
|
|
289
|
+
});
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
229
292
|
output({
|
|
230
293
|
items: specItems,
|
|
231
|
-
total:
|
|
294
|
+
total: effectiveTotal,
|
|
232
295
|
showing: specItems.length,
|
|
233
296
|
grepPattern: options.grep,
|
|
234
297
|
tree: options.tree,
|
|
298
|
+
under: options.under,
|
|
235
299
|
}, () => {
|
|
236
300
|
if (options.tree) {
|
|
301
|
+
// AC: @module-scoped-item-listing ac-under-with-tree
|
|
237
302
|
formatItemTree(specItems, options.verbose, options.grep);
|
|
238
303
|
}
|
|
239
304
|
else {
|
|
@@ -248,8 +313,8 @@ export function registerItemCommands(program) {
|
|
|
248
313
|
});
|
|
249
314
|
// kspec item get <ref>
|
|
250
315
|
item
|
|
251
|
-
.command(
|
|
252
|
-
.description(
|
|
316
|
+
.command("get <ref>")
|
|
317
|
+
.description("Get details for a specific item")
|
|
253
318
|
.action(async (ref) => {
|
|
254
319
|
try {
|
|
255
320
|
const ctx = await initContext();
|
|
@@ -267,7 +332,7 @@ export function registerItemCommands(program) {
|
|
|
267
332
|
if (!traitsByTrait.has(trait.ulid)) {
|
|
268
333
|
traitsByTrait.set(trait.ulid, { trait, acs: [] });
|
|
269
334
|
}
|
|
270
|
-
traitsByTrait.get(trait.ulid)
|
|
335
|
+
traitsByTrait.get(trait.ulid)?.acs.push(ac);
|
|
271
336
|
}
|
|
272
337
|
// Build JSON output with inherited traits
|
|
273
338
|
const jsonOutput = {
|
|
@@ -280,31 +345,62 @@ export function registerItemCommands(program) {
|
|
|
280
345
|
};
|
|
281
346
|
output(jsonOutput, () => {
|
|
282
347
|
console.log(chalk.bold(item.title));
|
|
283
|
-
console.log(chalk.gray(
|
|
348
|
+
console.log(chalk.gray("─".repeat(40)));
|
|
284
349
|
console.log(`${fieldLabels.ulid} ${item._ulid}`);
|
|
285
350
|
if (item.slugs.length > 0) {
|
|
286
|
-
console.log(`${fieldLabels.slugs} ${item.slugs.join(
|
|
351
|
+
console.log(`${fieldLabels.slugs} ${item.slugs.join(", ")}`);
|
|
287
352
|
}
|
|
288
353
|
console.log(`${fieldLabels.type} ${item.type}`);
|
|
289
|
-
if (item.status && typeof item.status ===
|
|
354
|
+
if (item.status && typeof item.status === "object") {
|
|
290
355
|
const s = item.status;
|
|
291
356
|
if (s.maturity)
|
|
292
357
|
console.log(`${fieldLabels.maturity} ${s.maturity}`);
|
|
293
|
-
if (s.implementation)
|
|
294
|
-
|
|
358
|
+
if (s.implementation) {
|
|
359
|
+
// AC: @trait-retrospective ac-4
|
|
360
|
+
// Show retrospective verification source
|
|
361
|
+
const isRetrospective = item.traits?.includes("@trait-retrospective");
|
|
362
|
+
const statusLabel = isRetrospective
|
|
363
|
+
? `${s.implementation} (retrospective)`
|
|
364
|
+
: s.implementation;
|
|
365
|
+
console.log(`${fieldLabels.implementation}${statusLabel}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// AC: @trait-retrospective ac-4
|
|
369
|
+
// Show verification metadata for retrospective specs
|
|
370
|
+
const isRetrospective = item.traits?.includes("@trait-retrospective");
|
|
371
|
+
if (isRetrospective && (item.verified_at || item.verified_by)) {
|
|
372
|
+
const verifiedDate = item.verified_at
|
|
373
|
+
? new Date(item.verified_at).toISOString().split("T")[0]
|
|
374
|
+
: "unknown";
|
|
375
|
+
const verifiedBy = item.verified_by || "unknown";
|
|
376
|
+
console.log(`Verified: ${verifiedDate} by ${verifiedBy}`);
|
|
377
|
+
}
|
|
378
|
+
if ("tags" in item &&
|
|
379
|
+
Array.isArray(item.tags) &&
|
|
380
|
+
item.tags.length > 0) {
|
|
381
|
+
console.log(`${fieldLabels.tags} ${item.tags.join(", ")}`);
|
|
382
|
+
}
|
|
383
|
+
// AC: @item-get ac-4
|
|
384
|
+
if (Array.isArray(item.depends_on) && item.depends_on.length > 0) {
|
|
385
|
+
console.log(`${fieldLabels.dependsOn} ${item.depends_on.join(", ")}`);
|
|
295
386
|
}
|
|
296
|
-
if (
|
|
297
|
-
console.log(`${fieldLabels.
|
|
387
|
+
if (Array.isArray(item.implements) && item.implements.length > 0) {
|
|
388
|
+
console.log(`${fieldLabels.implements} ${item.implements.join(", ")}`);
|
|
389
|
+
}
|
|
390
|
+
if (Array.isArray(item.relates_to) && item.relates_to.length > 0) {
|
|
391
|
+
console.log(`${fieldLabels.relatesTo} ${item.relates_to.join(", ")}`);
|
|
298
392
|
}
|
|
299
393
|
if (item.description) {
|
|
300
|
-
console.log(
|
|
394
|
+
console.log(`\n${sectionHeaders.description}`);
|
|
301
395
|
console.log(item.description);
|
|
302
396
|
}
|
|
303
397
|
// AC: @trait-display ac-1 - Show own AC first
|
|
304
|
-
if (
|
|
305
|
-
|
|
398
|
+
if ("acceptance_criteria" in item &&
|
|
399
|
+
Array.isArray(item.acceptance_criteria) &&
|
|
400
|
+
item.acceptance_criteria.length > 0) {
|
|
401
|
+
console.log(`\n${sectionHeaders.acceptanceCriteria}`);
|
|
306
402
|
for (const ac of item.acceptance_criteria) {
|
|
307
|
-
if (ac && typeof ac ===
|
|
403
|
+
if (ac && typeof ac === "object" && "id" in ac) {
|
|
308
404
|
const acObj = ac;
|
|
309
405
|
console.log(chalk.cyan(` [${acObj.id}]`));
|
|
310
406
|
if (acObj.given)
|
|
@@ -321,7 +417,8 @@ export function registerItemCommands(program) {
|
|
|
321
417
|
for (const { trait, acs } of traitsByTrait.values()) {
|
|
322
418
|
console.log(chalk.gray(`\n─── Inherited from @${trait.slug} ───`));
|
|
323
419
|
for (const ac of acs) {
|
|
324
|
-
console.log(chalk.cyan(` [${ac.id}]`) +
|
|
420
|
+
console.log(chalk.cyan(` [${ac.id}]`) +
|
|
421
|
+
chalk.gray(` (from @${trait.slug})`));
|
|
325
422
|
if (ac.given)
|
|
326
423
|
console.log(` Given: ${ac.given}`);
|
|
327
424
|
if (ac.when)
|
|
@@ -340,16 +437,16 @@ export function registerItemCommands(program) {
|
|
|
340
437
|
});
|
|
341
438
|
// kspec item types - show available types and counts
|
|
342
439
|
item
|
|
343
|
-
.command(
|
|
344
|
-
.description(
|
|
440
|
+
.command("types")
|
|
441
|
+
.description("Show item types and counts")
|
|
345
442
|
.action(async () => {
|
|
346
443
|
try {
|
|
347
444
|
const ctx = await initContext();
|
|
348
445
|
const { itemIndex } = await buildIndexes(ctx);
|
|
349
446
|
const typeCounts = itemIndex.getTypeCounts();
|
|
350
447
|
output(Object.fromEntries(typeCounts), () => {
|
|
351
|
-
console.log(chalk.bold(
|
|
352
|
-
console.log(chalk.gray(
|
|
448
|
+
console.log(chalk.bold("Item Types"));
|
|
449
|
+
console.log(chalk.gray("─".repeat(30)));
|
|
353
450
|
for (const [type, count] of typeCounts) {
|
|
354
451
|
console.log(` ${type}: ${count}`);
|
|
355
452
|
}
|
|
@@ -363,16 +460,16 @@ export function registerItemCommands(program) {
|
|
|
363
460
|
});
|
|
364
461
|
// kspec item tags - show available tags and counts
|
|
365
462
|
item
|
|
366
|
-
.command(
|
|
367
|
-
.description(
|
|
463
|
+
.command("tags")
|
|
464
|
+
.description("Show tags and counts")
|
|
368
465
|
.action(async () => {
|
|
369
466
|
try {
|
|
370
467
|
const ctx = await initContext();
|
|
371
468
|
const { itemIndex } = await buildIndexes(ctx);
|
|
372
469
|
const tagCounts = itemIndex.getTagCounts();
|
|
373
470
|
output(Object.fromEntries(tagCounts), () => {
|
|
374
|
-
console.log(chalk.bold(
|
|
375
|
-
console.log(chalk.gray(
|
|
471
|
+
console.log(chalk.bold("Tags"));
|
|
472
|
+
console.log(chalk.gray("─".repeat(30)));
|
|
376
473
|
for (const [tag, count] of tagCounts) {
|
|
377
474
|
console.log(` #${tag}: ${count}`);
|
|
378
475
|
}
|
|
@@ -384,17 +481,22 @@ export function registerItemCommands(program) {
|
|
|
384
481
|
}
|
|
385
482
|
});
|
|
386
483
|
// kspec item add - create a new spec item under a parent
|
|
387
|
-
item
|
|
388
|
-
.
|
|
389
|
-
.
|
|
390
|
-
.requiredOption(
|
|
391
|
-
.
|
|
392
|
-
.option(
|
|
393
|
-
.option(
|
|
394
|
-
.option(
|
|
395
|
-
.option(
|
|
396
|
-
.option(
|
|
397
|
-
.option(
|
|
484
|
+
markMutating(item.command("add"))
|
|
485
|
+
.description("Create a new spec item under a parent")
|
|
486
|
+
.requiredOption("--under <ref>", "Parent item reference (e.g., @core-primitives)")
|
|
487
|
+
.requiredOption("--title <title>", "Item title")
|
|
488
|
+
.option("--type <type>", "Item type (feature, requirement, constraint, decision)", "feature")
|
|
489
|
+
.option("--slug <slug>", "Human-friendly slug")
|
|
490
|
+
.option("--priority <priority>", "Priority (high, medium, low)")
|
|
491
|
+
.option("--tag <tag...>", "Tags")
|
|
492
|
+
.option("--trait <trait...>", "Traits to apply (e.g., @trait-testable)")
|
|
493
|
+
.option("--description <desc>", "Description")
|
|
494
|
+
.option("--as <field>", "Child field override (e.g., requirements, constraints)")
|
|
495
|
+
.addHelpText("after", `
|
|
496
|
+
Examples:
|
|
497
|
+
$ kspec item add --under @parent --title "Feature name" --type feature
|
|
498
|
+
$ kspec item add --under @parent --title "Multi-tag" --tag api public
|
|
499
|
+
$ kspec item add --under @parent --title "API endpoint" --trait @trait-api-endpoint`)
|
|
398
500
|
.action(async (options) => {
|
|
399
501
|
try {
|
|
400
502
|
const ctx = await initContext();
|
|
@@ -407,7 +509,7 @@ export function registerItemCommands(program) {
|
|
|
407
509
|
}
|
|
408
510
|
const parent = parentResult.item;
|
|
409
511
|
// Check it's not a task
|
|
410
|
-
if (
|
|
512
|
+
if ("status" in parent && typeof parent.status === "string") {
|
|
411
513
|
error(errors.reference.parentIsTask(options.under));
|
|
412
514
|
process.exit(EXIT_CODES.ERROR);
|
|
413
515
|
}
|
|
@@ -419,33 +521,66 @@ export function registerItemCommands(program) {
|
|
|
419
521
|
process.exit(EXIT_CODES.CONFLICT);
|
|
420
522
|
}
|
|
421
523
|
}
|
|
524
|
+
// Validate and canonicalize traits
|
|
525
|
+
const validatedTraits = [];
|
|
526
|
+
const seenTraitUlids = new Set();
|
|
527
|
+
let hasTraitErrors = false;
|
|
528
|
+
if (options.trait) {
|
|
529
|
+
for (const traitRef of options.trait) {
|
|
530
|
+
const traitResult = refIndex.resolve(traitRef);
|
|
531
|
+
if (!traitResult.ok) {
|
|
532
|
+
error(`Trait not found: ${traitRef}`);
|
|
533
|
+
hasTraitErrors = true;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
const traitItem = traitResult.item;
|
|
537
|
+
if (traitItem.type !== "trait") {
|
|
538
|
+
error(`${traitRef} is not a trait (type: ${traitItem.type})`);
|
|
539
|
+
hasTraitErrors = true;
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
// Deduplicate by ULID
|
|
543
|
+
if (seenTraitUlids.has(traitItem._ulid)) {
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
seenTraitUlids.add(traitItem._ulid);
|
|
547
|
+
// Store canonical ref (prefer slug over ULID)
|
|
548
|
+
const canonicalRef = `@${traitItem.slugs[0] || traitItem._ulid}`;
|
|
549
|
+
validatedTraits.push(canonicalRef);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (hasTraitErrors) {
|
|
553
|
+
process.exit(EXIT_CODES.NOT_FOUND);
|
|
554
|
+
}
|
|
422
555
|
const input = {
|
|
423
556
|
title: options.title,
|
|
424
557
|
type: options.type,
|
|
425
558
|
slugs: options.slug ? [options.slug] : [],
|
|
426
559
|
priority: options.priority,
|
|
427
|
-
tags: options.tag
|
|
560
|
+
tags: parseTagsArray(options.tag),
|
|
428
561
|
description: options.description,
|
|
429
562
|
depends_on: [],
|
|
430
563
|
implements: [],
|
|
431
564
|
relates_to: [],
|
|
432
565
|
tests: [],
|
|
433
|
-
traits:
|
|
566
|
+
traits: validatedTraits,
|
|
434
567
|
notes: [],
|
|
435
568
|
};
|
|
436
569
|
const newItem = createSpecItem(input);
|
|
437
570
|
const result = await addChildItem(ctx, parent, newItem, options.as);
|
|
438
571
|
// Build index including the new item for accurate short ULID
|
|
439
572
|
const index = new ReferenceIndex([], [...items, result.item]);
|
|
440
|
-
const itemSlug = result.item.slugs?.[0] ||
|
|
441
|
-
|
|
573
|
+
const itemSlug = result.item.slugs?.[0] ||
|
|
574
|
+
index.shortUlid(result.item._ulid);
|
|
575
|
+
await commitIfShadow(ctx.shadow, "item-add", itemSlug);
|
|
442
576
|
success(`Created item: ${index.shortUlid(result.item._ulid)} under @${parent.slugs[0] || parent._ulid.slice(0, 8)}`, {
|
|
443
577
|
item: result.item,
|
|
444
578
|
path: result.path,
|
|
445
579
|
});
|
|
446
580
|
// Derive hint
|
|
447
581
|
if (!isJsonMode()) {
|
|
448
|
-
const refSlug = result.item.slugs?.[0] ||
|
|
582
|
+
const refSlug = result.item.slugs?.[0] ||
|
|
583
|
+
index.shortUlid(result.item._ulid);
|
|
449
584
|
console.log(chalk.gray(`\nDerive implementation task? kspec derive @${refSlug}`));
|
|
450
585
|
}
|
|
451
586
|
}
|
|
@@ -455,22 +590,38 @@ export function registerItemCommands(program) {
|
|
|
455
590
|
}
|
|
456
591
|
});
|
|
457
592
|
// kspec item set - update a spec item field
|
|
458
|
-
item
|
|
459
|
-
.
|
|
460
|
-
.
|
|
461
|
-
.option(
|
|
462
|
-
.option(
|
|
463
|
-
.option(
|
|
464
|
-
.option(
|
|
465
|
-
.option(
|
|
466
|
-
.option(
|
|
467
|
-
.option(
|
|
468
|
-
.option(
|
|
469
|
-
.option(
|
|
593
|
+
markMutating(item.command("set <ref>"))
|
|
594
|
+
.description("Update a spec item field")
|
|
595
|
+
.option("--title <title>", "Set title")
|
|
596
|
+
.option("--type <type>", "Set type")
|
|
597
|
+
.option("--slug <slug>", "Add a slug")
|
|
598
|
+
.option("--remove-slug <slug>", "Remove a slug")
|
|
599
|
+
.option("--priority <priority>", "Set priority")
|
|
600
|
+
.option("--tag <tag...>", "Set tags (replaces existing)")
|
|
601
|
+
.option("--description <desc>", "Set description")
|
|
602
|
+
.option("--status <status>", "Set implementation status (not_started, in_progress, implemented, verified)")
|
|
603
|
+
.option("--maturity <maturity>", "Set maturity (draft, proposed, stable, deferred, deprecated)")
|
|
604
|
+
.option("--verified-by <agent-ref>", "Set verified_by (for retrospective specs)")
|
|
605
|
+
.option("--verified-at <iso-timestamp>", "Set verified_at (defaults to now if --verified-by provided)")
|
|
606
|
+
.option("--trait <trait...>", "Set traits (replaces existing)")
|
|
607
|
+
.option("--relates-to <ref>", "Add a relates_to reference")
|
|
608
|
+
.option("--implements <ref>", "Add an implements reference")
|
|
609
|
+
.option("--depends-on <ref>", "Add a depends_on reference")
|
|
610
|
+
.option("--clear-relates-to", "Clear all relates_to references")
|
|
611
|
+
.option("--clear-implements", "Clear all implements references")
|
|
612
|
+
.option("--clear-depends-on", "Clear all depends_on references")
|
|
613
|
+
.addHelpText("after", `
|
|
614
|
+
Examples:
|
|
615
|
+
$ kspec item set @item-ref --title "New title"
|
|
616
|
+
$ kspec item set @item-ref --tag api internal security
|
|
617
|
+
$ kspec item set @item-ref --trait reusable testable
|
|
618
|
+
$ kspec item set @item-ref --relates-to @other-item
|
|
619
|
+
$ kspec item set @item-ref --implements @feature-spec
|
|
620
|
+
$ kspec item set @item-ref --depends-on @prereq-spec`)
|
|
470
621
|
.action(async (ref, options) => {
|
|
471
622
|
try {
|
|
472
623
|
const ctx = await initContext();
|
|
473
|
-
const { refIndex, items } = await buildIndexes(ctx);
|
|
624
|
+
const { refIndex, items, tasks } = await buildIndexes(ctx);
|
|
474
625
|
const result = refIndex.resolve(ref);
|
|
475
626
|
if (!result.ok) {
|
|
476
627
|
error(errors.reference.itemNotFound(ref));
|
|
@@ -478,7 +629,7 @@ export function registerItemCommands(program) {
|
|
|
478
629
|
}
|
|
479
630
|
const foundItem = result.item;
|
|
480
631
|
// Check if it's a task (tasks should use task commands)
|
|
481
|
-
if (
|
|
632
|
+
if ("status" in foundItem && typeof foundItem.status === "string") {
|
|
482
633
|
error(errors.reference.taskUseTaskCommands(ref));
|
|
483
634
|
process.exit(EXIT_CODES.ERROR);
|
|
484
635
|
}
|
|
@@ -502,6 +653,66 @@ export function registerItemCommands(program) {
|
|
|
502
653
|
process.exit(EXIT_CODES.ERROR);
|
|
503
654
|
}
|
|
504
655
|
}
|
|
656
|
+
// Mutual exclusivity: cannot add and clear same field
|
|
657
|
+
// Check this before ref resolution so usage errors take precedence
|
|
658
|
+
if (options.relatesTo && options.clearRelatesTo) {
|
|
659
|
+
error("Cannot use --relates-to and --clear-relates-to together");
|
|
660
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
661
|
+
}
|
|
662
|
+
if (options.implements && options.clearImplements) {
|
|
663
|
+
error("Cannot use --implements and --clear-implements together");
|
|
664
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
665
|
+
}
|
|
666
|
+
if (options.dependsOn && options.clearDependsOn) {
|
|
667
|
+
error("Cannot use --depends-on and --clear-depends-on together");
|
|
668
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
669
|
+
}
|
|
670
|
+
// Helper to validate relationship refs (must exist and be a spec item, not a task)
|
|
671
|
+
// Returns { ulid, canonicalRef } for deduplication and user-friendly storage
|
|
672
|
+
const validateRelationshipRef = (refStr, flagName) => {
|
|
673
|
+
const refResult = refIndex.resolve(refStr);
|
|
674
|
+
if (!refResult.ok) {
|
|
675
|
+
error(errors.reference.itemNotFound(refStr));
|
|
676
|
+
process.exit(EXIT_CODES.NOT_FOUND);
|
|
677
|
+
}
|
|
678
|
+
// Ensure it's a spec item, not a task
|
|
679
|
+
const isTask = tasks.some((t) => t._ulid === refResult.ulid);
|
|
680
|
+
if (isTask) {
|
|
681
|
+
error(`${flagName} reference must be a spec item, not a task: ${refStr}`);
|
|
682
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
683
|
+
}
|
|
684
|
+
// Use primary slug if available for user-friendly storage, otherwise ULID
|
|
685
|
+
const item = refResult.item;
|
|
686
|
+
const canonicalRef = item.slugs?.[0] ? `@${item.slugs[0]}` : `@${refResult.ulid}`;
|
|
687
|
+
return { ulid: refResult.ulid, canonicalRef };
|
|
688
|
+
};
|
|
689
|
+
// Helper to resolve existing refs to ULIDs for deduplication
|
|
690
|
+
const resolveRefsToUlids = (refs) => {
|
|
691
|
+
const ulids = new Set();
|
|
692
|
+
for (const ref of refs) {
|
|
693
|
+
const result = refIndex.resolve(ref);
|
|
694
|
+
if (result.ok) {
|
|
695
|
+
ulids.add(result.ulid);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return ulids;
|
|
699
|
+
};
|
|
700
|
+
// AC: @item-set ac-5 - --relates-to validation
|
|
701
|
+
// Store resolved ULID for deduplication and canonical ref for storage
|
|
702
|
+
let relatesToResolved;
|
|
703
|
+
if (options.relatesTo) {
|
|
704
|
+
relatesToResolved = validateRelationshipRef(options.relatesTo, "--relates-to");
|
|
705
|
+
}
|
|
706
|
+
// AC: @item-set ac-6 - --implements validation
|
|
707
|
+
let implementsResolved;
|
|
708
|
+
if (options.implements) {
|
|
709
|
+
implementsResolved = validateRelationshipRef(options.implements, "--implements");
|
|
710
|
+
}
|
|
711
|
+
// AC: @item-set ac-7 - --depends-on validation
|
|
712
|
+
let dependsOnResolved;
|
|
713
|
+
if (options.dependsOn) {
|
|
714
|
+
dependsOnResolved = validateRelationshipRef(options.dependsOn, "--depends-on");
|
|
715
|
+
}
|
|
505
716
|
// Build updates object
|
|
506
717
|
const updates = {};
|
|
507
718
|
if (options.title)
|
|
@@ -511,7 +722,7 @@ export function registerItemCommands(program) {
|
|
|
511
722
|
if (options.slug || options.removeSlug) {
|
|
512
723
|
let slugs = [...(foundItem.slugs || [])];
|
|
513
724
|
if (options.removeSlug) {
|
|
514
|
-
slugs = slugs.filter(s => s !== options.removeSlug);
|
|
725
|
+
slugs = slugs.filter((s) => s !== options.removeSlug);
|
|
515
726
|
}
|
|
516
727
|
if (options.slug) {
|
|
517
728
|
slugs.push(options.slug);
|
|
@@ -521,12 +732,14 @@ export function registerItemCommands(program) {
|
|
|
521
732
|
if (options.priority)
|
|
522
733
|
updates.priority = options.priority;
|
|
523
734
|
if (options.tag)
|
|
524
|
-
updates.tags = options.tag;
|
|
735
|
+
updates.tags = parseTagsArray(options.tag);
|
|
736
|
+
if (options.trait)
|
|
737
|
+
updates.traits = options.trait;
|
|
525
738
|
if (options.description)
|
|
526
739
|
updates.description = options.description;
|
|
527
740
|
// Handle status updates
|
|
528
741
|
if (options.status || options.maturity) {
|
|
529
|
-
const currentStatus = foundItem.status && typeof foundItem.status ===
|
|
742
|
+
const currentStatus = foundItem.status && typeof foundItem.status === "object"
|
|
530
743
|
? foundItem.status
|
|
531
744
|
: {};
|
|
532
745
|
updates.status = {
|
|
@@ -535,8 +748,53 @@ export function registerItemCommands(program) {
|
|
|
535
748
|
...(options.maturity && { maturity: options.maturity }),
|
|
536
749
|
};
|
|
537
750
|
}
|
|
751
|
+
// Handle verification metadata (for retrospective specs)
|
|
752
|
+
if (options.verifiedBy) {
|
|
753
|
+
updates.verified_by = options.verifiedBy;
|
|
754
|
+
// Default verified_at to now if not specified
|
|
755
|
+
updates.verified_at = options.verifiedAt || new Date().toISOString();
|
|
756
|
+
}
|
|
757
|
+
else if (options.verifiedAt) {
|
|
758
|
+
updates.verified_at = options.verifiedAt;
|
|
759
|
+
}
|
|
760
|
+
// AC: @item-set ac-5 - Handle relates_to (append semantics)
|
|
761
|
+
// Uses resolved ULIDs for deduplication and stores canonical slug format
|
|
762
|
+
if (relatesToResolved) {
|
|
763
|
+
const current = foundItem.relates_to || [];
|
|
764
|
+
const existingUlids = resolveRefsToUlids(current);
|
|
765
|
+
if (!existingUlids.has(relatesToResolved.ulid)) {
|
|
766
|
+
updates.relates_to = [...current, relatesToResolved.canonicalRef];
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (options.clearRelatesTo) {
|
|
770
|
+
updates.relates_to = [];
|
|
771
|
+
}
|
|
772
|
+
// AC: @item-set ac-6 - Handle implements (append semantics)
|
|
773
|
+
// Uses resolved ULIDs for deduplication and stores canonical slug format
|
|
774
|
+
if (implementsResolved) {
|
|
775
|
+
const current = foundItem.implements || [];
|
|
776
|
+
const existingUlids = resolveRefsToUlids(current);
|
|
777
|
+
if (!existingUlids.has(implementsResolved.ulid)) {
|
|
778
|
+
updates.implements = [...current, implementsResolved.canonicalRef];
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
if (options.clearImplements) {
|
|
782
|
+
updates.implements = [];
|
|
783
|
+
}
|
|
784
|
+
// AC: @item-set ac-7 - Handle depends_on (append semantics)
|
|
785
|
+
// Uses resolved ULIDs for deduplication and stores canonical slug format
|
|
786
|
+
if (dependsOnResolved) {
|
|
787
|
+
const current = foundItem.depends_on || [];
|
|
788
|
+
const existingUlids = resolveRefsToUlids(current);
|
|
789
|
+
if (!existingUlids.has(dependsOnResolved.ulid)) {
|
|
790
|
+
updates.depends_on = [...current, dependsOnResolved.canonicalRef];
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (options.clearDependsOn) {
|
|
794
|
+
updates.depends_on = [];
|
|
795
|
+
}
|
|
538
796
|
if (Object.keys(updates).length === 0) {
|
|
539
|
-
warn(
|
|
797
|
+
warn("No updates specified");
|
|
540
798
|
return;
|
|
541
799
|
}
|
|
542
800
|
const updated = await updateSpecItem(ctx, foundItem, updates);
|
|
@@ -547,8 +805,10 @@ export function registerItemCommands(program) {
|
|
|
547
805
|
const cascadeResult = await handleStatusCascade(ctx, updated, options.status, items, refIndex);
|
|
548
806
|
updatedItems.push(...cascadeResult);
|
|
549
807
|
}
|
|
550
|
-
await commitIfShadow(ctx.shadow,
|
|
551
|
-
success(`Updated item: ${refIndex.shortUlid(updated._ulid)}`, {
|
|
808
|
+
await commitIfShadow(ctx.shadow, "item-set", itemSlug);
|
|
809
|
+
success(`Updated item: ${refIndex.shortUlid(updated._ulid)}`, {
|
|
810
|
+
item: updated,
|
|
811
|
+
});
|
|
552
812
|
// Derive hint
|
|
553
813
|
if (!isJsonMode()) {
|
|
554
814
|
const refSlug = updated.slugs?.[0] || refIndex.shortUlid(updated._ulid);
|
|
@@ -561,11 +821,10 @@ export function registerItemCommands(program) {
|
|
|
561
821
|
}
|
|
562
822
|
});
|
|
563
823
|
// kspec item delete - delete a spec item
|
|
564
|
-
item
|
|
565
|
-
.
|
|
566
|
-
.
|
|
567
|
-
.option(
|
|
568
|
-
.option('--cascade', 'Delete item and all descendants')
|
|
824
|
+
markMutating(item.command("delete <ref>"))
|
|
825
|
+
.description("Delete a spec item (including nested items)")
|
|
826
|
+
.option("--force", "Skip confirmation")
|
|
827
|
+
.option("--cascade", "Delete item and all descendants")
|
|
569
828
|
.action(async (ref, options) => {
|
|
570
829
|
try {
|
|
571
830
|
const ctx = await initContext();
|
|
@@ -577,7 +836,7 @@ export function registerItemCommands(program) {
|
|
|
577
836
|
}
|
|
578
837
|
const foundItem = result.item;
|
|
579
838
|
// Check if it's a task
|
|
580
|
-
if (
|
|
839
|
+
if ("status" in foundItem && typeof foundItem.status === "string") {
|
|
581
840
|
error(errors.reference.itemUseTaskCancel(ref));
|
|
582
841
|
process.exit(EXIT_CODES.ERROR);
|
|
583
842
|
}
|
|
@@ -585,15 +844,17 @@ export function registerItemCommands(program) {
|
|
|
585
844
|
error(errors.operation.cannotDeleteNoSource);
|
|
586
845
|
process.exit(EXIT_CODES.ERROR);
|
|
587
846
|
}
|
|
588
|
-
// AC-7
|
|
847
|
+
// AC: @spec-item-delete-children ac-7 - Check if this is a trait with implementors
|
|
589
848
|
const implementors = findTraitImplementors(foundItem, items);
|
|
590
849
|
if (implementors.length > 0) {
|
|
591
|
-
const implementorRefs = implementors
|
|
850
|
+
const implementorRefs = implementors
|
|
851
|
+
.map((i) => `@${i.slugs[0] || i._ulid.slice(0, 8)}`)
|
|
852
|
+
.join(", ");
|
|
592
853
|
const errorMsg = `Cannot delete: trait is used by ${implementors.length} specs. Remove trait from specs first: ${implementorRefs}`;
|
|
593
854
|
if (isJsonMode()) {
|
|
594
855
|
error(errorMsg, {
|
|
595
|
-
error:
|
|
596
|
-
implementors: implementors.map(i => ({
|
|
856
|
+
error: "trait_in_use",
|
|
857
|
+
implementors: implementors.map((i) => ({
|
|
597
858
|
ulid: i._ulid,
|
|
598
859
|
slug: i.slugs[0],
|
|
599
860
|
title: i.title,
|
|
@@ -605,16 +866,16 @@ export function registerItemCommands(program) {
|
|
|
605
866
|
}
|
|
606
867
|
process.exit(EXIT_CODES.ERROR);
|
|
607
868
|
}
|
|
608
|
-
// AC-1
|
|
869
|
+
// AC: @spec-item-delete-children ac-1 ac-8 - Check for child items (nested YAML items, not relates_to refs)
|
|
609
870
|
const children = findChildItems(foundItem, items);
|
|
610
871
|
if (children.length > 0 && !options.cascade) {
|
|
611
|
-
// AC-1
|
|
872
|
+
// AC: @spec-item-delete-children ac-1 - Block deletion if children exist without --cascade
|
|
612
873
|
const errorMsg = `Cannot delete: item has ${children.length} children. Use --cascade to delete recursively`;
|
|
613
874
|
if (isJsonMode()) {
|
|
614
|
-
// AC-10
|
|
875
|
+
// AC: @spec-item-delete-children ac-10 - JSON error includes children array
|
|
615
876
|
error(errorMsg, {
|
|
616
|
-
error:
|
|
617
|
-
children: children.map(c => ({
|
|
877
|
+
error: "has_children",
|
|
878
|
+
children: children.map((c) => ({
|
|
618
879
|
ulid: c._ulid,
|
|
619
880
|
slug: c.slugs[0],
|
|
620
881
|
title: c.title,
|
|
@@ -627,44 +888,46 @@ export function registerItemCommands(program) {
|
|
|
627
888
|
}
|
|
628
889
|
process.exit(EXIT_CODES.ERROR);
|
|
629
890
|
}
|
|
630
|
-
// AC-9
|
|
891
|
+
// AC: @spec-item-delete-children ac-9 - Custom confirmation prompt for cascade
|
|
631
892
|
if (children.length > 0 && options.cascade && !options.force) {
|
|
632
893
|
const itemRef = `@${foundItem.slugs[0] || foundItem._ulid.slice(0, 8)}`;
|
|
633
894
|
// Check for JSON mode - requires --force
|
|
634
895
|
if (isJsonMode()) {
|
|
635
|
-
error(
|
|
896
|
+
error("Confirmation required. Use --force with --json");
|
|
636
897
|
process.exit(EXIT_CODES.ERROR);
|
|
637
898
|
}
|
|
638
899
|
// Check for non-interactive environment
|
|
639
|
-
const isTTY = process.env.KSPEC_TEST_TTY ===
|
|
900
|
+
const isTTY = process.env.KSPEC_TEST_TTY === "true" || process.stdin.isTTY;
|
|
640
901
|
if (!isTTY) {
|
|
641
|
-
error(
|
|
902
|
+
error("Non-interactive environment. Use --force to proceed");
|
|
642
903
|
process.exit(EXIT_CODES.ERROR);
|
|
643
904
|
}
|
|
644
905
|
// Show confirmation prompt
|
|
645
|
-
const readline = await import(
|
|
906
|
+
const readline = await import("node:readline");
|
|
646
907
|
const rl = readline.createInterface({
|
|
647
908
|
input: process.stdin,
|
|
648
909
|
output: process.stdout,
|
|
649
910
|
});
|
|
650
|
-
const response = await new Promise(resolve => {
|
|
651
|
-
rl.question(chalk.yellow(`Delete ${itemRef} and ${children.length} descendant items? [y/N] `), answer => {
|
|
911
|
+
const response = await new Promise((resolve) => {
|
|
912
|
+
rl.question(chalk.yellow(`Delete ${itemRef} and ${children.length} descendant items? [y/N] `), (answer) => {
|
|
652
913
|
rl.close();
|
|
653
914
|
resolve(answer);
|
|
654
915
|
});
|
|
655
916
|
});
|
|
656
|
-
if (response.toLowerCase() !==
|
|
657
|
-
console.log(chalk.gray(
|
|
917
|
+
if (response.toLowerCase() !== "y") {
|
|
918
|
+
console.log(chalk.gray("Operation cancelled"));
|
|
658
919
|
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
659
920
|
}
|
|
660
921
|
}
|
|
661
|
-
// AC-2
|
|
662
|
-
const itemsToDelete = options.cascade
|
|
922
|
+
// AC: @spec-item-delete-children ac-2 ac-3 - Delete item and all descendants with cascade
|
|
923
|
+
const itemsToDelete = options.cascade
|
|
924
|
+
? [foundItem, ...children]
|
|
925
|
+
: [foundItem];
|
|
663
926
|
let deletedCount = 0;
|
|
664
927
|
// Delete in reverse order (deepest first) to avoid path issues
|
|
665
928
|
const sortedItems = [...itemsToDelete].sort((a, b) => {
|
|
666
|
-
const aDepth = a._path ? a._path.split(
|
|
667
|
-
const bDepth = b._path ? b._path.split(
|
|
929
|
+
const aDepth = a._path ? a._path.split(".").length : 0;
|
|
930
|
+
const bDepth = b._path ? b._path.split(".").length : 0;
|
|
668
931
|
return bDepth - aDepth;
|
|
669
932
|
});
|
|
670
933
|
for (const itemToDelete of sortedItems) {
|
|
@@ -674,20 +937,26 @@ export function registerItemCommands(program) {
|
|
|
674
937
|
}
|
|
675
938
|
}
|
|
676
939
|
if (deletedCount > 0) {
|
|
677
|
-
// AC-6
|
|
940
|
+
// AC: @spec-item-delete-children ac-6 - Single shadow commit with all deletions
|
|
678
941
|
const itemSlug = foundItem.slugs[0] || refIndex.shortUlid(foundItem._ulid);
|
|
679
942
|
const commitMsg = deletedCount > 1 ? `${deletedCount} items` : itemSlug;
|
|
680
|
-
await commitIfShadow(ctx.shadow,
|
|
943
|
+
await commitIfShadow(ctx.shadow, "item-delete", commitMsg);
|
|
681
944
|
if (deletedCount > 1) {
|
|
682
|
-
success(`Deleted ${deletedCount} items`, {
|
|
945
|
+
success(`Deleted ${deletedCount} items`, {
|
|
946
|
+
deleted: deletedCount,
|
|
947
|
+
root_ulid: foundItem._ulid,
|
|
948
|
+
});
|
|
683
949
|
}
|
|
684
950
|
else {
|
|
685
|
-
success(`Deleted item: ${foundItem.title}`, {
|
|
951
|
+
success(`Deleted item: ${foundItem.title}`, {
|
|
952
|
+
deleted: true,
|
|
953
|
+
ulid: foundItem._ulid,
|
|
954
|
+
});
|
|
686
955
|
}
|
|
687
956
|
}
|
|
688
957
|
else {
|
|
689
958
|
error(errors.failures.deleteItem);
|
|
690
|
-
console.log(chalk.gray(
|
|
959
|
+
console.log(chalk.gray(`Edit the source file directly: ${foundItem._sourceFile}`));
|
|
691
960
|
process.exit(EXIT_CODES.ERROR);
|
|
692
961
|
}
|
|
693
962
|
}
|
|
@@ -697,14 +966,13 @@ export function registerItemCommands(program) {
|
|
|
697
966
|
}
|
|
698
967
|
});
|
|
699
968
|
// kspec item patch - update item fields via JSON
|
|
700
|
-
item
|
|
701
|
-
.
|
|
702
|
-
.
|
|
703
|
-
.option(
|
|
704
|
-
.option(
|
|
705
|
-
.option(
|
|
706
|
-
.option(
|
|
707
|
-
.option('--fail-fast', 'Stop on first error (bulk mode)')
|
|
969
|
+
markMutating(item.command("patch [ref]"))
|
|
970
|
+
.description("Update spec item fields via JSON patch")
|
|
971
|
+
.option("--data <json>", "JSON data to patch")
|
|
972
|
+
.option("--bulk", "Read patches from stdin (JSONL or JSON array)")
|
|
973
|
+
.option("--allow-unknown", "Allow fields not in schema")
|
|
974
|
+
.option("--dry-run", "Preview changes without applying")
|
|
975
|
+
.option("--fail-fast", "Stop on first error (bulk mode)")
|
|
708
976
|
.action(async (ref, options) => {
|
|
709
977
|
try {
|
|
710
978
|
const ctx = await initContext();
|
|
@@ -735,7 +1003,7 @@ export function registerItemCommands(program) {
|
|
|
735
1003
|
});
|
|
736
1004
|
// Shadow commit if any updates
|
|
737
1005
|
if (!options.dryRun && result.summary.updated > 0) {
|
|
738
|
-
await commitIfShadow(ctx.shadow,
|
|
1006
|
+
await commitIfShadow(ctx.shadow, "item-patch", `${result.summary.updated} items`);
|
|
739
1007
|
}
|
|
740
1008
|
output(result, () => formatBulkPatchResult(result, options.dryRun));
|
|
741
1009
|
if (result.summary.failed > 0) {
|
|
@@ -755,7 +1023,7 @@ export function registerItemCommands(program) {
|
|
|
755
1023
|
data = JSON.parse(options.data);
|
|
756
1024
|
}
|
|
757
1025
|
catch (err) {
|
|
758
|
-
error(errors.validation.invalidJsonInData(err instanceof Error ? err.message :
|
|
1026
|
+
error(errors.validation.invalidJsonInData(err instanceof Error ? err.message : ""));
|
|
759
1027
|
process.exit(EXIT_CODES.ERROR);
|
|
760
1028
|
}
|
|
761
1029
|
}
|
|
@@ -766,7 +1034,7 @@ export function registerItemCommands(program) {
|
|
|
766
1034
|
data = JSON.parse(stdin.trim());
|
|
767
1035
|
}
|
|
768
1036
|
catch (err) {
|
|
769
|
-
error(errors.validation.invalidJsonFromStdin(err instanceof Error ? err.message :
|
|
1037
|
+
error(errors.validation.invalidJsonFromStdin(err instanceof Error ? err.message : ""));
|
|
770
1038
|
process.exit(EXIT_CODES.ERROR);
|
|
771
1039
|
}
|
|
772
1040
|
}
|
|
@@ -781,7 +1049,9 @@ export function registerItemCommands(program) {
|
|
|
781
1049
|
const strictSchema = SpecItemPatchSchema.strict();
|
|
782
1050
|
const parseResult = strictSchema.safeParse(data);
|
|
783
1051
|
if (!parseResult.success) {
|
|
784
|
-
const issues = parseResult.error.issues
|
|
1052
|
+
const issues = parseResult.error.issues
|
|
1053
|
+
.map((i) => `${i.path.join(".")}: ${i.message}`)
|
|
1054
|
+
.join("; ");
|
|
785
1055
|
error(errors.validation.invalidPatchDataWithIssues(issues));
|
|
786
1056
|
process.exit(EXIT_CODES.ERROR);
|
|
787
1057
|
}
|
|
@@ -794,23 +1064,28 @@ export function registerItemCommands(program) {
|
|
|
794
1064
|
process.exit(EXIT_CODES.ERROR);
|
|
795
1065
|
}
|
|
796
1066
|
// Find the item
|
|
797
|
-
const foundItem = items.find(i => i._ulid === resolved.ulid);
|
|
1067
|
+
const foundItem = items.find((i) => i._ulid === resolved.ulid);
|
|
798
1068
|
if (!foundItem) {
|
|
799
1069
|
error(errors.reference.notItem(ref));
|
|
800
1070
|
process.exit(EXIT_CODES.ERROR);
|
|
801
1071
|
}
|
|
802
1072
|
if (options.dryRun) {
|
|
803
|
-
output({
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1073
|
+
output({
|
|
1074
|
+
ref,
|
|
1075
|
+
data,
|
|
1076
|
+
wouldApplyTo: foundItem.title,
|
|
1077
|
+
ulid: foundItem._ulid,
|
|
1078
|
+
}, () => {
|
|
1079
|
+
console.log(chalk.yellow("Would patch:"), foundItem.title);
|
|
1080
|
+
console.log(chalk.gray("ULID:"), foundItem._ulid.slice(0, 8));
|
|
1081
|
+
console.log(chalk.gray("Changes:"));
|
|
807
1082
|
console.log(JSON.stringify(data, null, 2));
|
|
808
1083
|
});
|
|
809
1084
|
return;
|
|
810
1085
|
}
|
|
811
1086
|
const updated = await updateSpecItem(ctx, foundItem, data);
|
|
812
1087
|
const itemSlug = foundItem.slugs[0] || refIndex.shortUlid(foundItem._ulid);
|
|
813
|
-
await commitIfShadow(ctx.shadow,
|
|
1088
|
+
await commitIfShadow(ctx.shadow, "item-patch", itemSlug);
|
|
814
1089
|
success(`Patched item: ${itemSlug}`, { item: updated });
|
|
815
1090
|
}
|
|
816
1091
|
}
|
|
@@ -821,8 +1096,8 @@ export function registerItemCommands(program) {
|
|
|
821
1096
|
});
|
|
822
1097
|
// kspec item status - show implementation status with linked tasks
|
|
823
1098
|
item
|
|
824
|
-
.command(
|
|
825
|
-
.description(
|
|
1099
|
+
.command("status <ref>")
|
|
1100
|
+
.description("Show implementation status and linked tasks for a spec item")
|
|
826
1101
|
.action(async (ref) => {
|
|
827
1102
|
try {
|
|
828
1103
|
const ctx = await initContext();
|
|
@@ -836,7 +1111,7 @@ export function registerItemCommands(program) {
|
|
|
836
1111
|
}
|
|
837
1112
|
const foundItem = result.item;
|
|
838
1113
|
// Check if it's a task
|
|
839
|
-
if (
|
|
1114
|
+
if ("status" in foundItem && typeof foundItem.status === "string") {
|
|
840
1115
|
error(errors.reference.notItem(ref));
|
|
841
1116
|
process.exit(EXIT_CODES.ERROR);
|
|
842
1117
|
}
|
|
@@ -850,34 +1125,40 @@ export function registerItemCommands(program) {
|
|
|
850
1125
|
}
|
|
851
1126
|
output(summary, () => {
|
|
852
1127
|
console.log(chalk.bold(foundItem.title));
|
|
853
|
-
console.log(chalk.gray(
|
|
1128
|
+
console.log(chalk.gray("─".repeat(40)));
|
|
854
1129
|
// Status
|
|
855
|
-
const currentColor = summary.currentStatus ===
|
|
856
|
-
|
|
1130
|
+
const currentColor = summary.currentStatus === "implemented"
|
|
1131
|
+
? chalk.green
|
|
1132
|
+
: summary.currentStatus === "in_progress"
|
|
1133
|
+
? chalk.yellow
|
|
857
1134
|
: chalk.gray;
|
|
858
|
-
const expectedColor = summary.expectedStatus ===
|
|
859
|
-
|
|
1135
|
+
const expectedColor = summary.expectedStatus === "implemented"
|
|
1136
|
+
? chalk.green
|
|
1137
|
+
: summary.expectedStatus === "in_progress"
|
|
1138
|
+
? chalk.yellow
|
|
860
1139
|
: chalk.gray;
|
|
861
1140
|
console.log(`Current status: ${currentColor(summary.currentStatus)}`);
|
|
862
1141
|
console.log(`Expected status: ${expectedColor(summary.expectedStatus)}`);
|
|
863
1142
|
if (!summary.isAligned) {
|
|
864
|
-
console.log(chalk.yellow(
|
|
1143
|
+
console.log(chalk.yellow("\n⚠ Status mismatch - run task complete to sync"));
|
|
865
1144
|
}
|
|
866
1145
|
else {
|
|
867
|
-
console.log(chalk.green(
|
|
1146
|
+
console.log(chalk.green("\n✓ Aligned"));
|
|
868
1147
|
}
|
|
869
1148
|
// Linked tasks
|
|
870
|
-
console.log(chalk.bold(
|
|
1149
|
+
console.log(chalk.bold("\nLinked Tasks:"));
|
|
871
1150
|
if (summary.linkedTasks.length === 0) {
|
|
872
|
-
console.log(chalk.gray(
|
|
1151
|
+
console.log(chalk.gray(" No tasks reference this spec item"));
|
|
873
1152
|
}
|
|
874
1153
|
else {
|
|
875
1154
|
for (const task of summary.linkedTasks) {
|
|
876
|
-
const statusColor = task.taskStatus ===
|
|
877
|
-
|
|
1155
|
+
const statusColor = task.taskStatus === "completed"
|
|
1156
|
+
? chalk.green
|
|
1157
|
+
: task.taskStatus === "in_progress"
|
|
1158
|
+
? chalk.blue
|
|
878
1159
|
: chalk.gray;
|
|
879
1160
|
const shortId = task.taskUlid.slice(0, 8);
|
|
880
|
-
const notes = task.hasNotes ? chalk.gray(
|
|
1161
|
+
const notes = task.hasNotes ? chalk.gray(" (has notes)") : "";
|
|
881
1162
|
console.log(` ${statusColor(`[${task.taskStatus}]`)} ${shortId} ${task.taskTitle}${notes}`);
|
|
882
1163
|
}
|
|
883
1164
|
}
|
|
@@ -889,11 +1170,10 @@ export function registerItemCommands(program) {
|
|
|
889
1170
|
}
|
|
890
1171
|
});
|
|
891
1172
|
// kspec item note <ref> <message>
|
|
892
|
-
item
|
|
893
|
-
.
|
|
894
|
-
.
|
|
895
|
-
.option(
|
|
896
|
-
.option('--supersedes <ulid>', 'ULID of note this supersedes')
|
|
1173
|
+
markMutating(item.command("note <ref> <message>"))
|
|
1174
|
+
.description("Add a note to a spec item")
|
|
1175
|
+
.option("--author <author>", "Note author")
|
|
1176
|
+
.option("--supersedes <ulid>", "ULID of note this supersedes")
|
|
897
1177
|
.action(async (ref, message, options) => {
|
|
898
1178
|
try {
|
|
899
1179
|
const ctx = await initContext();
|
|
@@ -905,7 +1185,7 @@ export function registerItemCommands(program) {
|
|
|
905
1185
|
error(errors.reference.itemNotFound(ref));
|
|
906
1186
|
process.exit(EXIT_CODES.ERROR);
|
|
907
1187
|
}
|
|
908
|
-
const foundItem = items.find(i => i._ulid === result.ulid);
|
|
1188
|
+
const foundItem = items.find((i) => i._ulid === result.ulid);
|
|
909
1189
|
if (!foundItem) {
|
|
910
1190
|
error(errors.reference.itemNotFound(ref));
|
|
911
1191
|
process.exit(EXIT_CODES.ERROR);
|
|
@@ -914,7 +1194,7 @@ export function registerItemCommands(program) {
|
|
|
914
1194
|
const updatedNotes = [...(foundItem.notes || []), note];
|
|
915
1195
|
await updateSpecItem(ctx, foundItem, { notes: updatedNotes });
|
|
916
1196
|
const itemSlug = foundItem.slugs[0] || refIndex.shortUlid(foundItem._ulid);
|
|
917
|
-
await commitIfShadow(ctx.shadow,
|
|
1197
|
+
await commitIfShadow(ctx.shadow, "item-note", itemSlug);
|
|
918
1198
|
success(`Added note to spec item: ${refIndex.shortUlid(foundItem._ulid)}`, { note });
|
|
919
1199
|
}
|
|
920
1200
|
catch (err) {
|
|
@@ -924,8 +1204,8 @@ export function registerItemCommands(program) {
|
|
|
924
1204
|
});
|
|
925
1205
|
// kspec item notes <ref>
|
|
926
1206
|
item
|
|
927
|
-
.command(
|
|
928
|
-
.description(
|
|
1207
|
+
.command("notes <ref>")
|
|
1208
|
+
.description("Show notes for a spec item")
|
|
929
1209
|
.action(async (ref) => {
|
|
930
1210
|
try {
|
|
931
1211
|
const ctx = await initContext();
|
|
@@ -937,7 +1217,7 @@ export function registerItemCommands(program) {
|
|
|
937
1217
|
error(errors.reference.itemNotFound(ref));
|
|
938
1218
|
process.exit(EXIT_CODES.ERROR);
|
|
939
1219
|
}
|
|
940
|
-
const foundItem = items.find(i => i._ulid === result.ulid);
|
|
1220
|
+
const foundItem = items.find((i) => i._ulid === result.ulid);
|
|
941
1221
|
if (!foundItem) {
|
|
942
1222
|
error(errors.reference.itemNotFound(ref));
|
|
943
1223
|
process.exit(EXIT_CODES.ERROR);
|
|
@@ -945,14 +1225,14 @@ export function registerItemCommands(program) {
|
|
|
945
1225
|
const notes = foundItem.notes || [];
|
|
946
1226
|
output(notes, () => {
|
|
947
1227
|
if (notes.length === 0) {
|
|
948
|
-
console.log(
|
|
1228
|
+
console.log("No notes");
|
|
949
1229
|
}
|
|
950
1230
|
else {
|
|
951
1231
|
for (const note of notes) {
|
|
952
|
-
const author = note.author ||
|
|
1232
|
+
const author = note.author || "unknown";
|
|
953
1233
|
console.log(`[${note.created_at}] ${author}:`);
|
|
954
1234
|
console.log(note.content);
|
|
955
|
-
console.log(
|
|
1235
|
+
console.log("");
|
|
956
1236
|
}
|
|
957
1237
|
}
|
|
958
1238
|
});
|
|
@@ -964,14 +1244,14 @@ export function registerItemCommands(program) {
|
|
|
964
1244
|
});
|
|
965
1245
|
// Create subcommand group for acceptance criteria operations
|
|
966
1246
|
const acCmd = item
|
|
967
|
-
.command(
|
|
968
|
-
.description(
|
|
1247
|
+
.command("ac")
|
|
1248
|
+
.description("Manage acceptance criteria on spec items");
|
|
969
1249
|
// Helper: Generate next AC ID based on existing AC
|
|
970
1250
|
function generateNextAcId(existingAc) {
|
|
971
1251
|
if (!existingAc || existingAc.length === 0)
|
|
972
|
-
return
|
|
1252
|
+
return "ac-1";
|
|
973
1253
|
const numericIds = existingAc
|
|
974
|
-
.map(ac => ac.id.match(/^ac-(\d+)$/)?.[1])
|
|
1254
|
+
.map((ac) => ac.id.match(/^ac-(\d+)$/)?.[1])
|
|
975
1255
|
.filter((id) => id !== null && id !== undefined)
|
|
976
1256
|
.map(Number);
|
|
977
1257
|
const maxId = numericIds.length > 0 ? Math.max(...numericIds) : 0;
|
|
@@ -988,7 +1268,7 @@ export function registerItemCommands(program) {
|
|
|
988
1268
|
}
|
|
989
1269
|
const foundItem = result.item;
|
|
990
1270
|
// Check if it's a task
|
|
991
|
-
if (
|
|
1271
|
+
if ("status" in foundItem && typeof foundItem.status === "string") {
|
|
992
1272
|
error(errors.operation.tasksNoAcceptanceCriteria(ref));
|
|
993
1273
|
process.exit(EXIT_CODES.NOT_FOUND);
|
|
994
1274
|
}
|
|
@@ -996,8 +1276,8 @@ export function registerItemCommands(program) {
|
|
|
996
1276
|
}
|
|
997
1277
|
// kspec item ac list <ref>
|
|
998
1278
|
acCmd
|
|
999
|
-
.command(
|
|
1000
|
-
.description(
|
|
1279
|
+
.command("list <ref>")
|
|
1280
|
+
.description("List acceptance criteria for a spec item")
|
|
1001
1281
|
.action(async (ref) => {
|
|
1002
1282
|
try {
|
|
1003
1283
|
const { item, refIndex } = await resolveSpecItem(ref);
|
|
@@ -1006,7 +1286,7 @@ export function registerItemCommands(program) {
|
|
|
1006
1286
|
console.log(chalk.bold(`Acceptance Criteria for: ${item.title} (@${item.slugs[0] || refIndex.shortUlid(item._ulid)})`));
|
|
1007
1287
|
console.log();
|
|
1008
1288
|
if (ac.length === 0) {
|
|
1009
|
-
console.log(chalk.gray(
|
|
1289
|
+
console.log(chalk.gray("No acceptance criteria"));
|
|
1010
1290
|
}
|
|
1011
1291
|
else {
|
|
1012
1292
|
for (const criterion of ac) {
|
|
@@ -1026,13 +1306,12 @@ export function registerItemCommands(program) {
|
|
|
1026
1306
|
}
|
|
1027
1307
|
});
|
|
1028
1308
|
// kspec item ac add <ref>
|
|
1029
|
-
acCmd
|
|
1030
|
-
.
|
|
1031
|
-
.
|
|
1032
|
-
.
|
|
1033
|
-
.requiredOption(
|
|
1034
|
-
.requiredOption(
|
|
1035
|
-
.requiredOption('--then <text>', 'The expected outcome (Then...)')
|
|
1309
|
+
markMutating(acCmd.command("add <ref>"))
|
|
1310
|
+
.description("Add an acceptance criterion to a spec item")
|
|
1311
|
+
.option("--id <id>", "AC identifier (auto-generated if not provided)")
|
|
1312
|
+
.requiredOption("--given <text>", "The precondition (Given...)")
|
|
1313
|
+
.requiredOption("--when <text>", "The action/trigger (When...)")
|
|
1314
|
+
.requiredOption("--then <text>", "The expected outcome (Then...)")
|
|
1036
1315
|
.action(async (ref, options) => {
|
|
1037
1316
|
try {
|
|
1038
1317
|
const { ctx, item, refIndex } = await resolveSpecItem(ref);
|
|
@@ -1040,7 +1319,7 @@ export function registerItemCommands(program) {
|
|
|
1040
1319
|
// Determine ID
|
|
1041
1320
|
const acId = options.id || generateNextAcId(existingAc);
|
|
1042
1321
|
// Check for duplicate ID
|
|
1043
|
-
if (existingAc.some(ac => ac.id === acId)) {
|
|
1322
|
+
if (existingAc.some((ac) => ac.id === acId)) {
|
|
1044
1323
|
const itemRef = item.slugs[0] || refIndex.shortUlid(item._ulid);
|
|
1045
1324
|
error(errors.conflict.acAlreadyExists(acId, itemRef));
|
|
1046
1325
|
process.exit(EXIT_CODES.CONFLICT);
|
|
@@ -1056,8 +1335,10 @@ export function registerItemCommands(program) {
|
|
|
1056
1335
|
const updatedAc = [...existingAc, newAc];
|
|
1057
1336
|
await updateSpecItem(ctx, item, { acceptance_criteria: updatedAc });
|
|
1058
1337
|
const itemSlug = item.slugs[0] || refIndex.shortUlid(item._ulid);
|
|
1059
|
-
await commitIfShadow(ctx.shadow,
|
|
1060
|
-
success(`Added acceptance criterion: ${acId} to @${itemSlug}`, {
|
|
1338
|
+
await commitIfShadow(ctx.shadow, "item-ac-add", itemSlug);
|
|
1339
|
+
success(`Added acceptance criterion: ${acId} to @${itemSlug}`, {
|
|
1340
|
+
ac: newAc,
|
|
1341
|
+
});
|
|
1061
1342
|
}
|
|
1062
1343
|
catch (err) {
|
|
1063
1344
|
error(errors.failures.addAc, err);
|
|
@@ -1065,19 +1346,18 @@ export function registerItemCommands(program) {
|
|
|
1065
1346
|
}
|
|
1066
1347
|
});
|
|
1067
1348
|
// kspec item ac set <ref> <ac-id>
|
|
1068
|
-
acCmd
|
|
1069
|
-
.
|
|
1070
|
-
.
|
|
1071
|
-
.option(
|
|
1072
|
-
.option(
|
|
1073
|
-
.option(
|
|
1074
|
-
.option('--then <text>', 'Update the expected outcome')
|
|
1349
|
+
markMutating(acCmd.command("set <ref> <acId>"))
|
|
1350
|
+
.description("Update an acceptance criterion")
|
|
1351
|
+
.option("--id <newId>", "Rename the AC ID")
|
|
1352
|
+
.option("--given <text>", "Update the precondition")
|
|
1353
|
+
.option("--when <text>", "Update the action/trigger")
|
|
1354
|
+
.option("--then <text>", "Update the expected outcome")
|
|
1075
1355
|
.action(async (ref, acId, options) => {
|
|
1076
1356
|
try {
|
|
1077
1357
|
const { ctx, item, refIndex } = await resolveSpecItem(ref);
|
|
1078
1358
|
const existingAc = item.acceptance_criteria || [];
|
|
1079
1359
|
// Find the AC
|
|
1080
|
-
const acIndex = existingAc.findIndex(ac => ac.id === acId);
|
|
1360
|
+
const acIndex = existingAc.findIndex((ac) => ac.id === acId);
|
|
1081
1361
|
if (acIndex === -1) {
|
|
1082
1362
|
const itemRef = item.slugs[0] || refIndex.shortUlid(item._ulid);
|
|
1083
1363
|
error(errors.reference.acNotFound(acId, itemRef));
|
|
@@ -1085,11 +1365,13 @@ export function registerItemCommands(program) {
|
|
|
1085
1365
|
}
|
|
1086
1366
|
// Check for no updates
|
|
1087
1367
|
if (!options.id && !options.given && !options.when && !options.then) {
|
|
1088
|
-
warn(
|
|
1368
|
+
warn("No updates specified");
|
|
1089
1369
|
return;
|
|
1090
1370
|
}
|
|
1091
1371
|
// Check for duplicate ID if renaming
|
|
1092
|
-
if (options.id &&
|
|
1372
|
+
if (options.id &&
|
|
1373
|
+
options.id !== acId &&
|
|
1374
|
+
existingAc.some((ac) => ac.id === options.id)) {
|
|
1093
1375
|
error(errors.conflict.acIdAlreadyExists(options.id));
|
|
1094
1376
|
process.exit(EXIT_CODES.CONFLICT);
|
|
1095
1377
|
}
|
|
@@ -1104,18 +1386,18 @@ export function registerItemCommands(program) {
|
|
|
1104
1386
|
...(options.then && { then: options.then }),
|
|
1105
1387
|
};
|
|
1106
1388
|
if (options.id)
|
|
1107
|
-
updatedFields.push(
|
|
1389
|
+
updatedFields.push("id");
|
|
1108
1390
|
if (options.given)
|
|
1109
|
-
updatedFields.push(
|
|
1391
|
+
updatedFields.push("given");
|
|
1110
1392
|
if (options.when)
|
|
1111
|
-
updatedFields.push(
|
|
1393
|
+
updatedFields.push("when");
|
|
1112
1394
|
if (options.then)
|
|
1113
|
-
updatedFields.push(
|
|
1395
|
+
updatedFields.push("then");
|
|
1114
1396
|
// Update item
|
|
1115
1397
|
await updateSpecItem(ctx, item, { acceptance_criteria: updatedAc });
|
|
1116
1398
|
const itemSlug = item.slugs[0] || refIndex.shortUlid(item._ulid);
|
|
1117
|
-
await commitIfShadow(ctx.shadow,
|
|
1118
|
-
success(`Updated acceptance criterion: ${acId} on @${itemSlug} (${updatedFields.join(
|
|
1399
|
+
await commitIfShadow(ctx.shadow, "item-ac-set", itemSlug);
|
|
1400
|
+
success(`Updated acceptance criterion: ${acId} on @${itemSlug} (${updatedFields.join(", ")})`, { ac: updatedAc[acIndex] });
|
|
1119
1401
|
}
|
|
1120
1402
|
catch (err) {
|
|
1121
1403
|
error(errors.failures.updateAc, err);
|
|
@@ -1123,16 +1405,15 @@ export function registerItemCommands(program) {
|
|
|
1123
1405
|
}
|
|
1124
1406
|
});
|
|
1125
1407
|
// kspec item ac remove <ref> <ac-id>
|
|
1126
|
-
acCmd
|
|
1127
|
-
.
|
|
1128
|
-
.
|
|
1129
|
-
.option('--force', 'Skip confirmation')
|
|
1408
|
+
markMutating(acCmd.command("remove <ref> <acId>"))
|
|
1409
|
+
.description("Remove an acceptance criterion")
|
|
1410
|
+
.option("--force", "Skip confirmation")
|
|
1130
1411
|
.action(async (ref, acId, options) => {
|
|
1131
1412
|
try {
|
|
1132
1413
|
const { ctx, item, refIndex } = await resolveSpecItem(ref);
|
|
1133
1414
|
const existingAc = item.acceptance_criteria || [];
|
|
1134
1415
|
// Find the AC
|
|
1135
|
-
const acIndex = existingAc.findIndex(ac => ac.id === acId);
|
|
1416
|
+
const acIndex = existingAc.findIndex((ac) => ac.id === acId);
|
|
1136
1417
|
if (acIndex === -1) {
|
|
1137
1418
|
const itemRef = item.slugs[0] || refIndex.shortUlid(item._ulid);
|
|
1138
1419
|
error(errors.reference.acNotFound(acId, itemRef));
|
|
@@ -1140,20 +1421,20 @@ export function registerItemCommands(program) {
|
|
|
1140
1421
|
}
|
|
1141
1422
|
// Confirmation required unless --force
|
|
1142
1423
|
if (!options.force) {
|
|
1143
|
-
// AC-5
|
|
1424
|
+
// AC: @spec-item-delete-children ac-5 - JSON mode requires --force
|
|
1144
1425
|
if (isJsonMode()) {
|
|
1145
|
-
error(
|
|
1426
|
+
error("Confirmation required. Use --force with --json");
|
|
1146
1427
|
process.exit(EXIT_CODES.ERROR);
|
|
1147
1428
|
}
|
|
1148
|
-
// AC-6
|
|
1429
|
+
// AC: @spec-item-delete-children ac-6 - Non-interactive environment requires --force
|
|
1149
1430
|
// Allow KSPEC_TEST_TTY for testing interactive prompts
|
|
1150
|
-
const isTTY = process.env.KSPEC_TEST_TTY ===
|
|
1431
|
+
const isTTY = process.env.KSPEC_TEST_TTY === "1" || process.stdin.isTTY;
|
|
1151
1432
|
if (!isTTY) {
|
|
1152
|
-
error(
|
|
1433
|
+
error("Non-interactive environment. Use --force to proceed");
|
|
1153
1434
|
process.exit(EXIT_CODES.ERROR);
|
|
1154
1435
|
}
|
|
1155
|
-
// AC-1
|
|
1156
|
-
const readline = await import(
|
|
1436
|
+
// AC: @spec-item-delete-children ac-1 - Prompt for confirmation
|
|
1437
|
+
const readline = await import("node:readline");
|
|
1157
1438
|
const rl = readline.createInterface({
|
|
1158
1439
|
input: process.stdin,
|
|
1159
1440
|
output: process.stdout,
|
|
@@ -1162,19 +1443,21 @@ export function registerItemCommands(program) {
|
|
|
1162
1443
|
rl.question(`Remove acceptance criterion ${acId}? [y/N] `, resolve);
|
|
1163
1444
|
});
|
|
1164
1445
|
rl.close();
|
|
1165
|
-
// AC-3
|
|
1166
|
-
if (answer.toLowerCase() !==
|
|
1167
|
-
error(
|
|
1446
|
+
// AC: @spec-item-delete-children ac-3 - User declines (n, N, or empty)
|
|
1447
|
+
if (answer.toLowerCase() !== "y") {
|
|
1448
|
+
error("Operation cancelled");
|
|
1168
1449
|
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
1169
1450
|
}
|
|
1170
1451
|
}
|
|
1171
|
-
// AC-4
|
|
1172
|
-
// AC-2
|
|
1173
|
-
const updatedAc = existingAc.filter(ac => ac.id !== acId);
|
|
1452
|
+
// AC: @spec-item-delete-children ac-4 - With --force, proceed immediately without prompt
|
|
1453
|
+
// AC: @spec-item-delete-children ac-2 - User confirmed, proceed with removal
|
|
1454
|
+
const updatedAc = existingAc.filter((ac) => ac.id !== acId);
|
|
1174
1455
|
await updateSpecItem(ctx, item, { acceptance_criteria: updatedAc });
|
|
1175
1456
|
const itemSlug = item.slugs[0] || refIndex.shortUlid(item._ulid);
|
|
1176
|
-
await commitIfShadow(ctx.shadow,
|
|
1177
|
-
success(`Removed acceptance criterion: ${acId} from @${itemSlug}`, {
|
|
1457
|
+
await commitIfShadow(ctx.shadow, "item-ac-remove", itemSlug);
|
|
1458
|
+
success(`Removed acceptance criterion: ${acId} from @${itemSlug}`, {
|
|
1459
|
+
removed: acId,
|
|
1460
|
+
});
|
|
1178
1461
|
}
|
|
1179
1462
|
catch (err) {
|
|
1180
1463
|
error(errors.failures.removeAc, err);
|
|
@@ -1192,20 +1475,20 @@ async function readStdinFully() {
|
|
|
1192
1475
|
return null;
|
|
1193
1476
|
}
|
|
1194
1477
|
return new Promise((resolve) => {
|
|
1195
|
-
let data =
|
|
1478
|
+
let data = "";
|
|
1196
1479
|
const timeout = setTimeout(() => {
|
|
1197
1480
|
process.stdin.removeAllListeners();
|
|
1198
1481
|
resolve(data || null);
|
|
1199
1482
|
}, 5000); // 5 second timeout for bulk input
|
|
1200
|
-
process.stdin.setEncoding(
|
|
1201
|
-
process.stdin.on(
|
|
1483
|
+
process.stdin.setEncoding("utf8");
|
|
1484
|
+
process.stdin.on("data", (chunk) => {
|
|
1202
1485
|
data += chunk;
|
|
1203
1486
|
});
|
|
1204
|
-
process.stdin.on(
|
|
1487
|
+
process.stdin.on("end", () => {
|
|
1205
1488
|
clearTimeout(timeout);
|
|
1206
1489
|
resolve(data || null);
|
|
1207
1490
|
});
|
|
1208
|
-
process.stdin.on(
|
|
1491
|
+
process.stdin.on("error", () => {
|
|
1209
1492
|
clearTimeout(timeout);
|
|
1210
1493
|
resolve(null);
|
|
1211
1494
|
});
|
|
@@ -1221,20 +1504,20 @@ async function readStdinIfAvailable() {
|
|
|
1221
1504
|
return null;
|
|
1222
1505
|
}
|
|
1223
1506
|
return new Promise((resolve) => {
|
|
1224
|
-
let data =
|
|
1507
|
+
let data = "";
|
|
1225
1508
|
const timeout = setTimeout(() => {
|
|
1226
1509
|
process.stdin.removeAllListeners();
|
|
1227
1510
|
resolve(data || null);
|
|
1228
1511
|
}, 100); // 100ms timeout for quick check
|
|
1229
|
-
process.stdin.setEncoding(
|
|
1230
|
-
process.stdin.on(
|
|
1512
|
+
process.stdin.setEncoding("utf8");
|
|
1513
|
+
process.stdin.on("data", (chunk) => {
|
|
1231
1514
|
data += chunk;
|
|
1232
1515
|
});
|
|
1233
|
-
process.stdin.on(
|
|
1516
|
+
process.stdin.on("end", () => {
|
|
1234
1517
|
clearTimeout(timeout);
|
|
1235
1518
|
resolve(data || null);
|
|
1236
1519
|
});
|
|
1237
|
-
process.stdin.on(
|
|
1520
|
+
process.stdin.on("error", () => {
|
|
1238
1521
|
clearTimeout(timeout);
|
|
1239
1522
|
resolve(null);
|
|
1240
1523
|
});
|
|
@@ -1247,7 +1530,7 @@ async function readStdinIfAvailable() {
|
|
|
1247
1530
|
function parseBulkInput(input) {
|
|
1248
1531
|
const trimmed = input.trim();
|
|
1249
1532
|
// Try JSON array first
|
|
1250
|
-
if (trimmed.startsWith(
|
|
1533
|
+
if (trimmed.startsWith("[")) {
|
|
1251
1534
|
const parsed = JSON.parse(trimmed);
|
|
1252
1535
|
if (!Array.isArray(parsed)) {
|
|
1253
1536
|
throw new Error(errors.validation.expectedJsonArray);
|
|
@@ -1255,13 +1538,13 @@ function parseBulkInput(input) {
|
|
|
1255
1538
|
return parsed.map((item, i) => validatePatchOperation(item, i));
|
|
1256
1539
|
}
|
|
1257
1540
|
// Parse as JSONL (one JSON object per line)
|
|
1258
|
-
const lines = trimmed.split(
|
|
1541
|
+
const lines = trimmed.split("\n").filter((line) => line.trim());
|
|
1259
1542
|
return lines.map((line, i) => {
|
|
1260
1543
|
try {
|
|
1261
1544
|
return validatePatchOperation(JSON.parse(line), i);
|
|
1262
1545
|
}
|
|
1263
1546
|
catch (err) {
|
|
1264
|
-
throw new Error(errors.validation.jsonLineError(i + 1, err instanceof Error ? err.message :
|
|
1547
|
+
throw new Error(errors.validation.jsonLineError(i + 1, err instanceof Error ? err.message : "Invalid JSON"));
|
|
1265
1548
|
}
|
|
1266
1549
|
});
|
|
1267
1550
|
}
|
|
@@ -1269,14 +1552,14 @@ function parseBulkInput(input) {
|
|
|
1269
1552
|
* Validate a patch operation object
|
|
1270
1553
|
*/
|
|
1271
1554
|
function validatePatchOperation(obj, index) {
|
|
1272
|
-
if (!obj || typeof obj !==
|
|
1555
|
+
if (!obj || typeof obj !== "object") {
|
|
1273
1556
|
throw new Error(errors.validation.patchMustBeObject(index));
|
|
1274
1557
|
}
|
|
1275
1558
|
const op = obj;
|
|
1276
|
-
if (typeof op.ref !==
|
|
1559
|
+
if (typeof op.ref !== "string" || !op.ref) {
|
|
1277
1560
|
throw new Error(errors.validation.patchMustHaveRef(index));
|
|
1278
1561
|
}
|
|
1279
|
-
if (!op.data || typeof op.data !==
|
|
1562
|
+
if (!op.data || typeof op.data !== "object") {
|
|
1280
1563
|
throw new Error(errors.validation.patchMustHaveData(index));
|
|
1281
1564
|
}
|
|
1282
1565
|
return { ref: op.ref, data: op.data };
|
|
@@ -1285,20 +1568,20 @@ function validatePatchOperation(obj, index) {
|
|
|
1285
1568
|
* Format bulk patch result for human output
|
|
1286
1569
|
*/
|
|
1287
1570
|
function formatBulkPatchResult(result, isDryRun = false) {
|
|
1288
|
-
const prefix = isDryRun ?
|
|
1571
|
+
const prefix = isDryRun ? "Would patch" : "Patched";
|
|
1289
1572
|
for (const r of result.results) {
|
|
1290
|
-
if (r.status ===
|
|
1291
|
-
console.log(chalk.green(
|
|
1573
|
+
if (r.status === "updated") {
|
|
1574
|
+
console.log(chalk.green("OK"), `${prefix}: ${r.ref} (${r.ulid?.slice(0, 8)})`);
|
|
1292
1575
|
}
|
|
1293
|
-
else if (r.status ===
|
|
1294
|
-
console.log(chalk.red(
|
|
1576
|
+
else if (r.status === "error") {
|
|
1577
|
+
console.log(chalk.red("ERR"), `${r.ref}: ${r.error}`);
|
|
1295
1578
|
}
|
|
1296
1579
|
else {
|
|
1297
|
-
console.log(chalk.gray(
|
|
1580
|
+
console.log(chalk.gray("SKIP"), r.ref);
|
|
1298
1581
|
}
|
|
1299
1582
|
}
|
|
1300
|
-
console.log(
|
|
1301
|
-
console.log(chalk.bold(
|
|
1583
|
+
console.log("");
|
|
1584
|
+
console.log(chalk.bold("Summary:"));
|
|
1302
1585
|
console.log(` Total: ${result.summary.total}`);
|
|
1303
1586
|
console.log(chalk.green(` Updated: ${result.summary.updated}`));
|
|
1304
1587
|
if (result.summary.failed > 0) {
|