@kynetic-ai/spec 0.1.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +250 -17
- package/dist/acp/client.d.ts +18 -4
- package/dist/acp/client.d.ts.map +1 -1
- package/dist/acp/client.js +44 -26
- package/dist/acp/client.js.map +1 -1
- package/dist/acp/framing.d.ts +2 -2
- package/dist/acp/framing.d.ts.map +1 -1
- package/dist/acp/framing.js +37 -29
- package/dist/acp/framing.js.map +1 -1
- package/dist/acp/index.d.ts +6 -7
- package/dist/acp/index.d.ts.map +1 -1
- package/dist/acp/index.js +3 -3
- package/dist/acp/index.js.map +1 -1
- package/dist/acp/types.d.ts +5 -5
- package/dist/acp/types.d.ts.map +1 -1
- package/dist/acp/types.js +18 -18
- package/dist/acp/types.js.map +1 -1
- package/dist/agents/adapters.d.ts.map +1 -1
- package/dist/agents/adapters.js +24 -13
- package/dist/agents/adapters.js.map +1 -1
- package/dist/agents/index.d.ts +2 -2
- package/dist/agents/index.js +2 -2
- package/dist/agents/spawner.d.ts +4 -4
- package/dist/agents/spawner.d.ts.map +1 -1
- package/dist/agents/spawner.js +6 -6
- package/dist/agents/spawner.js.map +1 -1
- package/dist/cli/batch-context.d.ts +43 -0
- package/dist/cli/batch-context.d.ts.map +1 -0
- package/dist/cli/batch-context.js +93 -0
- package/dist/cli/batch-context.js.map +1 -0
- package/dist/cli/batch-exec.d.ts +107 -0
- package/dist/cli/batch-exec.d.ts.map +1 -0
- package/dist/cli/batch-exec.js +706 -0
- package/dist/cli/batch-exec.js.map +1 -0
- package/dist/cli/batch.d.ts +4 -2
- package/dist/cli/batch.d.ts.map +1 -1
- package/dist/cli/batch.js +15 -14
- package/dist/cli/batch.js.map +1 -1
- package/dist/cli/command-annotations.d.ts +23 -0
- package/dist/cli/command-annotations.d.ts.map +1 -0
- package/dist/cli/command-annotations.js +27 -0
- package/dist/cli/command-annotations.js.map +1 -0
- package/dist/cli/commands/agents.d.ts +46 -0
- package/dist/cli/commands/agents.d.ts.map +1 -0
- package/dist/cli/commands/agents.js +377 -0
- package/dist/cli/commands/agents.js.map +1 -0
- package/dist/cli/commands/batch.d.ts +20 -0
- package/dist/cli/commands/batch.d.ts.map +1 -0
- package/dist/cli/commands/batch.js +214 -0
- package/dist/cli/commands/batch.js.map +1 -0
- package/dist/cli/commands/clone-for-testing.d.ts +1 -1
- package/dist/cli/commands/clone-for-testing.d.ts.map +1 -1
- package/dist/cli/commands/clone-for-testing.js +37 -47
- package/dist/cli/commands/clone-for-testing.js.map +1 -1
- package/dist/cli/commands/derive.d.ts +1 -1
- package/dist/cli/commands/derive.d.ts.map +1 -1
- package/dist/cli/commands/derive.js +141 -88
- package/dist/cli/commands/derive.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +11 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +152 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/export.d.ts +12 -0
- package/dist/cli/commands/export.d.ts.map +1 -0
- package/dist/cli/commands/export.js +134 -0
- package/dist/cli/commands/export.js.map +1 -0
- package/dist/cli/commands/help.d.ts +1 -1
- package/dist/cli/commands/help.d.ts.map +1 -1
- package/dist/cli/commands/help.js +163 -37
- package/dist/cli/commands/help.js.map +1 -1
- package/dist/cli/commands/inbox.d.ts +1 -1
- package/dist/cli/commands/inbox.d.ts.map +1 -1
- package/dist/cli/commands/inbox.js +178 -56
- package/dist/cli/commands/inbox.js.map +1 -1
- package/dist/cli/commands/index.d.ts +31 -19
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +31 -19
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts +5 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +108 -57
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/item.d.ts +1 -1
- package/dist/cli/commands/item.d.ts.map +1 -1
- package/dist/cli/commands/item.js +557 -274
- package/dist/cli/commands/item.js.map +1 -1
- package/dist/cli/commands/link.d.ts +1 -1
- package/dist/cli/commands/link.d.ts.map +1 -1
- package/dist/cli/commands/link.js +55 -46
- package/dist/cli/commands/link.js.map +1 -1
- package/dist/cli/commands/log.d.ts +1 -1
- package/dist/cli/commands/log.d.ts.map +1 -1
- package/dist/cli/commands/log.js +58 -51
- package/dist/cli/commands/log.js.map +1 -1
- package/dist/cli/commands/merge-driver.d.ts +19 -0
- package/dist/cli/commands/merge-driver.d.ts.map +1 -0
- package/dist/cli/commands/merge-driver.js +398 -0
- package/dist/cli/commands/merge-driver.js.map +1 -0
- package/dist/cli/commands/meta.d.ts +1 -1
- package/dist/cli/commands/meta.d.ts.map +1 -1
- package/dist/cli/commands/meta.js +534 -399
- package/dist/cli/commands/meta.js.map +1 -1
- package/dist/cli/commands/module.d.ts +1 -1
- package/dist/cli/commands/module.d.ts.map +1 -1
- package/dist/cli/commands/module.js +30 -25
- package/dist/cli/commands/module.js.map +1 -1
- package/dist/cli/commands/plan-import.d.ts +11 -0
- package/dist/cli/commands/plan-import.d.ts.map +1 -0
- package/dist/cli/commands/plan-import.js +547 -0
- package/dist/cli/commands/plan-import.js.map +1 -0
- package/dist/cli/commands/plan.d.ts +10 -0
- package/dist/cli/commands/plan.d.ts.map +1 -0
- package/dist/cli/commands/plan.js +421 -0
- package/dist/cli/commands/plan.js.map +1 -0
- package/dist/cli/commands/ralph.d.ts +1 -1
- package/dist/cli/commands/ralph.d.ts.map +1 -1
- package/dist/cli/commands/ralph.js +1109 -170
- package/dist/cli/commands/ralph.js.map +1 -1
- package/dist/cli/commands/refs.d.ts +13 -0
- package/dist/cli/commands/refs.d.ts.map +1 -0
- package/dist/cli/commands/refs.js +283 -0
- package/dist/cli/commands/refs.js.map +1 -0
- package/dist/cli/commands/search.d.ts +1 -1
- package/dist/cli/commands/search.d.ts.map +1 -1
- package/dist/cli/commands/search.js +199 -37
- package/dist/cli/commands/search.js.map +1 -1
- package/dist/cli/commands/serve.d.ts +10 -0
- package/dist/cli/commands/serve.d.ts.map +1 -0
- package/dist/cli/commands/serve.js +491 -0
- package/dist/cli/commands/serve.js.map +1 -0
- package/dist/cli/commands/session.d.ts +25 -6
- package/dist/cli/commands/session.d.ts.map +1 -1
- package/dist/cli/commands/session.js +810 -127
- package/dist/cli/commands/session.js.map +1 -1
- package/dist/cli/commands/setup-seeding.d.ts +81 -0
- package/dist/cli/commands/setup-seeding.d.ts.map +1 -0
- package/dist/cli/commands/setup-seeding.js +292 -0
- package/dist/cli/commands/setup-seeding.js.map +1 -0
- package/dist/cli/commands/setup.d.ts +77 -3
- package/dist/cli/commands/setup.d.ts.map +1 -1
- package/dist/cli/commands/setup.js +1267 -274
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/shadow.d.ts +1 -1
- package/dist/cli/commands/shadow.d.ts.map +1 -1
- package/dist/cli/commands/shadow.js +70 -50
- package/dist/cli/commands/shadow.js.map +1 -1
- package/dist/cli/commands/skill-crud.d.ts +58 -0
- package/dist/cli/commands/skill-crud.d.ts.map +1 -0
- package/dist/cli/commands/skill-crud.js +753 -0
- package/dist/cli/commands/skill-crud.js.map +1 -0
- package/dist/cli/commands/skill-diff.d.ts +27 -0
- package/dist/cli/commands/skill-diff.d.ts.map +1 -0
- package/dist/cli/commands/skill-diff.js +840 -0
- package/dist/cli/commands/skill-diff.js.map +1 -0
- package/dist/cli/commands/skill-install.d.ts +56 -0
- package/dist/cli/commands/skill-install.d.ts.map +1 -0
- package/dist/cli/commands/skill-install.js +509 -0
- package/dist/cli/commands/skill-install.js.map +1 -0
- package/dist/cli/commands/skill.d.ts +20 -0
- package/dist/cli/commands/skill.d.ts.map +1 -0
- package/dist/cli/commands/skill.js +36 -0
- package/dist/cli/commands/skill.js.map +1 -0
- package/dist/cli/commands/task.d.ts +1 -1
- package/dist/cli/commands/task.d.ts.map +1 -1
- package/dist/cli/commands/task.js +584 -350
- package/dist/cli/commands/task.js.map +1 -1
- package/dist/cli/commands/tasks.d.ts +26 -1
- package/dist/cli/commands/tasks.d.ts.map +1 -1
- package/dist/cli/commands/tasks.js +225 -122
- package/dist/cli/commands/tasks.js.map +1 -1
- package/dist/cli/commands/trait.d.ts +1 -1
- package/dist/cli/commands/trait.d.ts.map +1 -1
- package/dist/cli/commands/trait.js +166 -101
- package/dist/cli/commands/trait.js.map +1 -1
- package/dist/cli/commands/triage.d.ts +7 -0
- package/dist/cli/commands/triage.d.ts.map +1 -0
- package/dist/cli/commands/triage.js +483 -0
- package/dist/cli/commands/triage.js.map +1 -0
- package/dist/cli/commands/util.d.ts +7 -0
- package/dist/cli/commands/util.d.ts.map +1 -0
- package/dist/cli/commands/util.js +30 -0
- package/dist/cli/commands/util.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +1 -1
- package/dist/cli/commands/validate.d.ts.map +1 -1
- package/dist/cli/commands/validate.js +264 -83
- package/dist/cli/commands/validate.js.map +1 -1
- package/dist/cli/commands/workflow.d.ts +16 -0
- package/dist/cli/commands/workflow.d.ts.map +1 -0
- package/dist/cli/commands/workflow.js +851 -0
- package/dist/cli/commands/workflow.js.map +1 -0
- package/dist/cli/exit-codes.d.ts +7 -0
- package/dist/cli/exit-codes.d.ts.map +1 -1
- package/dist/cli/exit-codes.js +26 -18
- package/dist/cli/exit-codes.js.map +1 -1
- package/dist/cli/help/content.d.ts.map +1 -1
- package/dist/cli/help/content.js +86 -71
- package/dist/cli/help/content.js.map +1 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +131 -19
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/introspection.d.ts +6 -2
- package/dist/cli/introspection.d.ts.map +1 -1
- package/dist/cli/introspection.js +11 -8
- package/dist/cli/introspection.js.map +1 -1
- package/dist/cli/output.d.ts +64 -4
- package/dist/cli/output.d.ts.map +1 -1
- package/dist/cli/output.js +237 -85
- package/dist/cli/output.js.map +1 -1
- package/dist/cli/parse-utils.d.ts +21 -0
- package/dist/cli/parse-utils.d.ts.map +1 -0
- package/dist/cli/parse-utils.js +32 -0
- package/dist/cli/parse-utils.js.map +1 -0
- package/dist/cli/pid-utils.d.ts +72 -0
- package/dist/cli/pid-utils.d.ts.map +1 -0
- package/dist/cli/pid-utils.js +174 -0
- package/dist/cli/pid-utils.js.map +1 -0
- package/dist/cli/suggest.d.ts.map +1 -1
- package/dist/cli/suggest.js +1 -2
- package/dist/cli/suggest.js.map +1 -1
- package/dist/cli/validators.d.ts +43 -0
- package/dist/cli/validators.d.ts.map +1 -0
- package/dist/cli/validators.js +84 -0
- package/dist/cli/validators.js.map +1 -0
- package/dist/daemon/index.ts +52 -0
- package/dist/daemon/middleware/project-context.ts +126 -0
- package/dist/daemon/pid.ts +179 -0
- package/dist/daemon/project-context.ts +343 -0
- package/dist/daemon/routes/inbox.ts +164 -0
- package/dist/daemon/routes/items.ts +322 -0
- package/dist/daemon/routes/meta.ts +118 -0
- package/dist/daemon/routes/projects.ts +162 -0
- package/dist/daemon/routes/tasks.ts +327 -0
- package/dist/daemon/routes/triage.ts +402 -0
- package/dist/daemon/routes/validation.ts +248 -0
- package/dist/daemon/server.ts +408 -0
- package/dist/daemon/watcher.ts +195 -0
- package/dist/daemon/websocket/handler.ts +138 -0
- package/dist/daemon/websocket/heartbeat.ts +71 -0
- package/dist/daemon/websocket/pubsub.ts +125 -0
- package/dist/daemon/websocket/types.ts +66 -0
- package/dist/export/html.d.ts +19 -0
- package/dist/export/html.d.ts.map +1 -0
- package/dist/export/html.js +239 -0
- package/dist/export/html.js.map +1 -0
- package/dist/export/index.d.ts +10 -0
- package/dist/export/index.d.ts.map +1 -0
- package/dist/export/index.js +10 -0
- package/dist/export/index.js.map +1 -0
- package/dist/export/json.d.ts +24 -0
- package/dist/export/json.d.ts.map +1 -0
- package/dist/export/json.js +198 -0
- package/dist/export/json.js.map +1 -0
- package/dist/export/triage.d.ts +51 -0
- package/dist/export/triage.d.ts.map +1 -0
- package/dist/export/triage.js +83 -0
- package/dist/export/triage.js.map +1 -0
- package/dist/export/types.d.ts +122 -0
- package/dist/export/types.d.ts.map +1 -0
- package/dist/export/types.js +9 -0
- package/dist/export/types.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/lib/claude-plugin-registry.d.ts +66 -0
- package/dist/lib/claude-plugin-registry.d.ts.map +1 -0
- package/dist/lib/claude-plugin-registry.js +318 -0
- package/dist/lib/claude-plugin-registry.js.map +1 -0
- package/dist/merge/arrays.d.ts +87 -0
- package/dist/merge/arrays.d.ts.map +1 -0
- package/dist/merge/arrays.js +164 -0
- package/dist/merge/arrays.js.map +1 -0
- package/dist/merge/file-type.d.ts +32 -0
- package/dist/merge/file-type.d.ts.map +1 -0
- package/dist/merge/file-type.js +70 -0
- package/dist/merge/file-type.js.map +1 -0
- package/dist/merge/index.d.ts +14 -0
- package/dist/merge/index.d.ts.map +1 -0
- package/dist/merge/index.js +11 -0
- package/dist/merge/index.js.map +1 -0
- package/dist/merge/objects.d.ts +46 -0
- package/dist/merge/objects.d.ts.map +1 -0
- package/dist/merge/objects.js +193 -0
- package/dist/merge/objects.js.map +1 -0
- package/dist/merge/parse.d.ts +23 -0
- package/dist/merge/parse.d.ts.map +1 -0
- package/dist/merge/parse.js +78 -0
- package/dist/merge/parse.js.map +1 -0
- package/dist/merge/resolve.d.ts +66 -0
- package/dist/merge/resolve.d.ts.map +1 -0
- package/dist/merge/resolve.js +189 -0
- package/dist/merge/resolve.js.map +1 -0
- package/dist/merge/types.d.ts +82 -0
- package/dist/merge/types.d.ts.map +1 -0
- package/dist/merge/types.js +8 -0
- package/dist/merge/types.js.map +1 -0
- package/dist/parser/agent-data-sections.d.ts +53 -0
- package/dist/parser/agent-data-sections.d.ts.map +1 -0
- package/dist/parser/agent-data-sections.js +118 -0
- package/dist/parser/agent-data-sections.js.map +1 -0
- package/dist/parser/alignment.d.ts +4 -4
- package/dist/parser/alignment.d.ts.map +1 -1
- package/dist/parser/alignment.js +27 -22
- package/dist/parser/alignment.js.map +1 -1
- package/dist/parser/assess.d.ts +5 -5
- package/dist/parser/assess.d.ts.map +1 -1
- package/dist/parser/assess.js +36 -32
- package/dist/parser/assess.js.map +1 -1
- package/dist/parser/config.d.ts +457 -0
- package/dist/parser/config.d.ts.map +1 -0
- package/dist/parser/config.js +373 -0
- package/dist/parser/config.js.map +1 -0
- package/dist/parser/convention-validation.d.ts +1 -1
- package/dist/parser/convention-validation.d.ts.map +1 -1
- package/dist/parser/convention-validation.js +21 -16
- package/dist/parser/convention-validation.js.map +1 -1
- package/dist/parser/coverage-cache.d.ts +49 -0
- package/dist/parser/coverage-cache.d.ts.map +1 -0
- package/dist/parser/coverage-cache.js +123 -0
- package/dist/parser/coverage-cache.js.map +1 -0
- package/dist/parser/daemon-status.d.ts +37 -0
- package/dist/parser/daemon-status.d.ts.map +1 -0
- package/dist/parser/daemon-status.js +67 -0
- package/dist/parser/daemon-status.js.map +1 -0
- package/dist/parser/doctor.d.ts +107 -0
- package/dist/parser/doctor.d.ts.map +1 -0
- package/dist/parser/doctor.js +366 -0
- package/dist/parser/doctor.js.map +1 -0
- package/dist/parser/fix.d.ts +1 -1
- package/dist/parser/fix.d.ts.map +1 -1
- package/dist/parser/fix.js +31 -27
- package/dist/parser/fix.js.map +1 -1
- package/dist/parser/index.d.ts +16 -11
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +16 -11
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/items.d.ts +8 -2
- package/dist/parser/items.d.ts.map +1 -1
- package/dist/parser/items.js +71 -35
- package/dist/parser/items.js.map +1 -1
- package/dist/parser/meta.d.ts +167 -9
- package/dist/parser/meta.d.ts.map +1 -1
- package/dist/parser/meta.js +379 -46
- package/dist/parser/meta.js.map +1 -1
- package/dist/parser/plan-document.d.ts +197 -0
- package/dist/parser/plan-document.d.ts.map +1 -0
- package/dist/parser/plan-document.js +341 -0
- package/dist/parser/plan-document.js.map +1 -0
- package/dist/parser/plans.d.ts +59 -0
- package/dist/parser/plans.d.ts.map +1 -0
- package/dist/parser/plans.js +239 -0
- package/dist/parser/plans.js.map +1 -0
- package/dist/parser/refs.d.ts +22 -9
- package/dist/parser/refs.d.ts.map +1 -1
- package/dist/parser/refs.js +102 -50
- package/dist/parser/refs.js.map +1 -1
- package/dist/parser/setup-status.d.ts +71 -0
- package/dist/parser/setup-status.d.ts.map +1 -0
- package/dist/parser/setup-status.js +269 -0
- package/dist/parser/setup-status.js.map +1 -0
- package/dist/parser/shadow.d.ts +150 -19
- package/dist/parser/shadow.d.ts.map +1 -1
- package/dist/parser/shadow.js +548 -187
- package/dist/parser/shadow.js.map +1 -1
- package/dist/parser/skill-render.d.ts +317 -0
- package/dist/parser/skill-render.d.ts.map +1 -0
- package/dist/parser/skill-render.js +943 -0
- package/dist/parser/skill-render.js.map +1 -0
- package/dist/parser/traits.d.ts +3 -3
- package/dist/parser/traits.d.ts.map +1 -1
- package/dist/parser/traits.js +2 -2
- package/dist/parser/traits.js.map +1 -1
- package/dist/parser/validate-skills.d.ts +32 -0
- package/dist/parser/validate-skills.d.ts.map +1 -0
- package/dist/parser/validate-skills.js +202 -0
- package/dist/parser/validate-skills.js.map +1 -0
- package/dist/parser/validate.d.ts +45 -3
- package/dist/parser/validate.d.ts.map +1 -1
- package/dist/parser/validate.js +622 -105
- package/dist/parser/validate.js.map +1 -1
- package/dist/parser/yaml.d.ts +83 -19
- package/dist/parser/yaml.d.ts.map +1 -1
- package/dist/parser/yaml.js +478 -173
- package/dist/parser/yaml.js.map +1 -1
- package/dist/ralph/cli-renderer.d.ts +8 -1
- package/dist/ralph/cli-renderer.d.ts.map +1 -1
- package/dist/ralph/cli-renderer.js +105 -34
- package/dist/ralph/cli-renderer.js.map +1 -1
- package/dist/ralph/events.d.ts +10 -10
- package/dist/ralph/events.d.ts.map +1 -1
- package/dist/ralph/events.js +301 -98
- package/dist/ralph/events.js.map +1 -1
- package/dist/ralph/index.d.ts +5 -2
- package/dist/ralph/index.d.ts.map +1 -1
- package/dist/ralph/index.js +9 -3
- package/dist/ralph/index.js.map +1 -1
- package/dist/ralph/loop-errors.d.ts +83 -0
- package/dist/ralph/loop-errors.d.ts.map +1 -0
- package/dist/ralph/loop-errors.js +150 -0
- package/dist/ralph/loop-errors.js.map +1 -0
- package/dist/ralph/subagent.d.ts +94 -0
- package/dist/ralph/subagent.d.ts.map +1 -0
- package/dist/ralph/subagent.js +193 -0
- package/dist/ralph/subagent.js.map +1 -0
- package/dist/ralph/wrap-up.d.ts +125 -0
- package/dist/ralph/wrap-up.d.ts.map +1 -0
- package/dist/ralph/wrap-up.js +270 -0
- package/dist/ralph/wrap-up.js.map +1 -0
- package/dist/schema/batch.d.ts +97 -0
- package/dist/schema/batch.d.ts.map +1 -0
- package/dist/schema/batch.js +24 -0
- package/dist/schema/batch.js.map +1 -0
- package/dist/schema/common.d.ts +8 -2
- package/dist/schema/common.d.ts.map +1 -1
- package/dist/schema/common.js +42 -31
- package/dist/schema/common.js.map +1 -1
- package/dist/schema/inbox.d.ts +12 -12
- package/dist/schema/inbox.js +4 -4
- package/dist/schema/inbox.js.map +1 -1
- package/dist/schema/index.d.ts +8 -5
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +8 -5
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/meta.d.ts +1454 -27
- package/dist/schema/meta.d.ts.map +1 -1
- package/dist/schema/meta.js +198 -21
- package/dist/schema/meta.js.map +1 -1
- package/dist/schema/plan.d.ts +285 -0
- package/dist/schema/plan.d.ts.map +1 -0
- package/dist/schema/plan.js +81 -0
- package/dist/schema/plan.js.map +1 -0
- package/dist/schema/spec.d.ts +72 -33
- package/dist/schema/spec.d.ts.map +1 -1
- package/dist/schema/spec.js +22 -9
- package/dist/schema/spec.js.map +1 -1
- package/dist/schema/task.d.ts +172 -161
- package/dist/schema/task.d.ts.map +1 -1
- package/dist/schema/task.js +21 -12
- package/dist/schema/task.js.map +1 -1
- package/dist/schema/triage.d.ts +266 -0
- package/dist/schema/triage.d.ts.map +1 -0
- package/dist/schema/triage.js +134 -0
- package/dist/schema/triage.js.map +1 -0
- package/dist/sessions/index.d.ts +2 -2
- package/dist/sessions/index.d.ts.map +1 -1
- package/dist/sessions/index.js +3 -3
- package/dist/sessions/index.js.map +1 -1
- package/dist/sessions/store.d.ts +241 -1
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +810 -31
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/types.d.ts +10 -10
- package/dist/sessions/types.d.ts.map +1 -1
- package/dist/sessions/types.js +10 -9
- package/dist/sessions/types.js.map +1 -1
- package/dist/strings/errors.d.ts +55 -0
- package/dist/strings/errors.d.ts.map +1 -1
- package/dist/strings/errors.js +138 -106
- package/dist/strings/errors.js.map +1 -1
- package/dist/strings/guidance.d.ts.map +1 -1
- package/dist/strings/guidance.js +16 -16
- package/dist/strings/guidance.js.map +1 -1
- package/dist/strings/index.d.ts +4 -4
- package/dist/strings/index.d.ts.map +1 -1
- package/dist/strings/index.js +4 -4
- package/dist/strings/index.js.map +1 -1
- package/dist/strings/labels.d.ts +4 -0
- package/dist/strings/labels.d.ts.map +1 -1
- package/dist/strings/labels.js +45 -41
- package/dist/strings/labels.js.map +1 -1
- package/dist/strings/validation.d.ts.map +1 -1
- package/dist/strings/validation.js +71 -71
- package/dist/strings/validation.js.map +1 -1
- package/dist/triage/actions.d.ts +27 -0
- package/dist/triage/actions.d.ts.map +1 -0
- package/dist/triage/actions.js +95 -0
- package/dist/triage/actions.js.map +1 -0
- package/dist/triage/constants.d.ts +6 -0
- package/dist/triage/constants.d.ts.map +1 -0
- package/dist/triage/constants.js +7 -0
- package/dist/triage/constants.js.map +1 -0
- package/dist/triage/index.d.ts +3 -0
- package/dist/triage/index.d.ts.map +1 -0
- package/dist/triage/index.js +3 -0
- package/dist/triage/index.js.map +1 -0
- package/dist/utils/commit.d.ts +1 -1
- package/dist/utils/commit.d.ts.map +1 -1
- package/dist/utils/commit.js +28 -26
- package/dist/utils/commit.js.map +1 -1
- package/dist/utils/git.d.ts +1 -1
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +40 -38
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/grep.js +11 -11
- package/dist/utils/grep.js.map +1 -1
- package/dist/utils/index.d.ts +7 -7
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +4 -4
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/time.d.ts.map +1 -1
- package/dist/utils/time.js +10 -10
- package/dist/utils/time.js.map +1 -1
- package/package.json +28 -5
- package/plugin/.claude-plugin/marketplace.json +17 -0
- package/plugin/.claude-plugin/plugin.json +5 -0
- package/plugin/plugins/kspec/skills/create-workflow/SKILL.md +235 -0
- package/plugin/plugins/kspec/skills/help/SKILL.md +42 -0
- package/plugin/plugins/kspec/skills/observations/SKILL.md +143 -0
- package/plugin/plugins/kspec/skills/plan/SKILL.md +343 -0
- package/plugin/plugins/kspec/skills/reflect/SKILL.md +161 -0
- package/plugin/plugins/kspec/skills/review/SKILL.md +193 -0
- package/plugin/plugins/kspec/skills/task-work/SKILL.md +303 -0
- package/plugin/plugins/kspec/skills/triage/SKILL.md +206 -0
- package/plugin/plugins/kspec/skills/triage/docs/automation.md +120 -0
- package/plugin/plugins/kspec/skills/triage/docs/inbox.md +144 -0
- package/plugin/plugins/kspec/skills/triage/docs/observations.md +85 -0
- package/plugin/plugins/kspec/skills/triage-automation/SKILL.md +140 -0
- package/plugin/plugins/kspec/skills/triage-inbox/SKILL.md +232 -0
- package/plugin/plugins/kspec/skills/writing-specs/SKILL.md +340 -0
- package/templates/agents-sections/01-quick-start.md +22 -0
- package/templates/agents-sections/02-shadow-branch.md +34 -0
- package/templates/agents-sections/03-task-lifecycle.md +48 -0
- package/templates/agents-sections/04-pr-workflow.md +17 -0
- package/templates/agents-sections/05-commit-convention.md +27 -0
- package/templates/agents-sections/06-ralph-loop.md +45 -0
- package/templates/hooks/pre-commit +34 -0
- package/templates/skills/create-workflow/SKILL.md +228 -0
- package/templates/skills/help/SKILL.md +37 -0
- package/templates/skills/manifest.yaml +60 -0
- package/templates/skills/observations/SKILL.md +137 -0
- package/templates/skills/plan/SKILL.md +336 -0
- package/templates/skills/reflect/SKILL.md +155 -0
- package/templates/skills/review/SKILL.md +186 -0
- package/templates/skills/task-work/SKILL.md +296 -0
- package/templates/skills/triage/SKILL.md +199 -0
- package/templates/skills/triage/docs/automation.md +120 -0
- package/templates/skills/triage/docs/inbox.md +144 -0
- package/templates/skills/triage/docs/observations.md +85 -0
- package/templates/skills/triage-automation/SKILL.md +134 -0
- package/templates/skills/triage-inbox/SKILL.md +225 -0
- package/templates/skills/writing-specs/SKILL.md +333 -0
package/dist/parser/yaml.js
CHANGED
|
@@ -1,14 +1,40 @@
|
|
|
1
|
-
import
|
|
2
|
-
import * as
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { ItemIndex } from
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { ulid } from "ulid";
|
|
5
|
+
import * as YAML from "yaml";
|
|
6
|
+
import { InboxFileSchema, InboxItemSchema, ManifestSchema, SpecItemSchema, TaskSchema, TasksFileSchema, TriageFileSchema, TriageRecordSchema, } from "../schema/index.js";
|
|
7
|
+
import { errors } from "../strings/index.js";
|
|
8
|
+
import { ItemIndex } from "./items.js";
|
|
9
|
+
import { ReferenceIndex } from "./refs.js";
|
|
10
|
+
import { detectRunningFromShadowWorktree, detectShadow, ShadowError, } from "./shadow.js";
|
|
11
|
+
import { loadProjectConfig, } from "./config.js";
|
|
12
|
+
import { TraitIndex } from "./traits.js";
|
|
13
|
+
/**
|
|
14
|
+
* Log a debug message (only when KSPEC_DEBUG=1)
|
|
15
|
+
*/
|
|
16
|
+
function debugLog(prefix, message) {
|
|
17
|
+
if (process.env.KSPEC_DEBUG === "1") {
|
|
18
|
+
console.error(`[DEBUG] ${prefix}: ${message}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Parse a manifest and emit deprecation warnings for deprecated fields.
|
|
23
|
+
*
|
|
24
|
+
* AC: @config-manifest-cleanup ac-4 — debug-level deprecation note for daemon block
|
|
25
|
+
*/
|
|
26
|
+
function parseManifestWithWarnings(rawManifest) {
|
|
27
|
+
const manifest = ManifestSchema.parse(rawManifest);
|
|
28
|
+
// AC: @config-manifest-cleanup ac-4 — log debug-level deprecation note for daemon block
|
|
29
|
+
if (manifest.daemon) {
|
|
30
|
+
debugLog("manifest", 'Deprecated "daemon" block found in manifest. Use kspec.config.yaml instead.');
|
|
31
|
+
}
|
|
32
|
+
// Also warn for deprecated config block
|
|
33
|
+
if (manifest.config) {
|
|
34
|
+
debugLog("manifest", 'Deprecated "config" block found in manifest. Use kspec.config.yaml instead.');
|
|
35
|
+
}
|
|
36
|
+
return manifest;
|
|
37
|
+
}
|
|
12
38
|
/**
|
|
13
39
|
* Parse YAML content into an object
|
|
14
40
|
* Uses the modern yaml library which has consistent type handling
|
|
@@ -35,16 +61,16 @@ export function toYaml(obj) {
|
|
|
35
61
|
// Post-process to fix yaml library blank line accumulation bug.
|
|
36
62
|
// Filter out lines that contain only spaces/tabs (not truly empty lines).
|
|
37
63
|
yamlString = yamlString
|
|
38
|
-
.split(
|
|
39
|
-
.filter(line => !/^[ \t]+$/.test(line))
|
|
40
|
-
.join(
|
|
64
|
+
.split("\n")
|
|
65
|
+
.filter((line) => !/^[ \t]+$/.test(line))
|
|
66
|
+
.join("\n");
|
|
41
67
|
return yamlString;
|
|
42
68
|
}
|
|
43
69
|
/**
|
|
44
70
|
* Read and parse a YAML file
|
|
45
71
|
*/
|
|
46
72
|
export async function readYamlFile(filePath) {
|
|
47
|
-
const content = await fs.readFile(filePath,
|
|
73
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
48
74
|
return parseYaml(content);
|
|
49
75
|
}
|
|
50
76
|
/**
|
|
@@ -52,7 +78,7 @@ export async function readYamlFile(filePath) {
|
|
|
52
78
|
*/
|
|
53
79
|
export async function writeYamlFile(filePath, data) {
|
|
54
80
|
const content = toYaml(data);
|
|
55
|
-
await fs.writeFile(filePath, content,
|
|
81
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
56
82
|
}
|
|
57
83
|
/**
|
|
58
84
|
* Write object to YAML file while preserving formatting and comments.
|
|
@@ -63,7 +89,7 @@ export async function writeYamlFile(filePath, data) {
|
|
|
63
89
|
*/
|
|
64
90
|
export async function writeYamlFilePreserveFormat(filePath, data) {
|
|
65
91
|
const content = toYaml(data);
|
|
66
|
-
await fs.writeFile(filePath, content,
|
|
92
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
67
93
|
}
|
|
68
94
|
/**
|
|
69
95
|
* Find task files in a directory
|
|
@@ -79,45 +105,47 @@ export async function findTaskFiles(dir) {
|
|
|
79
105
|
const subFiles = await findTaskFiles(fullPath);
|
|
80
106
|
files.push(...subFiles);
|
|
81
107
|
}
|
|
82
|
-
else if (entry.isFile() && entry.name.endsWith(
|
|
108
|
+
else if (entry.isFile() && entry.name.endsWith(".tasks.yaml")) {
|
|
83
109
|
files.push(fullPath);
|
|
84
110
|
}
|
|
85
111
|
}
|
|
86
112
|
}
|
|
87
|
-
catch (
|
|
113
|
+
catch (_error) {
|
|
88
114
|
// Directory doesn't exist or not readable
|
|
89
115
|
}
|
|
90
116
|
return files;
|
|
91
117
|
}
|
|
92
118
|
/**
|
|
93
|
-
* Find the manifest file
|
|
119
|
+
* Find the manifest file.
|
|
120
|
+
*
|
|
121
|
+
* Discovery algorithm per directory:
|
|
122
|
+
* 1. Check for explicit names: kynetic.yaml, kynetic.spec.yaml (backward compat)
|
|
123
|
+
* 2. If not found, scan for *.yaml files with 'kynetic:' version field
|
|
124
|
+
*
|
|
125
|
+
* Searches current dir, then spec/ subdir, then parent directories.
|
|
94
126
|
*/
|
|
95
127
|
export async function findManifest(startDir) {
|
|
96
128
|
let dir = startDir;
|
|
97
129
|
while (true) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
await fs.access(filePath);
|
|
103
|
-
return filePath;
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
// File doesn't exist, try next
|
|
107
|
-
}
|
|
130
|
+
// Check current directory
|
|
131
|
+
const manifestInDir = await findManifestInDir(dir);
|
|
132
|
+
if (manifestInDir) {
|
|
133
|
+
return manifestInDir;
|
|
108
134
|
}
|
|
109
135
|
// Also check in spec/ subdirectory
|
|
110
|
-
const specDir = path.join(dir,
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
await
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
// File doesn't exist, try next
|
|
136
|
+
const specDir = path.join(dir, "spec");
|
|
137
|
+
try {
|
|
138
|
+
const stat = await fs.stat(specDir);
|
|
139
|
+
if (stat.isDirectory()) {
|
|
140
|
+
const manifestInSpec = await findManifestInDir(specDir);
|
|
141
|
+
if (manifestInSpec) {
|
|
142
|
+
return manifestInSpec;
|
|
143
|
+
}
|
|
119
144
|
}
|
|
120
145
|
}
|
|
146
|
+
catch {
|
|
147
|
+
// spec/ doesn't exist
|
|
148
|
+
}
|
|
121
149
|
const parentDir = path.dirname(dir);
|
|
122
150
|
if (parentDir === dir) {
|
|
123
151
|
// Reached root
|
|
@@ -130,20 +158,60 @@ export async function findManifest(startDir) {
|
|
|
130
158
|
* Initialize context by finding manifest.
|
|
131
159
|
*
|
|
132
160
|
* Detection order:
|
|
133
|
-
* 1.
|
|
134
|
-
* 2.
|
|
161
|
+
* 1. Load project config from git root (before shadow detection)
|
|
162
|
+
* 2. Check for shadow branch (.kspec/ directory)
|
|
163
|
+
* 3. Fall back to traditional spec/ directory
|
|
135
164
|
*
|
|
136
165
|
* When shadow is detected, all operations use .kspec/ as specDir.
|
|
166
|
+
*
|
|
167
|
+
* AC: @project-config ac-2 — config loaded before shadow detection
|
|
137
168
|
*/
|
|
138
169
|
export async function initContext(startDir) {
|
|
139
170
|
const cwd = startDir || process.cwd();
|
|
171
|
+
// AC: @project-config ac-2, ac-6, ac-7 — load config before shadow detection
|
|
172
|
+
// Config is loaded from git root, not cwd or KSPEC_SPEC_DIR temp dir
|
|
173
|
+
const configResult = await loadProjectConfig(cwd);
|
|
174
|
+
// AC: @project-config ac-3 — emit warning to stderr if config had issues
|
|
175
|
+
if (configResult.warning) {
|
|
176
|
+
console.error(`Warning: ${configResult.warning}`);
|
|
177
|
+
}
|
|
178
|
+
const { config } = configResult;
|
|
179
|
+
// KSPEC_SPEC_DIR override: used by batch atomic mode to redirect to temp copy
|
|
180
|
+
const specDirOverride = process.env.KSPEC_SPEC_DIR;
|
|
181
|
+
if (specDirOverride) {
|
|
182
|
+
const specDir = path.resolve(specDirOverride);
|
|
183
|
+
const manifestPath = await findManifestInDir(specDir);
|
|
184
|
+
let manifest = null;
|
|
185
|
+
if (manifestPath) {
|
|
186
|
+
try {
|
|
187
|
+
const rawManifest = await readYamlFile(manifestPath);
|
|
188
|
+
manifest = parseManifestWithWarnings(rawManifest);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// Manifest exists but may be invalid
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
rootDir: path.dirname(specDir),
|
|
196
|
+
specDir,
|
|
197
|
+
manifestPath,
|
|
198
|
+
manifest,
|
|
199
|
+
shadow: null, // No shadow in overridden context
|
|
200
|
+
config,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
140
203
|
// Check if running from inside the shadow worktree
|
|
141
|
-
|
|
204
|
+
// AC: @config-shadow ac-8 — pass configured directory for detection
|
|
205
|
+
const mainProjectRoot = await detectRunningFromShadowWorktree(cwd, config.shadow.directory);
|
|
142
206
|
if (mainProjectRoot) {
|
|
143
|
-
throw new ShadowError(errors.project.runningFromShadow,
|
|
207
|
+
throw new ShadowError(errors.project.runningFromShadow, "RUNNING_FROM_SHADOW", `Run from project root: cd ${path.relative(cwd, mainProjectRoot) || mainProjectRoot}`);
|
|
144
208
|
}
|
|
145
209
|
// Try to detect shadow branch first
|
|
146
|
-
|
|
210
|
+
// AC: @config-shadow ac-1 ac-2 — use configured branch/directory names
|
|
211
|
+
const shadow = await detectShadow(cwd, {
|
|
212
|
+
branchName: config.shadow.branch,
|
|
213
|
+
directory: config.shadow.directory,
|
|
214
|
+
});
|
|
147
215
|
if (shadow?.enabled) {
|
|
148
216
|
// Shadow mode: use .kspec/ for everything
|
|
149
217
|
const specDir = shadow.worktreeDir;
|
|
@@ -152,7 +220,7 @@ export async function initContext(startDir) {
|
|
|
152
220
|
if (manifestPath) {
|
|
153
221
|
try {
|
|
154
222
|
const rawManifest = await readYamlFile(manifestPath);
|
|
155
|
-
manifest =
|
|
223
|
+
manifest = parseManifestWithWarnings(rawManifest);
|
|
156
224
|
}
|
|
157
225
|
catch {
|
|
158
226
|
// Manifest exists but may be invalid
|
|
@@ -164,6 +232,7 @@ export async function initContext(startDir) {
|
|
|
164
232
|
manifestPath,
|
|
165
233
|
manifest,
|
|
166
234
|
shadow,
|
|
235
|
+
config,
|
|
167
236
|
};
|
|
168
237
|
}
|
|
169
238
|
// Traditional mode: find manifest in spec/ or current directory
|
|
@@ -174,7 +243,7 @@ export async function initContext(startDir) {
|
|
|
174
243
|
if (manifestPath) {
|
|
175
244
|
const manifestDir = path.dirname(manifestPath);
|
|
176
245
|
// Handle spec/ subdirectory
|
|
177
|
-
if (path.basename(manifestDir) ===
|
|
246
|
+
if (path.basename(manifestDir) === "spec") {
|
|
178
247
|
rootDir = path.dirname(manifestDir);
|
|
179
248
|
specDir = manifestDir;
|
|
180
249
|
}
|
|
@@ -184,21 +253,42 @@ export async function initContext(startDir) {
|
|
|
184
253
|
}
|
|
185
254
|
try {
|
|
186
255
|
const rawManifest = await readYamlFile(manifestPath);
|
|
187
|
-
manifest =
|
|
256
|
+
manifest = parseManifestWithWarnings(rawManifest);
|
|
188
257
|
}
|
|
189
258
|
catch {
|
|
190
259
|
// Manifest exists but may be invalid
|
|
191
260
|
}
|
|
192
261
|
}
|
|
193
|
-
return { rootDir, specDir, manifestPath, manifest, shadow: null };
|
|
262
|
+
return { rootDir, specDir, manifestPath, manifest, shadow: null, config };
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Check if a filename is a potential manifest file.
|
|
266
|
+
* Excludes files with suffixes that indicate other kspec file types.
|
|
267
|
+
*
|
|
268
|
+
* AC: @manifest-discovery ac-5 (excludes task/inbox/meta/runs files)
|
|
269
|
+
*/
|
|
270
|
+
function isManifestCandidate(filename) {
|
|
271
|
+
if (!filename.endsWith(".yaml"))
|
|
272
|
+
return false;
|
|
273
|
+
const exclusions = [".tasks.yaml", ".inbox.yaml", ".meta.yaml", ".runs.yaml"];
|
|
274
|
+
return !exclusions.some((excl) => filename.endsWith(excl));
|
|
194
275
|
}
|
|
195
276
|
/**
|
|
196
277
|
* Find manifest file within a specific directory (no parent traversal).
|
|
197
278
|
* Used for shadow mode where we know exactly where to look.
|
|
279
|
+
*
|
|
280
|
+
* Discovery algorithm:
|
|
281
|
+
* 1. Check for explicit names: kynetic.yaml, kynetic.spec.yaml (backward compat)
|
|
282
|
+
* 2. If not found, scan directory for *.yaml files (excluding other kspec types)
|
|
283
|
+
* 3. For each candidate, validate it contains a 'kynetic:' version field
|
|
284
|
+
* 4. Return first valid match (alphabetically after explicit names)
|
|
285
|
+
*
|
|
286
|
+
* AC: @manifest-discovery ac-1, ac-2, ac-3, ac-4, ac-5
|
|
198
287
|
*/
|
|
199
288
|
async function findManifestInDir(dir) {
|
|
200
|
-
|
|
201
|
-
|
|
289
|
+
// AC: @manifest-discovery ac-1, ac-2 - explicit names have priority
|
|
290
|
+
const priorityCandidates = ["kynetic.yaml", "kynetic.spec.yaml"];
|
|
291
|
+
for (const candidate of priorityCandidates) {
|
|
202
292
|
const filePath = path.join(dir, candidate);
|
|
203
293
|
try {
|
|
204
294
|
await fs.access(filePath);
|
|
@@ -208,6 +298,28 @@ async function findManifestInDir(dir) {
|
|
|
208
298
|
// File doesn't exist, try next
|
|
209
299
|
}
|
|
210
300
|
}
|
|
301
|
+
// AC: @manifest-discovery ac-3, ac-4, ac-5 - glob fallback with validation
|
|
302
|
+
try {
|
|
303
|
+
const entries = await fs.readdir(dir);
|
|
304
|
+
// AC: @manifest-discovery ac-4 - alphabetical order
|
|
305
|
+
const candidates = entries.filter(isManifestCandidate).sort();
|
|
306
|
+
for (const candidate of candidates) {
|
|
307
|
+
const filePath = path.join(dir, candidate);
|
|
308
|
+
try {
|
|
309
|
+
const raw = await readYamlFile(filePath);
|
|
310
|
+
// AC: @manifest-discovery ac-5 - validate kynetic version field
|
|
311
|
+
if (raw && typeof raw === "object" && "kynetic" in raw) {
|
|
312
|
+
return filePath;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
// Skip invalid files
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
// Directory read failed
|
|
322
|
+
}
|
|
211
323
|
return null;
|
|
212
324
|
}
|
|
213
325
|
/**
|
|
@@ -223,7 +335,7 @@ async function loadTasksFromFile(filePath) {
|
|
|
223
335
|
if (Array.isArray(raw)) {
|
|
224
336
|
taskList = raw;
|
|
225
337
|
}
|
|
226
|
-
else if (raw && typeof raw ===
|
|
338
|
+
else if (raw && typeof raw === "object" && "tasks" in raw) {
|
|
227
339
|
const parsed = TasksFileSchema.safeParse(raw);
|
|
228
340
|
if (parsed.success) {
|
|
229
341
|
// Add _sourceFile to each task from this file
|
|
@@ -265,11 +377,11 @@ export async function loadAllTasks(ctx) {
|
|
|
265
377
|
const taskFiles = await findTaskFiles(ctx.specDir);
|
|
266
378
|
// Also check for standalone files in specDir
|
|
267
379
|
const standaloneLocations = [
|
|
268
|
-
path.join(ctx.specDir,
|
|
269
|
-
path.join(ctx.specDir,
|
|
270
|
-
path.join(ctx.specDir,
|
|
271
|
-
path.join(ctx.specDir,
|
|
272
|
-
path.join(ctx.specDir,
|
|
380
|
+
path.join(ctx.specDir, "tasks.yaml"),
|
|
381
|
+
path.join(ctx.specDir, "project.tasks.yaml"),
|
|
382
|
+
path.join(ctx.specDir, "kynetic.tasks.yaml"),
|
|
383
|
+
path.join(ctx.specDir, "backlog.tasks.yaml"),
|
|
384
|
+
path.join(ctx.specDir, "active.tasks.yaml"),
|
|
273
385
|
];
|
|
274
386
|
for (const loc of standaloneLocations) {
|
|
275
387
|
try {
|
|
@@ -294,8 +406,8 @@ export async function loadAllTasks(ctx) {
|
|
|
294
406
|
const taskFiles = await findTaskFiles(ctx.rootDir);
|
|
295
407
|
// Also check common locations
|
|
296
408
|
const additionalPaths = [
|
|
297
|
-
path.join(ctx.rootDir,
|
|
298
|
-
path.join(ctx.rootDir,
|
|
409
|
+
path.join(ctx.rootDir, "tasks"),
|
|
410
|
+
path.join(ctx.rootDir, "spec"),
|
|
299
411
|
];
|
|
300
412
|
for (const additionalPath of additionalPaths) {
|
|
301
413
|
const files = await findTaskFiles(additionalPath);
|
|
@@ -303,11 +415,11 @@ export async function loadAllTasks(ctx) {
|
|
|
303
415
|
}
|
|
304
416
|
// Also look for standalone tasks.yaml and project.tasks.yaml
|
|
305
417
|
const standaloneLocations = [
|
|
306
|
-
path.join(ctx.rootDir,
|
|
307
|
-
path.join(ctx.rootDir,
|
|
308
|
-
path.join(ctx.rootDir,
|
|
309
|
-
path.join(ctx.rootDir,
|
|
310
|
-
path.join(ctx.rootDir,
|
|
418
|
+
path.join(ctx.rootDir, "tasks.yaml"),
|
|
419
|
+
path.join(ctx.rootDir, "project.tasks.yaml"),
|
|
420
|
+
path.join(ctx.rootDir, "spec", "project.tasks.yaml"),
|
|
421
|
+
path.join(ctx.rootDir, "backlog.tasks.yaml"),
|
|
422
|
+
path.join(ctx.rootDir, "active.tasks.yaml"),
|
|
311
423
|
];
|
|
312
424
|
for (const loc of standaloneLocations) {
|
|
313
425
|
try {
|
|
@@ -333,8 +445,8 @@ export async function loadAllTasks(ctx) {
|
|
|
333
445
|
*/
|
|
334
446
|
export function findTaskByRef(tasks, ref) {
|
|
335
447
|
// Remove @ prefix if present
|
|
336
|
-
const cleanRef = ref.startsWith(
|
|
337
|
-
return tasks.find(task => {
|
|
448
|
+
const cleanRef = ref.startsWith("@") ? ref.slice(1) : ref;
|
|
449
|
+
return tasks.find((task) => {
|
|
338
450
|
// Match full ULID
|
|
339
451
|
if (task._ulid === cleanRef)
|
|
340
452
|
return true;
|
|
@@ -354,7 +466,7 @@ export function findTaskByRef(tasks, ref) {
|
|
|
354
466
|
* Otherwise: spec/project.tasks.yaml
|
|
355
467
|
*/
|
|
356
468
|
export function getDefaultTaskFilePath(ctx) {
|
|
357
|
-
return path.join(ctx.specDir,
|
|
469
|
+
return path.join(ctx.specDir, "project.tasks.yaml");
|
|
358
470
|
}
|
|
359
471
|
/**
|
|
360
472
|
* Strip runtime metadata before serialization
|
|
@@ -379,7 +491,9 @@ export async function saveTask(ctx, task) {
|
|
|
379
491
|
try {
|
|
380
492
|
existingRaw = await readYamlFile(taskFilePath);
|
|
381
493
|
// Detect if file uses { tasks: [...] } format
|
|
382
|
-
if (existingRaw &&
|
|
494
|
+
if (existingRaw &&
|
|
495
|
+
typeof existingRaw === "object" &&
|
|
496
|
+
"tasks" in existingRaw) {
|
|
383
497
|
useTasksWrapper = true;
|
|
384
498
|
}
|
|
385
499
|
}
|
|
@@ -420,7 +534,7 @@ export async function saveTask(ctx, task) {
|
|
|
420
534
|
// Strip runtime metadata before saving
|
|
421
535
|
const cleanTask = stripRuntimeMetadata(task);
|
|
422
536
|
// Update existing or add new
|
|
423
|
-
const existingIndex = fileTasks.findIndex(t => t._ulid === task._ulid);
|
|
537
|
+
const existingIndex = fileTasks.findIndex((t) => t._ulid === task._ulid);
|
|
424
538
|
if (existingIndex >= 0) {
|
|
425
539
|
fileTasks[existingIndex] = cleanTask;
|
|
426
540
|
}
|
|
@@ -440,9 +554,9 @@ export async function saveTask(ctx, task) {
|
|
|
440
554
|
* Delete a task from its source file.
|
|
441
555
|
* Requires _sourceFile to know which file to modify.
|
|
442
556
|
*/
|
|
443
|
-
export async function deleteTask(
|
|
557
|
+
export async function deleteTask(_ctx, task) {
|
|
444
558
|
if (!task._sourceFile) {
|
|
445
|
-
throw new Error(
|
|
559
|
+
throw new Error("Cannot delete task without _sourceFile metadata");
|
|
446
560
|
}
|
|
447
561
|
const taskFilePath = task._sourceFile;
|
|
448
562
|
// Load existing file
|
|
@@ -450,7 +564,9 @@ export async function deleteTask(ctx, task) {
|
|
|
450
564
|
let useTasksWrapper = false;
|
|
451
565
|
try {
|
|
452
566
|
existingRaw = await readYamlFile(taskFilePath);
|
|
453
|
-
if (existingRaw &&
|
|
567
|
+
if (existingRaw &&
|
|
568
|
+
typeof existingRaw === "object" &&
|
|
569
|
+
"tasks" in existingRaw) {
|
|
454
570
|
useTasksWrapper = true;
|
|
455
571
|
}
|
|
456
572
|
}
|
|
@@ -488,7 +604,7 @@ export async function deleteTask(ctx, task) {
|
|
|
488
604
|
}
|
|
489
605
|
// Remove the task
|
|
490
606
|
const originalCount = fileTasks.length;
|
|
491
|
-
fileTasks = fileTasks.filter(t => t._ulid !== task._ulid);
|
|
607
|
+
fileTasks = fileTasks.filter((t) => t._ulid !== task._ulid);
|
|
492
608
|
if (fileTasks.length === originalCount) {
|
|
493
609
|
throw new Error(`Task not found in file: ${task._ulid}`);
|
|
494
610
|
}
|
|
@@ -509,8 +625,8 @@ export function createTask(input) {
|
|
|
509
625
|
...input,
|
|
510
626
|
_ulid: input._ulid || ulid(),
|
|
511
627
|
slugs: input.slugs || [],
|
|
512
|
-
type: input.type ||
|
|
513
|
-
status: input.status ||
|
|
628
|
+
type: input.type || "task",
|
|
629
|
+
status: input.status || "pending",
|
|
514
630
|
blocked_by: input.blocked_by || [],
|
|
515
631
|
depends_on: input.depends_on || [],
|
|
516
632
|
context: input.context || [],
|
|
@@ -526,23 +642,32 @@ export function createTask(input) {
|
|
|
526
642
|
* Get author from environment with fallback chain.
|
|
527
643
|
* Priority:
|
|
528
644
|
* 1. KSPEC_AUTHOR env var (explicit config, agent-agnostic)
|
|
529
|
-
* 2.
|
|
530
|
-
* 3.
|
|
531
|
-
* 4.
|
|
645
|
+
* 2. kspec.config.yaml identity.author (project-level default)
|
|
646
|
+
* 3. git user.name (developer identity)
|
|
647
|
+
* 4. USER/USERNAME env var (system user)
|
|
648
|
+
* 5. undefined (will show as 'unknown' in output)
|
|
532
649
|
*
|
|
533
650
|
* For Claude Code integration, add to ~/.claude/settings.json:
|
|
534
651
|
* { "env": { "KSPEC_AUTHOR": "@claude" } }
|
|
652
|
+
*
|
|
653
|
+
* AC: @config-author ac-1 ac-2 ac-3 — author priority chain
|
|
654
|
+
*
|
|
655
|
+
* @param configAuthor Optional author from kspec.config.yaml identity.author
|
|
535
656
|
*/
|
|
536
|
-
export function getAuthor() {
|
|
537
|
-
// 1. Explicit
|
|
657
|
+
export function getAuthor(configAuthor) {
|
|
658
|
+
// 1. Explicit env var (works for any agent) — AC: ac-2
|
|
538
659
|
if (process.env.KSPEC_AUTHOR) {
|
|
539
660
|
return process.env.KSPEC_AUTHOR;
|
|
540
661
|
}
|
|
541
|
-
// 2.
|
|
662
|
+
// 2. Project config author — AC: ac-1
|
|
663
|
+
if (configAuthor) {
|
|
664
|
+
return configAuthor;
|
|
665
|
+
}
|
|
666
|
+
// 3. Git user.name — AC: ac-3
|
|
542
667
|
try {
|
|
543
|
-
const gitUser = execSync(
|
|
544
|
-
encoding:
|
|
545
|
-
stdio: [
|
|
668
|
+
const gitUser = execSync("git config user.name", {
|
|
669
|
+
encoding: "utf-8",
|
|
670
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
546
671
|
}).trim();
|
|
547
672
|
if (gitUser) {
|
|
548
673
|
return gitUser;
|
|
@@ -551,12 +676,12 @@ export function getAuthor() {
|
|
|
551
676
|
catch {
|
|
552
677
|
// git not available or not in a repo
|
|
553
678
|
}
|
|
554
|
-
//
|
|
679
|
+
// 4. System user — AC: ac-3
|
|
555
680
|
const systemUser = process.env.USER || process.env.USERNAME;
|
|
556
681
|
if (systemUser) {
|
|
557
682
|
return systemUser;
|
|
558
683
|
}
|
|
559
|
-
//
|
|
684
|
+
// 5. No author available
|
|
560
685
|
return undefined;
|
|
561
686
|
}
|
|
562
687
|
/**
|
|
@@ -597,7 +722,7 @@ export function areDependenciesMet(task, allTasks) {
|
|
|
597
722
|
return true;
|
|
598
723
|
for (const depRef of task.depends_on) {
|
|
599
724
|
const depTask = findTaskByRef(allTasks, depRef);
|
|
600
|
-
if (!depTask || depTask.status !==
|
|
725
|
+
if (!depTask || depTask.status !== "completed") {
|
|
601
726
|
return false;
|
|
602
727
|
}
|
|
603
728
|
}
|
|
@@ -607,7 +732,7 @@ export function areDependenciesMet(task, allTasks) {
|
|
|
607
732
|
* Check if task is ready (pending + deps met + not blocked)
|
|
608
733
|
*/
|
|
609
734
|
export function isTaskReady(task, allTasks) {
|
|
610
|
-
if (task.status !==
|
|
735
|
+
if (task.status !== "pending")
|
|
611
736
|
return false;
|
|
612
737
|
if (task.blocked_by.length > 0)
|
|
613
738
|
return false;
|
|
@@ -619,14 +744,14 @@ export function isTaskReady(task, allTasks) {
|
|
|
619
744
|
*/
|
|
620
745
|
export function getReadyTasks(tasks) {
|
|
621
746
|
return tasks
|
|
622
|
-
.filter(task => isTaskReady(task, tasks))
|
|
747
|
+
.filter((task) => isTaskReady(task, tasks))
|
|
623
748
|
.sort((a, b) => {
|
|
624
749
|
// Primary: priority (lower number = higher priority)
|
|
625
750
|
if (a.priority !== b.priority) {
|
|
626
751
|
return a.priority - b.priority;
|
|
627
752
|
}
|
|
628
753
|
// Secondary: creation time (older first - FIFO within priority)
|
|
629
|
-
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
|
|
754
|
+
return (new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
|
630
755
|
});
|
|
631
756
|
}
|
|
632
757
|
// ============================================================
|
|
@@ -637,9 +762,11 @@ export function getReadyTasks(tasks) {
|
|
|
637
762
|
* Supports simple patterns like "modules/*.yaml" or "**\/*.yaml"
|
|
638
763
|
*/
|
|
639
764
|
export async function expandIncludePattern(pattern, baseDir) {
|
|
640
|
-
const fullPattern = path.isAbsolute(pattern)
|
|
765
|
+
const fullPattern = path.isAbsolute(pattern)
|
|
766
|
+
? pattern
|
|
767
|
+
: path.join(baseDir, pattern);
|
|
641
768
|
// If no glob characters, just return the path if it exists
|
|
642
|
-
if (!pattern.includes(
|
|
769
|
+
if (!pattern.includes("*")) {
|
|
643
770
|
try {
|
|
644
771
|
await fs.access(fullPattern);
|
|
645
772
|
return [fullPattern];
|
|
@@ -649,17 +776,17 @@ export async function expandIncludePattern(pattern, baseDir) {
|
|
|
649
776
|
}
|
|
650
777
|
}
|
|
651
778
|
// Split pattern into directory part and file pattern
|
|
652
|
-
const parts = pattern.split(
|
|
779
|
+
const parts = pattern.split("/");
|
|
653
780
|
let currentDir = baseDir;
|
|
654
781
|
const result = [];
|
|
655
782
|
// Find the first part with a glob
|
|
656
|
-
|
|
783
|
+
const globIndex = parts.findIndex((p) => p.includes("*"));
|
|
657
784
|
// Navigate to the directory before the glob
|
|
658
785
|
if (globIndex > 0) {
|
|
659
786
|
currentDir = path.join(baseDir, ...parts.slice(0, globIndex));
|
|
660
787
|
}
|
|
661
788
|
// Get the remaining pattern
|
|
662
|
-
const remainingPattern = parts.slice(globIndex).join(
|
|
789
|
+
const remainingPattern = parts.slice(globIndex).join("/");
|
|
663
790
|
await expandGlobRecursive(currentDir, remainingPattern, result);
|
|
664
791
|
return result;
|
|
665
792
|
}
|
|
@@ -667,9 +794,9 @@ export async function expandIncludePattern(pattern, baseDir) {
|
|
|
667
794
|
* Recursively expand glob patterns
|
|
668
795
|
*/
|
|
669
796
|
async function expandGlobRecursive(dir, pattern, result) {
|
|
670
|
-
const parts = pattern.split(
|
|
797
|
+
const parts = pattern.split("/");
|
|
671
798
|
const currentPattern = parts[0];
|
|
672
|
-
const remainingPattern = parts.slice(1).join(
|
|
799
|
+
const remainingPattern = parts.slice(1).join("/");
|
|
673
800
|
try {
|
|
674
801
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
675
802
|
for (const entry of entries) {
|
|
@@ -684,10 +811,10 @@ async function expandGlobRecursive(dir, pattern, result) {
|
|
|
684
811
|
}
|
|
685
812
|
else {
|
|
686
813
|
// This is the final pattern part
|
|
687
|
-
if (currentPattern ===
|
|
814
|
+
if (currentPattern === "**") {
|
|
688
815
|
// ** matches any depth - need special handling
|
|
689
816
|
if (entry.isDirectory()) {
|
|
690
|
-
await expandGlobRecursive(fullPath,
|
|
817
|
+
await expandGlobRecursive(fullPath, "**", result);
|
|
691
818
|
}
|
|
692
819
|
// Also match files at this level
|
|
693
820
|
result.push(fullPath);
|
|
@@ -698,7 +825,7 @@ async function expandGlobRecursive(dir, pattern, result) {
|
|
|
698
825
|
}
|
|
699
826
|
}
|
|
700
827
|
// Handle ** - also recurse into directories without consuming the pattern
|
|
701
|
-
if (currentPattern ===
|
|
828
|
+
if (currentPattern === "**" && entry.isDirectory()) {
|
|
702
829
|
const fullPath = path.join(dir, entry.name);
|
|
703
830
|
await expandGlobRecursive(fullPath, pattern, result);
|
|
704
831
|
}
|
|
@@ -712,15 +839,15 @@ async function expandGlobRecursive(dir, pattern, result) {
|
|
|
712
839
|
* Match a single path component against a glob pattern part
|
|
713
840
|
*/
|
|
714
841
|
function matchGlobPart(name, pattern) {
|
|
715
|
-
if (pattern ===
|
|
842
|
+
if (pattern === "*")
|
|
716
843
|
return true;
|
|
717
|
-
if (pattern ===
|
|
844
|
+
if (pattern === "**")
|
|
718
845
|
return true;
|
|
719
846
|
// Convert glob pattern to regex
|
|
720
847
|
const regexPattern = pattern
|
|
721
|
-
.replace(/[.+^${}()|[\]\\]/g,
|
|
722
|
-
.replace(/\*/g,
|
|
723
|
-
.replace(/\?/g,
|
|
848
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&") // Escape special regex chars
|
|
849
|
+
.replace(/\*/g, ".*") // * matches anything
|
|
850
|
+
.replace(/\?/g, "."); // ? matches single char
|
|
724
851
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
725
852
|
return regex.test(name);
|
|
726
853
|
}
|
|
@@ -728,26 +855,42 @@ function matchGlobPart(name, pattern) {
|
|
|
728
855
|
* Fields that may contain nested spec items
|
|
729
856
|
*/
|
|
730
857
|
const NESTED_ITEM_FIELDS = [
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
858
|
+
"modules",
|
|
859
|
+
"features",
|
|
860
|
+
"requirements",
|
|
861
|
+
"constraints",
|
|
862
|
+
"decisions",
|
|
863
|
+
"traits",
|
|
864
|
+
"acceptance_criteria",
|
|
738
865
|
];
|
|
739
866
|
/**
|
|
740
867
|
* Recursively extract all spec items from a raw YAML structure.
|
|
741
868
|
* Items can be nested under modules/features/requirements/etc.
|
|
742
869
|
* Tracks the path within the file for each item.
|
|
743
870
|
*/
|
|
744
|
-
export function extractItemsFromRaw(raw, sourceFile, items = [], currentPath =
|
|
745
|
-
if (!raw || typeof raw !==
|
|
871
|
+
export function extractItemsFromRaw(raw, sourceFile, items = [], currentPath = "") {
|
|
872
|
+
if (!raw || typeof raw !== "object") {
|
|
746
873
|
return items;
|
|
747
874
|
}
|
|
748
875
|
// Check if this object is itself a spec item (has _ulid)
|
|
749
|
-
if (
|
|
750
|
-
|
|
876
|
+
if ("_ulid" in raw &&
|
|
877
|
+
typeof raw._ulid === "string") {
|
|
878
|
+
// Strip nested item arrays before validation since they're processed separately
|
|
879
|
+
// and the SpecItemSchema expects refs (strings), not nested objects
|
|
880
|
+
const rawObj = raw;
|
|
881
|
+
const cleanedForValidation = { ...rawObj };
|
|
882
|
+
for (const field of NESTED_ITEM_FIELDS) {
|
|
883
|
+
if (field in cleanedForValidation && Array.isArray(cleanedForValidation[field])) {
|
|
884
|
+
const arr = cleanedForValidation[field];
|
|
885
|
+
// Check if array contains nested items (objects with _ulid) vs refs (strings)
|
|
886
|
+
const hasNestedItems = arr.some((item) => item && typeof item === "object" && "_ulid" in item);
|
|
887
|
+
if (hasNestedItems) {
|
|
888
|
+
// Strip nested items - they'll be extracted recursively
|
|
889
|
+
delete cleanedForValidation[field];
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
const result = SpecItemSchema.safeParse(cleanedForValidation);
|
|
751
894
|
if (result.success) {
|
|
752
895
|
items.push({
|
|
753
896
|
...result.data,
|
|
@@ -756,12 +899,13 @@ export function extractItemsFromRaw(raw, sourceFile, items = [], currentPath = '
|
|
|
756
899
|
});
|
|
757
900
|
}
|
|
758
901
|
// Even if the item itself was added, also extract nested items
|
|
759
|
-
const rawObj = raw;
|
|
760
902
|
for (const field of NESTED_ITEM_FIELDS) {
|
|
761
903
|
if (field in rawObj && Array.isArray(rawObj[field])) {
|
|
762
904
|
const arr = rawObj[field];
|
|
763
905
|
for (let i = 0; i < arr.length; i++) {
|
|
764
|
-
const nestedPath = currentPath
|
|
906
|
+
const nestedPath = currentPath
|
|
907
|
+
? `${currentPath}.${field}[${i}]`
|
|
908
|
+
: `${field}[${i}]`;
|
|
765
909
|
extractItemsFromRaw(arr[i], sourceFile, items, nestedPath);
|
|
766
910
|
}
|
|
767
911
|
}
|
|
@@ -781,7 +925,9 @@ export function extractItemsFromRaw(raw, sourceFile, items = [], currentPath = '
|
|
|
781
925
|
if (field in rawObj && Array.isArray(rawObj[field])) {
|
|
782
926
|
const arr = rawObj[field];
|
|
783
927
|
for (let i = 0; i < arr.length; i++) {
|
|
784
|
-
const nestedPath = currentPath
|
|
928
|
+
const nestedPath = currentPath
|
|
929
|
+
? `${currentPath}.${field}[${i}]`
|
|
930
|
+
: `${field}[${i}]`;
|
|
785
931
|
extractItemsFromRaw(arr[i], sourceFile, items, nestedPath);
|
|
786
932
|
}
|
|
787
933
|
}
|
|
@@ -795,7 +941,7 @@ export function extractItemsFromRaw(raw, sourceFile, items = [], currentPath = '
|
|
|
795
941
|
*/
|
|
796
942
|
export async function loadSpecFile(filePath) {
|
|
797
943
|
try {
|
|
798
|
-
const content = await fs.readFile(filePath,
|
|
944
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
799
945
|
const items = [];
|
|
800
946
|
// Parse all YAML documents in the file (handles files with ---)
|
|
801
947
|
const documents = YAML.parseAllDocuments(content);
|
|
@@ -812,7 +958,7 @@ export async function loadSpecFile(filePath) {
|
|
|
812
958
|
}
|
|
813
959
|
return items;
|
|
814
960
|
}
|
|
815
|
-
catch (
|
|
961
|
+
catch (_error) {
|
|
816
962
|
// File doesn't exist or parse error
|
|
817
963
|
return [];
|
|
818
964
|
}
|
|
@@ -846,8 +992,8 @@ export async function loadAllItems(ctx) {
|
|
|
846
992
|
*/
|
|
847
993
|
export function findItemByRef(items, ref) {
|
|
848
994
|
// Remove @ prefix if present
|
|
849
|
-
const cleanRef = ref.startsWith(
|
|
850
|
-
return items.find(item => {
|
|
995
|
+
const cleanRef = ref.startsWith("@") ? ref.slice(1) : ref;
|
|
996
|
+
return items.find((item) => {
|
|
851
997
|
// Match full ULID
|
|
852
998
|
if (item._ulid === cleanRef)
|
|
853
999
|
return true;
|
|
@@ -884,11 +1030,13 @@ export async function buildReferenceIndex(ctx) {
|
|
|
884
1030
|
/**
|
|
885
1031
|
* Build both ReferenceIndex and ItemIndex from context.
|
|
886
1032
|
* Use this when you need query capabilities in addition to reference resolution.
|
|
1033
|
+
* Pass plans for cross-namespace slug collision detection (plans aren't loaded by default
|
|
1034
|
+
* to avoid circular dependency with plans.ts).
|
|
887
1035
|
*/
|
|
888
|
-
export async function buildIndexes(ctx) {
|
|
1036
|
+
export async function buildIndexes(ctx, plans = []) {
|
|
889
1037
|
const tasks = await loadAllTasks(ctx);
|
|
890
1038
|
const items = await loadAllItems(ctx);
|
|
891
|
-
const refIndex = new ReferenceIndex(tasks, items);
|
|
1039
|
+
const refIndex = new ReferenceIndex(tasks, items, [], plans);
|
|
892
1040
|
const itemIndex = new ItemIndex(tasks, items);
|
|
893
1041
|
const traitIndex = new TraitIndex(items, refIndex);
|
|
894
1042
|
return { refIndex, itemIndex, traitIndex, tasks, items };
|
|
@@ -930,7 +1078,7 @@ function navigateToPath(root, pathStr) {
|
|
|
930
1078
|
// Navigate to the parent of the last segment
|
|
931
1079
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
932
1080
|
const [field, index] = segments[i];
|
|
933
|
-
if (typeof current !==
|
|
1081
|
+
if (typeof current !== "object" || current === null)
|
|
934
1082
|
return null;
|
|
935
1083
|
const obj = current;
|
|
936
1084
|
if (!Array.isArray(obj[field]))
|
|
@@ -939,7 +1087,7 @@ function navigateToPath(root, pathStr) {
|
|
|
939
1087
|
}
|
|
940
1088
|
// Get the final array and index
|
|
941
1089
|
const [finalField, finalIndex] = segments[segments.length - 1];
|
|
942
|
-
if (typeof current !==
|
|
1090
|
+
if (typeof current !== "object" || current === null)
|
|
943
1091
|
return null;
|
|
944
1092
|
const parent = current;
|
|
945
1093
|
if (!Array.isArray(parent[finalField]))
|
|
@@ -954,8 +1102,8 @@ function navigateToPath(root, pathStr) {
|
|
|
954
1102
|
* Find an item by ULID in a nested YAML structure.
|
|
955
1103
|
* Returns the path segments to reach it.
|
|
956
1104
|
*/
|
|
957
|
-
function findItemInStructure(root, ulid, currentPath =
|
|
958
|
-
if (!root || typeof root !==
|
|
1105
|
+
function findItemInStructure(root, ulid, currentPath = "") {
|
|
1106
|
+
if (!root || typeof root !== "object")
|
|
959
1107
|
return null;
|
|
960
1108
|
const obj = root;
|
|
961
1109
|
// Check if this is the item we're looking for
|
|
@@ -967,7 +1115,9 @@ function findItemInStructure(root, ulid, currentPath = '') {
|
|
|
967
1115
|
if (Array.isArray(obj[field])) {
|
|
968
1116
|
const arr = obj[field];
|
|
969
1117
|
for (let i = 0; i < arr.length; i++) {
|
|
970
|
-
const nestedPath = currentPath
|
|
1118
|
+
const nestedPath = currentPath
|
|
1119
|
+
? `${currentPath}.${field}[${i}]`
|
|
1120
|
+
: `${field}[${i}]`;
|
|
971
1121
|
const result = findItemInStructure(arr[i], ulid, nestedPath);
|
|
972
1122
|
if (result)
|
|
973
1123
|
return result;
|
|
@@ -989,6 +1139,7 @@ export function createSpecItem(input) {
|
|
|
989
1139
|
priority: input.priority,
|
|
990
1140
|
tags: input.tags || [],
|
|
991
1141
|
description: input.description,
|
|
1142
|
+
acceptance_criteria: input.acceptance_criteria,
|
|
992
1143
|
depends_on: input.depends_on || [],
|
|
993
1144
|
implements: input.implements || [],
|
|
994
1145
|
relates_to: input.relates_to || [],
|
|
@@ -1003,12 +1154,12 @@ export function createSpecItem(input) {
|
|
|
1003
1154
|
* Map from item type to the field name used to store children of that type.
|
|
1004
1155
|
*/
|
|
1005
1156
|
const TYPE_TO_CHILD_FIELD = {
|
|
1006
|
-
feature:
|
|
1007
|
-
requirement:
|
|
1008
|
-
constraint:
|
|
1009
|
-
decision:
|
|
1010
|
-
module:
|
|
1011
|
-
trait:
|
|
1157
|
+
feature: "features",
|
|
1158
|
+
requirement: "requirements",
|
|
1159
|
+
constraint: "constraints",
|
|
1160
|
+
decision: "decisions",
|
|
1161
|
+
module: "modules",
|
|
1162
|
+
trait: "traits",
|
|
1012
1163
|
};
|
|
1013
1164
|
/**
|
|
1014
1165
|
* Add a spec item as a child of a parent item.
|
|
@@ -1016,11 +1167,11 @@ const TYPE_TO_CHILD_FIELD = {
|
|
|
1016
1167
|
* @param child The new child item to add
|
|
1017
1168
|
* @param childField Optional field name override (defaults based on child.type)
|
|
1018
1169
|
*/
|
|
1019
|
-
export async function addChildItem(
|
|
1170
|
+
export async function addChildItem(_ctx, parent, child, childField) {
|
|
1020
1171
|
if (!parent._sourceFile) {
|
|
1021
|
-
throw new Error(
|
|
1172
|
+
throw new Error("Parent item has no source file");
|
|
1022
1173
|
}
|
|
1023
|
-
const field = childField || TYPE_TO_CHILD_FIELD[child.type ||
|
|
1174
|
+
const field = childField || TYPE_TO_CHILD_FIELD[child.type || "feature"] || "features";
|
|
1024
1175
|
// Load the raw YAML
|
|
1025
1176
|
const raw = await readYamlFile(parent._sourceFile);
|
|
1026
1177
|
// Find the parent in the structure
|
|
@@ -1037,7 +1188,7 @@ export async function addChildItem(ctx, parent, child, childField) {
|
|
|
1037
1188
|
else {
|
|
1038
1189
|
// Parent is the root item
|
|
1039
1190
|
parentObj = raw;
|
|
1040
|
-
parentPath =
|
|
1191
|
+
parentPath = "";
|
|
1041
1192
|
}
|
|
1042
1193
|
// Ensure the child field array exists
|
|
1043
1194
|
if (!Array.isArray(parentObj[field])) {
|
|
@@ -1049,7 +1200,9 @@ export async function addChildItem(ctx, parent, child, childField) {
|
|
|
1049
1200
|
childArray.push(cleanChild);
|
|
1050
1201
|
// Calculate the new child's path
|
|
1051
1202
|
const childIndex = childArray.length - 1;
|
|
1052
|
-
const childPath = parentPath
|
|
1203
|
+
const childPath = parentPath
|
|
1204
|
+
? `${parentPath}.${field}[${childIndex}]`
|
|
1205
|
+
: `${field}[${childIndex}]`;
|
|
1053
1206
|
// Write back with format preservation
|
|
1054
1207
|
await writeYamlFilePreserveFormat(parent._sourceFile, raw);
|
|
1055
1208
|
return { item: cleanChild, path: childPath };
|
|
@@ -1058,9 +1211,9 @@ export async function addChildItem(ctx, parent, child, childField) {
|
|
|
1058
1211
|
* Update a spec item in place within its source file.
|
|
1059
1212
|
* Works with nested structures using the _path field.
|
|
1060
1213
|
*/
|
|
1061
|
-
export async function updateSpecItem(
|
|
1214
|
+
export async function updateSpecItem(_ctx, item, updates) {
|
|
1062
1215
|
if (!item._sourceFile) {
|
|
1063
|
-
throw new Error(
|
|
1216
|
+
throw new Error("Item has no source file");
|
|
1064
1217
|
}
|
|
1065
1218
|
// Load the raw YAML
|
|
1066
1219
|
const raw = await readYamlFile(item._sourceFile);
|
|
@@ -1088,7 +1241,7 @@ export async function updateSpecItem(ctx, item, updates) {
|
|
|
1088
1241
|
}
|
|
1089
1242
|
// Apply updates (but never change _ulid)
|
|
1090
1243
|
for (const [key, value] of Object.entries(updates)) {
|
|
1091
|
-
if (key !==
|
|
1244
|
+
if (key !== "_ulid" && key !== "_sourceFile" && key !== "_path") {
|
|
1092
1245
|
targetObj[key] = value;
|
|
1093
1246
|
}
|
|
1094
1247
|
}
|
|
@@ -1102,12 +1255,12 @@ export async function updateSpecItem(ctx, item, updates) {
|
|
|
1102
1255
|
*/
|
|
1103
1256
|
export function findTraitImplementors(trait, allItems) {
|
|
1104
1257
|
// Check if the item is actually a trait
|
|
1105
|
-
if (trait.type !==
|
|
1258
|
+
if (trait.type !== "trait") {
|
|
1106
1259
|
return [];
|
|
1107
1260
|
}
|
|
1108
1261
|
// Find all items that reference this trait in their 'traits' array
|
|
1109
|
-
const traitRefs = [
|
|
1110
|
-
return allItems.filter(item => {
|
|
1262
|
+
const traitRefs = [`@${trait._ulid}`, ...trait.slugs.map((s) => `@${s}`)];
|
|
1263
|
+
return allItems.filter((item) => {
|
|
1111
1264
|
if (!item.traits || item.traits.length === 0)
|
|
1112
1265
|
return false;
|
|
1113
1266
|
return item.traits.some((traitRef) => traitRefs.includes(traitRef));
|
|
@@ -1117,7 +1270,7 @@ export function findTraitImplementors(trait, allItems) {
|
|
|
1117
1270
|
* Delete a spec item from its source file.
|
|
1118
1271
|
* Works with nested structures using the _path field.
|
|
1119
1272
|
*/
|
|
1120
|
-
export async function deleteSpecItem(
|
|
1273
|
+
export async function deleteSpecItem(_ctx, item) {
|
|
1121
1274
|
if (!item._sourceFile) {
|
|
1122
1275
|
return false;
|
|
1123
1276
|
}
|
|
@@ -1136,7 +1289,7 @@ export async function deleteSpecItem(ctx, item) {
|
|
|
1136
1289
|
}
|
|
1137
1290
|
// No path - try to find it by ULID
|
|
1138
1291
|
const found = findItemInStructure(raw, item._ulid);
|
|
1139
|
-
if (found
|
|
1292
|
+
if (found?.path) {
|
|
1140
1293
|
const nav = navigateToPath(raw, found.path);
|
|
1141
1294
|
if (nav) {
|
|
1142
1295
|
nav.array.splice(nav.index, 1);
|
|
@@ -1146,7 +1299,9 @@ export async function deleteSpecItem(ctx, item) {
|
|
|
1146
1299
|
}
|
|
1147
1300
|
// Maybe it's a root-level array item
|
|
1148
1301
|
if (Array.isArray(raw)) {
|
|
1149
|
-
const index = raw.findIndex((i) => typeof i ===
|
|
1302
|
+
const index = raw.findIndex((i) => typeof i === "object" &&
|
|
1303
|
+
i !== null &&
|
|
1304
|
+
i._ulid === item._ulid);
|
|
1150
1305
|
if (index >= 0) {
|
|
1151
1306
|
raw.splice(index, 1);
|
|
1152
1307
|
await writeYamlFilePreserveFormat(item._sourceFile, raw);
|
|
@@ -1170,7 +1325,7 @@ export async function saveSpecItem(ctx, item) {
|
|
|
1170
1325
|
return;
|
|
1171
1326
|
}
|
|
1172
1327
|
// Otherwise, this is more complex - would need a parent
|
|
1173
|
-
throw new Error(
|
|
1328
|
+
throw new Error("Cannot save new item without parent. Use addChildItem instead.");
|
|
1174
1329
|
}
|
|
1175
1330
|
/**
|
|
1176
1331
|
* Get the inbox file path.
|
|
@@ -1179,7 +1334,7 @@ export async function saveSpecItem(ctx, item) {
|
|
|
1179
1334
|
* Otherwise: spec/project.inbox.yaml
|
|
1180
1335
|
*/
|
|
1181
1336
|
export function getInboxFilePath(ctx) {
|
|
1182
|
-
return path.join(ctx.specDir,
|
|
1337
|
+
return path.join(ctx.specDir, "project.inbox.yaml");
|
|
1183
1338
|
}
|
|
1184
1339
|
/**
|
|
1185
1340
|
* Load all inbox items from the project.
|
|
@@ -1189,10 +1344,13 @@ export async function loadInboxItems(ctx) {
|
|
|
1189
1344
|
try {
|
|
1190
1345
|
const raw = await readYamlFile(inboxPath);
|
|
1191
1346
|
// Handle { inbox: [...] } format
|
|
1192
|
-
if (raw && typeof raw ===
|
|
1347
|
+
if (raw && typeof raw === "object" && "inbox" in raw) {
|
|
1193
1348
|
const parsed = InboxFileSchema.safeParse(raw);
|
|
1194
1349
|
if (parsed.success) {
|
|
1195
|
-
return parsed.data.inbox.map(item => ({
|
|
1350
|
+
return parsed.data.inbox.map((item) => ({
|
|
1351
|
+
...item,
|
|
1352
|
+
_sourceFile: inboxPath,
|
|
1353
|
+
}));
|
|
1196
1354
|
}
|
|
1197
1355
|
}
|
|
1198
1356
|
// Handle plain array format
|
|
@@ -1215,14 +1373,19 @@ export async function loadInboxItems(ctx) {
|
|
|
1215
1373
|
}
|
|
1216
1374
|
/**
|
|
1217
1375
|
* Create a new inbox item with auto-generated fields.
|
|
1376
|
+
*
|
|
1377
|
+
* AC: @config-author — supports config author in fallback chain
|
|
1378
|
+
*
|
|
1379
|
+
* @param input Inbox item input
|
|
1380
|
+
* @param configAuthor Optional author from kspec.config.yaml identity.author
|
|
1218
1381
|
*/
|
|
1219
|
-
export function createInboxItem(input) {
|
|
1382
|
+
export function createInboxItem(input, configAuthor) {
|
|
1220
1383
|
return {
|
|
1221
1384
|
_ulid: input._ulid || ulid(),
|
|
1222
1385
|
text: input.text,
|
|
1223
1386
|
created_at: input.created_at || new Date().toISOString(),
|
|
1224
1387
|
tags: input.tags || [],
|
|
1225
|
-
added_by: input.added_by ?? getAuthor(),
|
|
1388
|
+
added_by: input.added_by ?? getAuthor(configAuthor),
|
|
1226
1389
|
};
|
|
1227
1390
|
}
|
|
1228
1391
|
/**
|
|
@@ -1244,7 +1407,7 @@ export async function saveInboxItem(ctx, item) {
|
|
|
1244
1407
|
let existingItems = [];
|
|
1245
1408
|
try {
|
|
1246
1409
|
const raw = await readYamlFile(inboxPath);
|
|
1247
|
-
if (raw && typeof raw ===
|
|
1410
|
+
if (raw && typeof raw === "object" && "inbox" in raw) {
|
|
1248
1411
|
const parsed = InboxFileSchema.safeParse(raw);
|
|
1249
1412
|
if (parsed.success) {
|
|
1250
1413
|
existingItems = parsed.data.inbox;
|
|
@@ -1264,7 +1427,7 @@ export async function saveInboxItem(ctx, item) {
|
|
|
1264
1427
|
}
|
|
1265
1428
|
const cleanItem = stripInboxMetadata(item);
|
|
1266
1429
|
// Update existing or add new
|
|
1267
|
-
const existingIndex = existingItems.findIndex(i => i._ulid === item._ulid);
|
|
1430
|
+
const existingIndex = existingItems.findIndex((i) => i._ulid === item._ulid);
|
|
1268
1431
|
if (existingIndex >= 0) {
|
|
1269
1432
|
existingItems[existingIndex] = cleanItem;
|
|
1270
1433
|
}
|
|
@@ -1282,13 +1445,13 @@ export async function deleteInboxItem(ctx, ulid) {
|
|
|
1282
1445
|
try {
|
|
1283
1446
|
const raw = await readYamlFile(inboxPath);
|
|
1284
1447
|
let existingItems = [];
|
|
1285
|
-
if (raw && typeof raw ===
|
|
1448
|
+
if (raw && typeof raw === "object" && "inbox" in raw) {
|
|
1286
1449
|
const parsed = InboxFileSchema.safeParse(raw);
|
|
1287
1450
|
if (parsed.success) {
|
|
1288
1451
|
existingItems = parsed.data.inbox;
|
|
1289
1452
|
}
|
|
1290
1453
|
}
|
|
1291
|
-
const index = existingItems.findIndex(i => i._ulid === ulid);
|
|
1454
|
+
const index = existingItems.findIndex((i) => i._ulid === ulid);
|
|
1292
1455
|
if (index < 0) {
|
|
1293
1456
|
return false;
|
|
1294
1457
|
}
|
|
@@ -1304,8 +1467,8 @@ export async function deleteInboxItem(ctx, ulid) {
|
|
|
1304
1467
|
* Find an inbox item by reference (ULID or short ULID).
|
|
1305
1468
|
*/
|
|
1306
1469
|
export function findInboxItemByRef(items, ref) {
|
|
1307
|
-
const cleanRef = ref.startsWith(
|
|
1308
|
-
return items.find(item => {
|
|
1470
|
+
const cleanRef = ref.startsWith("@") ? ref.slice(1) : ref;
|
|
1471
|
+
return items.find((item) => {
|
|
1309
1472
|
// Match full ULID
|
|
1310
1473
|
if (item._ulid === cleanRef)
|
|
1311
1474
|
return true;
|
|
@@ -1315,6 +1478,144 @@ export function findInboxItemByRef(items, ref) {
|
|
|
1315
1478
|
return false;
|
|
1316
1479
|
});
|
|
1317
1480
|
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Get the triage file path.
|
|
1483
|
+
* AC: @triage-record-schema ac-6
|
|
1484
|
+
*
|
|
1485
|
+
* When shadow enabled: .kspec/project.triage.yaml
|
|
1486
|
+
* Otherwise: spec/project.triage.yaml
|
|
1487
|
+
*/
|
|
1488
|
+
export function getTriageFilePath(ctx) {
|
|
1489
|
+
return path.join(ctx.specDir, "project.triage.yaml");
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Load all triage records from the project.
|
|
1493
|
+
* AC: @triage-record-schema ac-6, ac-7
|
|
1494
|
+
*/
|
|
1495
|
+
export async function loadTriageRecords(ctx) {
|
|
1496
|
+
const triagePath = getTriageFilePath(ctx);
|
|
1497
|
+
try {
|
|
1498
|
+
const raw = await readYamlFile(triagePath);
|
|
1499
|
+
// Handle { kynetic_triage: "1.0", triage: [...] } format
|
|
1500
|
+
if (raw && typeof raw === "object" && "triage" in raw) {
|
|
1501
|
+
const parsed = TriageFileSchema.safeParse(raw);
|
|
1502
|
+
if (parsed.success) {
|
|
1503
|
+
return parsed.data.triage.map((record) => ({
|
|
1504
|
+
...record,
|
|
1505
|
+
_sourceFile: triagePath,
|
|
1506
|
+
}));
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
// Handle plain array format
|
|
1510
|
+
if (Array.isArray(raw)) {
|
|
1511
|
+
const records = [];
|
|
1512
|
+
for (const item of raw) {
|
|
1513
|
+
const result = TriageRecordSchema.safeParse(item);
|
|
1514
|
+
if (result.success) {
|
|
1515
|
+
records.push({ ...result.data, _sourceFile: triagePath });
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
return records;
|
|
1519
|
+
}
|
|
1520
|
+
return [];
|
|
1521
|
+
}
|
|
1522
|
+
catch {
|
|
1523
|
+
// File doesn't exist or parse error
|
|
1524
|
+
return [];
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Strip runtime metadata before serialization.
|
|
1529
|
+
*/
|
|
1530
|
+
function stripTriageMetadata(record) {
|
|
1531
|
+
const { _sourceFile, ...cleanRecord } = record;
|
|
1532
|
+
return cleanRecord;
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Save a triage record (add or update).
|
|
1536
|
+
* AC: @triage-record-schema ac-8 — upsert on inbox_ref (one record per inbox item)
|
|
1537
|
+
* AC: @triage-record-schema ac-9 — sets updated_at on every mutation
|
|
1538
|
+
*/
|
|
1539
|
+
export async function saveTriageRecord(ctx, record) {
|
|
1540
|
+
const triagePath = getTriageFilePath(ctx);
|
|
1541
|
+
// Ensure directory exists
|
|
1542
|
+
const dir = path.dirname(triagePath);
|
|
1543
|
+
await fs.mkdir(dir, { recursive: true });
|
|
1544
|
+
// Load existing records
|
|
1545
|
+
let existingRecords = [];
|
|
1546
|
+
try {
|
|
1547
|
+
const raw = await readYamlFile(triagePath);
|
|
1548
|
+
if (raw && typeof raw === "object" && "triage" in raw) {
|
|
1549
|
+
const parsed = TriageFileSchema.safeParse(raw);
|
|
1550
|
+
if (parsed.success) {
|
|
1551
|
+
existingRecords = parsed.data.triage;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
else if (Array.isArray(raw)) {
|
|
1555
|
+
for (const item of raw) {
|
|
1556
|
+
const result = TriageRecordSchema.safeParse(item);
|
|
1557
|
+
if (result.success) {
|
|
1558
|
+
existingRecords.push(result.data);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
catch {
|
|
1564
|
+
// File doesn't exist, start fresh
|
|
1565
|
+
}
|
|
1566
|
+
const cleanRecord = stripTriageMetadata(record);
|
|
1567
|
+
// AC: ac-9 — set updated_at on every mutation
|
|
1568
|
+
cleanRecord.updated_at = new Date().toISOString();
|
|
1569
|
+
// AC: ac-8 — upsert: check for existing record by ULID first, then by inbox_ref
|
|
1570
|
+
const existingByUlid = existingRecords.findIndex((r) => r._ulid === record._ulid);
|
|
1571
|
+
if (existingByUlid >= 0) {
|
|
1572
|
+
existingRecords[existingByUlid] = cleanRecord;
|
|
1573
|
+
}
|
|
1574
|
+
else {
|
|
1575
|
+
// Check for existing record with same inbox_ref (uniqueness constraint)
|
|
1576
|
+
// Preserve existing identity (_ulid, created_at) when upserting by inbox_ref
|
|
1577
|
+
const existingByInboxRef = existingRecords.findIndex((r) => r.inbox_ref === record.inbox_ref);
|
|
1578
|
+
if (existingByInboxRef >= 0) {
|
|
1579
|
+
const existing = existingRecords[existingByInboxRef];
|
|
1580
|
+
existingRecords[existingByInboxRef] = {
|
|
1581
|
+
...cleanRecord,
|
|
1582
|
+
_ulid: existing._ulid,
|
|
1583
|
+
created_at: existing.created_at,
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
else {
|
|
1587
|
+
existingRecords.push(cleanRecord);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
// Save with { kynetic_triage: "1.0", triage: [...] } format
|
|
1591
|
+
await writeYamlFilePreserveFormat(triagePath, {
|
|
1592
|
+
kynetic_triage: "1.0",
|
|
1593
|
+
triage: existingRecords,
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
/**
|
|
1597
|
+
* Find a triage record by reference (ULID or short ULID).
|
|
1598
|
+
*/
|
|
1599
|
+
export function findTriageRecordByRef(records, ref) {
|
|
1600
|
+
const cleanRef = ref.startsWith("@") ? ref.slice(1) : ref;
|
|
1601
|
+
return records.find((record) => {
|
|
1602
|
+
// Match full ULID
|
|
1603
|
+
if (record._ulid === cleanRef)
|
|
1604
|
+
return true;
|
|
1605
|
+
// Match short ULID (prefix)
|
|
1606
|
+
if (record._ulid.toLowerCase().startsWith(cleanRef.toLowerCase()))
|
|
1607
|
+
return true;
|
|
1608
|
+
return false;
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
/**
|
|
1612
|
+
* Find a triage record by inbox item reference.
|
|
1613
|
+
* AC: @triage-record-schema ac-8 — lookup by inbox_ref for upsert
|
|
1614
|
+
*/
|
|
1615
|
+
export function findTriageRecordByInboxRef(records, inboxRef) {
|
|
1616
|
+
const cleanRef = inboxRef.startsWith("@") ? inboxRef.slice(1) : inboxRef;
|
|
1617
|
+
return records.find((record) => record.inbox_ref === cleanRef);
|
|
1618
|
+
}
|
|
1318
1619
|
/**
|
|
1319
1620
|
* Bulk patch spec items.
|
|
1320
1621
|
* Resolves refs, validates data, applies patches.
|
|
@@ -1325,28 +1626,32 @@ export async function patchSpecItems(ctx, refIndex, items, patches, options = {}
|
|
|
1325
1626
|
let stopProcessing = false;
|
|
1326
1627
|
for (const patch of patches) {
|
|
1327
1628
|
if (stopProcessing) {
|
|
1328
|
-
results.push({ ref: patch.ref, status:
|
|
1629
|
+
results.push({ ref: patch.ref, status: "skipped" });
|
|
1329
1630
|
continue;
|
|
1330
1631
|
}
|
|
1331
1632
|
// Resolve ref
|
|
1332
1633
|
const resolved = refIndex.resolve(patch.ref);
|
|
1333
1634
|
if (!resolved.ok) {
|
|
1334
|
-
const errorMsg = resolved.error ===
|
|
1635
|
+
const errorMsg = resolved.error === "not_found"
|
|
1335
1636
|
? `Item not found: ${patch.ref}`
|
|
1336
|
-
: resolved.error ===
|
|
1637
|
+
: resolved.error === "ambiguous"
|
|
1337
1638
|
? `Ambiguous ref: ${patch.ref}`
|
|
1338
1639
|
: `Duplicate slug: ${patch.ref}`;
|
|
1339
|
-
results.push({ ref: patch.ref, status:
|
|
1640
|
+
results.push({ ref: patch.ref, status: "error", error: errorMsg });
|
|
1340
1641
|
if (options.failFast) {
|
|
1341
1642
|
stopProcessing = true;
|
|
1342
1643
|
}
|
|
1343
1644
|
continue;
|
|
1344
1645
|
}
|
|
1345
1646
|
// Find the item
|
|
1346
|
-
const item = items.find(i => i._ulid === resolved.ulid);
|
|
1647
|
+
const item = items.find((i) => i._ulid === resolved.ulid);
|
|
1347
1648
|
if (!item) {
|
|
1348
1649
|
// Ref resolved but it's not a spec item (might be a task)
|
|
1349
|
-
results.push({
|
|
1650
|
+
results.push({
|
|
1651
|
+
ref: patch.ref,
|
|
1652
|
+
status: "error",
|
|
1653
|
+
error: "Not a spec item",
|
|
1654
|
+
});
|
|
1350
1655
|
if (options.failFast) {
|
|
1351
1656
|
stopProcessing = true;
|
|
1352
1657
|
}
|
|
@@ -1354,17 +1659,17 @@ export async function patchSpecItems(ctx, refIndex, items, patches, options = {}
|
|
|
1354
1659
|
}
|
|
1355
1660
|
// Dry run - just record what would happen
|
|
1356
1661
|
if (options.dryRun) {
|
|
1357
|
-
results.push({ ref: patch.ref, status:
|
|
1662
|
+
results.push({ ref: patch.ref, status: "updated", ulid: item._ulid });
|
|
1358
1663
|
continue;
|
|
1359
1664
|
}
|
|
1360
1665
|
// Apply the patch
|
|
1361
1666
|
try {
|
|
1362
1667
|
await updateSpecItem(ctx, item, patch.data);
|
|
1363
|
-
results.push({ ref: patch.ref, status:
|
|
1668
|
+
results.push({ ref: patch.ref, status: "updated", ulid: item._ulid });
|
|
1364
1669
|
}
|
|
1365
1670
|
catch (err) {
|
|
1366
1671
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1367
|
-
results.push({ ref: patch.ref, status:
|
|
1672
|
+
results.push({ ref: patch.ref, status: "error", error: errorMsg });
|
|
1368
1673
|
if (options.failFast) {
|
|
1369
1674
|
stopProcessing = true;
|
|
1370
1675
|
}
|
|
@@ -1374,9 +1679,9 @@ export async function patchSpecItems(ctx, refIndex, items, patches, options = {}
|
|
|
1374
1679
|
results,
|
|
1375
1680
|
summary: {
|
|
1376
1681
|
total: patches.length,
|
|
1377
|
-
updated: results.filter(r => r.status ===
|
|
1378
|
-
failed: results.filter(r => r.status ===
|
|
1379
|
-
skipped: results.filter(r => r.status ===
|
|
1682
|
+
updated: results.filter((r) => r.status === "updated").length,
|
|
1683
|
+
failed: results.filter((r) => r.status === "error").length,
|
|
1684
|
+
skipped: results.filter((r) => r.status === "skipped").length,
|
|
1380
1685
|
},
|
|
1381
1686
|
};
|
|
1382
1687
|
}
|