@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/shadow.js
CHANGED
|
@@ -8,10 +8,14 @@
|
|
|
8
8
|
* - All kspec read/write operations target .kspec/
|
|
9
9
|
* - Changes auto-commit to shadow branch
|
|
10
10
|
*/
|
|
11
|
-
import
|
|
12
|
-
import * as
|
|
13
|
-
import
|
|
14
|
-
import { promisify } from
|
|
11
|
+
import { exec, execSync } from "node:child_process";
|
|
12
|
+
import * as fs from "node:fs/promises";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { promisify } from "node:util";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { isBatchMode } from "../cli/batch-context.js";
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = path.dirname(__filename);
|
|
15
19
|
const execAsync = promisify(exec);
|
|
16
20
|
// Import getVerboseMode for checking CLI --debug-shadow flag
|
|
17
21
|
// We use a getter function to avoid issues with circular dependencies
|
|
@@ -29,17 +33,31 @@ export class ShadowError extends Error {
|
|
|
29
33
|
super(message);
|
|
30
34
|
this.code = code;
|
|
31
35
|
this.suggestion = suggestion;
|
|
32
|
-
this.name =
|
|
36
|
+
this.name = "ShadowError";
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
39
|
/**
|
|
36
40
|
* Default shadow branch name
|
|
37
41
|
*/
|
|
38
|
-
export const SHADOW_BRANCH_NAME =
|
|
42
|
+
export const SHADOW_BRANCH_NAME = "kspec-meta";
|
|
39
43
|
/**
|
|
40
44
|
* Default shadow worktree directory
|
|
41
45
|
*/
|
|
42
|
-
export const SHADOW_WORKTREE_DIR =
|
|
46
|
+
export const SHADOW_WORKTREE_DIR = ".kspec";
|
|
47
|
+
/**
|
|
48
|
+
* Get effective branch name from options or default.
|
|
49
|
+
* AC: @config-shadow ac-7 — backward compat when called without config
|
|
50
|
+
*/
|
|
51
|
+
function getBranchName(options) {
|
|
52
|
+
return options?.branchName ?? SHADOW_BRANCH_NAME;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get effective directory name from options or default.
|
|
56
|
+
* AC: @config-shadow ac-7 — backward compat when called without config
|
|
57
|
+
*/
|
|
58
|
+
function getDirectoryName(options) {
|
|
59
|
+
return options?.directory ?? SHADOW_WORKTREE_DIR;
|
|
60
|
+
}
|
|
43
61
|
/**
|
|
44
62
|
* Check if debug mode is enabled.
|
|
45
63
|
* Debug mode can be enabled via:
|
|
@@ -49,7 +67,7 @@ export const SHADOW_WORKTREE_DIR = '.kspec';
|
|
|
49
67
|
* When enabled, shadow branch operations output detailed information.
|
|
50
68
|
*/
|
|
51
69
|
export function isDebugMode(verboseFlag) {
|
|
52
|
-
if (process.env.KSPEC_DEBUG ===
|
|
70
|
+
if (process.env.KSPEC_DEBUG === "1") {
|
|
53
71
|
return true;
|
|
54
72
|
}
|
|
55
73
|
if (verboseFlag === true) {
|
|
@@ -66,10 +84,10 @@ export function isDebugMode(verboseFlag) {
|
|
|
66
84
|
*/
|
|
67
85
|
export async function isGitRepo(dir) {
|
|
68
86
|
try {
|
|
69
|
-
execSync(
|
|
87
|
+
execSync("git rev-parse --git-dir", {
|
|
70
88
|
cwd: dir,
|
|
71
|
-
stdio: [
|
|
72
|
-
encoding:
|
|
89
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
90
|
+
encoding: "utf-8",
|
|
73
91
|
});
|
|
74
92
|
return true;
|
|
75
93
|
}
|
|
@@ -82,10 +100,10 @@ export async function isGitRepo(dir) {
|
|
|
82
100
|
*/
|
|
83
101
|
export function getGitRoot(dir) {
|
|
84
102
|
try {
|
|
85
|
-
const result = execSync(
|
|
103
|
+
const result = execSync("git rev-parse --show-toplevel", {
|
|
86
104
|
cwd: dir,
|
|
87
|
-
stdio: [
|
|
88
|
-
encoding:
|
|
105
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
106
|
+
encoding: "utf-8",
|
|
89
107
|
}).trim();
|
|
90
108
|
return result;
|
|
91
109
|
}
|
|
@@ -100,7 +118,7 @@ export async function branchExists(dir, branchName) {
|
|
|
100
118
|
try {
|
|
101
119
|
execSync(`git show-ref --verify --quiet refs/heads/${branchName}`, {
|
|
102
120
|
cwd: dir,
|
|
103
|
-
stdio: [
|
|
121
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
104
122
|
});
|
|
105
123
|
return true;
|
|
106
124
|
}
|
|
@@ -114,12 +132,12 @@ export async function branchExists(dir, branchName) {
|
|
|
114
132
|
export async function isValidWorktree(worktreeDir) {
|
|
115
133
|
try {
|
|
116
134
|
// Check if .git file exists (worktrees have a .git file, not directory)
|
|
117
|
-
const gitPath = path.join(worktreeDir,
|
|
135
|
+
const gitPath = path.join(worktreeDir, ".git");
|
|
118
136
|
const stat = await fs.stat(gitPath);
|
|
119
137
|
if (stat.isFile()) {
|
|
120
138
|
// Read the .git file to verify it points to a worktree
|
|
121
|
-
const content = await fs.readFile(gitPath,
|
|
122
|
-
return content.trim().startsWith(
|
|
139
|
+
const content = await fs.readFile(gitPath, "utf-8");
|
|
140
|
+
return content.trim().startsWith("gitdir:");
|
|
123
141
|
}
|
|
124
142
|
return false;
|
|
125
143
|
}
|
|
@@ -131,35 +149,66 @@ export async function isValidWorktree(worktreeDir) {
|
|
|
131
149
|
* Detect if running from inside the shadow worktree directory.
|
|
132
150
|
* Returns the main project root if detected, null otherwise.
|
|
133
151
|
*
|
|
152
|
+
* AC: @config-shadow ac-8 — detects custom worktree directories using git metadata
|
|
153
|
+
*
|
|
134
154
|
* Detection logic:
|
|
135
155
|
* 1. Check if .git is a file (worktrees have .git files, not directories)
|
|
136
156
|
* 2. Read the gitdir reference from the .git file
|
|
137
|
-
* 3. Check if it points to a worktree for
|
|
157
|
+
* 3. Check if it points to a worktree for kspec (pattern: <project>/.git/worktrees/...)
|
|
158
|
+
*
|
|
159
|
+
* For custom directories, we detect ANY worktree that:
|
|
160
|
+
* - Has a kspec manifest in it (indicating it's a kspec shadow worktree)
|
|
161
|
+
* - Or has a worktree name containing "kspec" or the configured directory name
|
|
162
|
+
*
|
|
163
|
+
* @param cwd Current working directory
|
|
164
|
+
* @param configuredDirectory Optional configured directory name for matching
|
|
138
165
|
*/
|
|
139
|
-
export async function detectRunningFromShadowWorktree(cwd) {
|
|
166
|
+
export async function detectRunningFromShadowWorktree(cwd, configuredDirectory) {
|
|
140
167
|
try {
|
|
141
|
-
const gitPath = path.join(cwd,
|
|
168
|
+
const gitPath = path.join(cwd, ".git");
|
|
142
169
|
const stat = await fs.stat(gitPath);
|
|
143
170
|
// Worktrees have a .git file, not directory
|
|
144
171
|
if (!stat.isFile()) {
|
|
145
172
|
return null;
|
|
146
173
|
}
|
|
147
|
-
const content = await fs.readFile(gitPath,
|
|
174
|
+
const content = await fs.readFile(gitPath, "utf-8");
|
|
148
175
|
const match = content.trim().match(/^gitdir:\s*(.+)$/);
|
|
149
176
|
if (!match) {
|
|
150
177
|
return null;
|
|
151
178
|
}
|
|
152
179
|
const gitdir = match[1];
|
|
153
|
-
// Check if this is a
|
|
154
|
-
if (gitdir.includes(
|
|
180
|
+
// Check if this is a worktree (pattern: <project>/.git/worktrees/<name>)
|
|
181
|
+
if (gitdir.includes(".git/worktrees/")) {
|
|
155
182
|
const worktreesMatch = gitdir.match(/^(.+)\/\.git\/worktrees\//);
|
|
156
183
|
if (worktreesMatch) {
|
|
157
184
|
const mainProjectRoot = worktreesMatch[1];
|
|
158
185
|
const cwdBase = path.basename(cwd);
|
|
159
186
|
const worktreeName = path.basename(gitdir);
|
|
160
|
-
|
|
187
|
+
// AC: ac-8 — check multiple patterns for shadow worktree detection
|
|
188
|
+
const directoryToCheck = configuredDirectory || SHADOW_WORKTREE_DIR;
|
|
189
|
+
// Check if directory name matches default, configured, or worktree contains "kspec"
|
|
190
|
+
if (cwdBase === SHADOW_WORKTREE_DIR ||
|
|
191
|
+
cwdBase === directoryToCheck ||
|
|
192
|
+
worktreeName.includes("kspec")) {
|
|
161
193
|
return mainProjectRoot;
|
|
162
194
|
}
|
|
195
|
+
// Additional check: see if this directory has a kspec manifest
|
|
196
|
+
// This catches custom directories that don't have "kspec" in the name
|
|
197
|
+
try {
|
|
198
|
+
const files = await fs.readdir(cwd);
|
|
199
|
+
const hasKspecManifest = files.some((f) => (f.endsWith(".yaml") || f.endsWith(".yml")) &&
|
|
200
|
+
!f.includes(".tasks.") &&
|
|
201
|
+
!f.includes(".inbox."));
|
|
202
|
+
// Check for modules directory or tasks file as additional signals
|
|
203
|
+
const hasModules = files.includes("modules");
|
|
204
|
+
const hasTasksFile = files.some((f) => f.includes(".tasks."));
|
|
205
|
+
if (hasKspecManifest && (hasModules || hasTasksFile)) {
|
|
206
|
+
return mainProjectRoot;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// Ignore read errors
|
|
211
|
+
}
|
|
163
212
|
}
|
|
164
213
|
}
|
|
165
214
|
return null;
|
|
@@ -170,14 +219,22 @@ export async function detectRunningFromShadowWorktree(cwd) {
|
|
|
170
219
|
}
|
|
171
220
|
/**
|
|
172
221
|
* Detect shadow branch configuration from a directory.
|
|
173
|
-
* Returns shadow config if
|
|
222
|
+
* Returns shadow config if worktree directory exists and is valid.
|
|
223
|
+
*
|
|
224
|
+
* AC: @config-shadow ac-1 ac-2 — uses configured branch/directory names
|
|
225
|
+
* AC: @config-shadow ac-7 — defaults to constants when options not provided
|
|
226
|
+
*
|
|
227
|
+
* @param startDir Directory to start detection from
|
|
228
|
+
* @param options Optional shadow configuration (branch name, directory)
|
|
174
229
|
*/
|
|
175
|
-
export async function detectShadow(startDir) {
|
|
230
|
+
export async function detectShadow(startDir, options) {
|
|
176
231
|
const gitRoot = getGitRoot(startDir);
|
|
177
232
|
if (!gitRoot) {
|
|
178
233
|
return null;
|
|
179
234
|
}
|
|
180
|
-
const
|
|
235
|
+
const directoryName = getDirectoryName(options);
|
|
236
|
+
const branchName = getBranchName(options);
|
|
237
|
+
const worktreeDir = path.join(gitRoot, directoryName);
|
|
181
238
|
try {
|
|
182
239
|
await fs.access(worktreeDir);
|
|
183
240
|
// Verify it's a valid worktree
|
|
@@ -185,7 +242,7 @@ export async function detectShadow(startDir) {
|
|
|
185
242
|
return {
|
|
186
243
|
enabled: true,
|
|
187
244
|
worktreeDir,
|
|
188
|
-
branchName
|
|
245
|
+
branchName,
|
|
189
246
|
projectRoot: gitRoot,
|
|
190
247
|
};
|
|
191
248
|
}
|
|
@@ -193,29 +250,38 @@ export async function detectShadow(startDir) {
|
|
|
193
250
|
return null;
|
|
194
251
|
}
|
|
195
252
|
catch {
|
|
196
|
-
//
|
|
253
|
+
// Worktree directory doesn't exist
|
|
197
254
|
return null;
|
|
198
255
|
}
|
|
199
256
|
}
|
|
200
257
|
/**
|
|
201
|
-
* Get detailed shadow branch status
|
|
258
|
+
* Get detailed shadow branch status.
|
|
259
|
+
*
|
|
260
|
+
* AC: @config-shadow ac-1 ac-2 — uses configured branch/directory names
|
|
261
|
+
* AC: @config-shadow ac-7 — defaults to constants when options not provided
|
|
262
|
+
*
|
|
263
|
+
* @param projectRoot Git repository root
|
|
264
|
+
* @param options Optional shadow configuration
|
|
202
265
|
*/
|
|
203
|
-
export async function getShadowStatus(projectRoot) {
|
|
204
|
-
const
|
|
266
|
+
export async function getShadowStatus(projectRoot, options) {
|
|
267
|
+
const directoryName = getDirectoryName(options);
|
|
268
|
+
const branchName = getBranchName(options);
|
|
269
|
+
const worktreeDir = path.join(projectRoot, directoryName);
|
|
205
270
|
const status = {
|
|
206
271
|
exists: false,
|
|
207
272
|
healthy: false,
|
|
208
273
|
branchExists: false,
|
|
209
274
|
worktreeExists: false,
|
|
210
275
|
worktreeLinked: false,
|
|
276
|
+
artifactsDirExists: false,
|
|
211
277
|
};
|
|
212
278
|
// Check if we're in a git repo
|
|
213
279
|
if (!(await isGitRepo(projectRoot))) {
|
|
214
|
-
status.error =
|
|
280
|
+
status.error = "Not a git repository";
|
|
215
281
|
return status;
|
|
216
282
|
}
|
|
217
283
|
// Check if branch exists
|
|
218
|
-
status.branchExists = await branchExists(projectRoot,
|
|
284
|
+
status.branchExists = await branchExists(projectRoot, branchName);
|
|
219
285
|
// Check if worktree directory exists
|
|
220
286
|
try {
|
|
221
287
|
await fs.access(worktreeDir);
|
|
@@ -227,37 +293,112 @@ export async function getShadowStatus(projectRoot) {
|
|
|
227
293
|
// Check if worktree is properly linked
|
|
228
294
|
if (status.worktreeExists) {
|
|
229
295
|
status.worktreeLinked = await isValidWorktree(worktreeDir);
|
|
296
|
+
// AC: @artifacts-directory ac-doctor-checks
|
|
297
|
+
try {
|
|
298
|
+
await fs.access(path.join(worktreeDir, "artifacts"));
|
|
299
|
+
status.artifactsDirExists = true;
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
status.artifactsDirExists = false;
|
|
303
|
+
}
|
|
230
304
|
}
|
|
231
305
|
// Determine overall status
|
|
232
306
|
status.exists = status.branchExists || status.worktreeExists;
|
|
233
|
-
status.healthy =
|
|
307
|
+
status.healthy =
|
|
308
|
+
status.branchExists && status.worktreeExists && status.worktreeLinked;
|
|
234
309
|
if (!status.healthy && status.exists) {
|
|
235
310
|
if (!status.branchExists) {
|
|
236
|
-
status.error =
|
|
311
|
+
status.error = "Shadow branch missing but worktree exists";
|
|
237
312
|
}
|
|
238
313
|
else if (!status.worktreeExists) {
|
|
239
|
-
status.error =
|
|
314
|
+
status.error = "Shadow branch exists but worktree missing";
|
|
240
315
|
}
|
|
241
316
|
else if (!status.worktreeLinked) {
|
|
242
|
-
status.error =
|
|
317
|
+
status.error = "Worktree exists but not properly linked";
|
|
243
318
|
}
|
|
244
319
|
}
|
|
245
320
|
return status;
|
|
246
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Check if detected shadow branch settings match the configuration.
|
|
324
|
+
*
|
|
325
|
+
* AC: @config-shadow ac-9 — detect mismatch and guide user to migrate
|
|
326
|
+
*
|
|
327
|
+
* This function detects when:
|
|
328
|
+
* - A shadow branch exists with default settings (kspec-meta, .kspec)
|
|
329
|
+
* - But config specifies different settings
|
|
330
|
+
*
|
|
331
|
+
* @param projectRoot Git repository root
|
|
332
|
+
* @param configuredBranch Configured branch name
|
|
333
|
+
* @param configuredDirectory Configured directory name
|
|
334
|
+
*/
|
|
335
|
+
export async function checkConfigMismatch(projectRoot, configuredBranch, configuredDirectory) {
|
|
336
|
+
const result = { hasMismatch: false };
|
|
337
|
+
// First check if default shadow exists
|
|
338
|
+
const defaultStatus = await getShadowStatus(projectRoot, {
|
|
339
|
+
branchName: SHADOW_BRANCH_NAME,
|
|
340
|
+
directory: SHADOW_WORKTREE_DIR,
|
|
341
|
+
});
|
|
342
|
+
if (!defaultStatus.healthy) {
|
|
343
|
+
// No existing shadow with defaults - no mismatch possible
|
|
344
|
+
return result;
|
|
345
|
+
}
|
|
346
|
+
// Check if configured settings differ from defaults
|
|
347
|
+
const branchDiffers = configuredBranch !== SHADOW_BRANCH_NAME;
|
|
348
|
+
const directoryDiffers = configuredDirectory !== SHADOW_WORKTREE_DIR;
|
|
349
|
+
if (!branchDiffers && !directoryDiffers) {
|
|
350
|
+
// Config matches defaults - no mismatch
|
|
351
|
+
return result;
|
|
352
|
+
}
|
|
353
|
+
// There's a mismatch - existing shadow uses defaults but config specifies different values
|
|
354
|
+
result.hasMismatch = true;
|
|
355
|
+
if (branchDiffers) {
|
|
356
|
+
result.branchMismatch = {
|
|
357
|
+
detected: SHADOW_BRANCH_NAME,
|
|
358
|
+
configured: configuredBranch,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
if (directoryDiffers) {
|
|
362
|
+
result.directoryMismatch = {
|
|
363
|
+
detected: SHADOW_WORKTREE_DIR,
|
|
364
|
+
configured: configuredDirectory,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
// Build guidance message
|
|
368
|
+
const parts = [];
|
|
369
|
+
if (result.branchMismatch) {
|
|
370
|
+
parts.push(`branch "${SHADOW_BRANCH_NAME}" (config wants "${configuredBranch}")`);
|
|
371
|
+
}
|
|
372
|
+
if (result.directoryMismatch) {
|
|
373
|
+
parts.push(`directory "${SHADOW_WORKTREE_DIR}" (config wants "${configuredDirectory}")`);
|
|
374
|
+
}
|
|
375
|
+
result.guidance = [
|
|
376
|
+
`Shadow branch exists with ${parts.join(" and ")}.`,
|
|
377
|
+
"",
|
|
378
|
+
"To migrate to configured settings:",
|
|
379
|
+
" 1. Export your specs: kspec export --all > backup.yaml",
|
|
380
|
+
" 2. Remove existing shadow: rm -rf .kspec && git branch -D kspec-meta",
|
|
381
|
+
" 3. Re-initialize: kspec init",
|
|
382
|
+
" 4. Import specs: kspec import backup.yaml",
|
|
383
|
+
"",
|
|
384
|
+
"Or update kspec.config.yaml to match existing settings.",
|
|
385
|
+
].join("\n");
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
247
388
|
/**
|
|
248
389
|
* Create an appropriate ShadowError based on status
|
|
249
390
|
*/
|
|
250
391
|
export function createShadowError(status) {
|
|
251
392
|
if (!status.branchExists && !status.worktreeExists) {
|
|
252
|
-
return new ShadowError(
|
|
393
|
+
return new ShadowError("Shadow branch not initialized", "NOT_INITIALIZED", "Run `kspec init` to create shadow branch and worktree.");
|
|
253
394
|
}
|
|
254
395
|
if (status.branchExists && !status.worktreeExists) {
|
|
255
|
-
return new ShadowError(
|
|
396
|
+
return new ShadowError(".kspec/ directory missing", "DIRECTORY_MISSING", "Run `kspec shadow repair` to recreate the worktree.");
|
|
256
397
|
}
|
|
257
398
|
if (status.worktreeExists && !status.worktreeLinked) {
|
|
258
|
-
return new ShadowError(
|
|
399
|
+
return new ShadowError("Worktree disconnected from git", "WORKTREE_DISCONNECTED", "Run `kspec shadow repair` to fix the worktree link.");
|
|
259
400
|
}
|
|
260
|
-
return new ShadowError(status.error ||
|
|
401
|
+
return new ShadowError(status.error || "Unknown shadow branch error", "GIT_ERROR", "Check git status and try `kspec shadow repair`.");
|
|
261
402
|
}
|
|
262
403
|
/**
|
|
263
404
|
* Auto-commit changes to shadow branch.
|
|
@@ -275,18 +416,18 @@ export async function shadowAutoCommit(worktreeDir, message, verbose) {
|
|
|
275
416
|
console.error(`[DEBUG] Shadow auto-commit: git add -A (cwd: ${worktreeDir})`);
|
|
276
417
|
}
|
|
277
418
|
// Stage all changes
|
|
278
|
-
execSync(
|
|
419
|
+
execSync("git add -A", {
|
|
279
420
|
cwd: worktreeDir,
|
|
280
|
-
stdio: [
|
|
421
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
281
422
|
});
|
|
282
423
|
// Check if there are staged changes
|
|
283
424
|
try {
|
|
284
425
|
if (debug) {
|
|
285
426
|
console.error(`[DEBUG] Shadow auto-commit: git diff --cached --quiet`);
|
|
286
427
|
}
|
|
287
|
-
execSync(
|
|
428
|
+
execSync("git diff --cached --quiet", {
|
|
288
429
|
cwd: worktreeDir,
|
|
289
|
-
stdio: [
|
|
430
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
290
431
|
});
|
|
291
432
|
// No error = no changes
|
|
292
433
|
if (debug) {
|
|
@@ -304,8 +445,8 @@ export async function shadowAutoCommit(worktreeDir, message, verbose) {
|
|
|
304
445
|
// Set KSPEC_SHADOW_COMMIT=1 to signal authorized commit to git hooks
|
|
305
446
|
execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, {
|
|
306
447
|
cwd: worktreeDir,
|
|
307
|
-
stdio: [
|
|
308
|
-
env: { ...process.env, KSPEC_SHADOW_COMMIT:
|
|
448
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
449
|
+
env: { ...process.env, KSPEC_SHADOW_COMMIT: "1" },
|
|
309
450
|
});
|
|
310
451
|
if (debug) {
|
|
311
452
|
console.error(`[DEBUG] Shadow auto-commit: Success`);
|
|
@@ -315,7 +456,7 @@ export async function shadowAutoCommit(worktreeDir, message, verbose) {
|
|
|
315
456
|
catch (error) {
|
|
316
457
|
// AC: Only log error if debug mode enabled
|
|
317
458
|
if (debug) {
|
|
318
|
-
console.error(
|
|
459
|
+
console.error("Shadow auto-commit failed:", error);
|
|
319
460
|
}
|
|
320
461
|
return false;
|
|
321
462
|
}
|
|
@@ -326,36 +467,36 @@ export async function shadowAutoCommit(worktreeDir, message, verbose) {
|
|
|
326
467
|
export function generateCommitMessage(operation, ref, detail) {
|
|
327
468
|
const parts = [];
|
|
328
469
|
switch (operation) {
|
|
329
|
-
case
|
|
470
|
+
case "task-start":
|
|
330
471
|
parts.push(`Start @${ref}`);
|
|
331
472
|
break;
|
|
332
|
-
case
|
|
473
|
+
case "task-complete":
|
|
333
474
|
parts.push(`Complete @${ref}`);
|
|
334
475
|
if (detail)
|
|
335
476
|
parts.push(`: ${detail}`);
|
|
336
477
|
break;
|
|
337
|
-
case
|
|
478
|
+
case "task-note":
|
|
338
479
|
parts.push(`Note on @${ref}`);
|
|
339
480
|
break;
|
|
340
|
-
case
|
|
481
|
+
case "task-add":
|
|
341
482
|
parts.push(`Add task: ${detail || ref}`);
|
|
342
483
|
break;
|
|
343
|
-
case
|
|
344
|
-
parts.push(`Inbox: ${detail?.slice(0, 50)}${(detail?.length || 0) > 50 ?
|
|
484
|
+
case "inbox-add":
|
|
485
|
+
parts.push(`Inbox: ${detail?.slice(0, 50)}${(detail?.length || 0) > 50 ? "..." : ""}`);
|
|
345
486
|
break;
|
|
346
|
-
case
|
|
487
|
+
case "inbox-promote":
|
|
347
488
|
parts.push(`Promote to @${ref}`);
|
|
348
489
|
break;
|
|
349
|
-
case
|
|
490
|
+
case "item-add":
|
|
350
491
|
parts.push(`Add @${ref}`);
|
|
351
492
|
break;
|
|
352
|
-
case
|
|
493
|
+
case "item-set":
|
|
353
494
|
parts.push(`Update @${ref}`);
|
|
354
495
|
break;
|
|
355
|
-
case
|
|
496
|
+
case "item-delete":
|
|
356
497
|
parts.push(`Delete @${ref}`);
|
|
357
498
|
break;
|
|
358
|
-
case
|
|
499
|
+
case "derive":
|
|
359
500
|
parts.push(`Derive from @${ref}`);
|
|
360
501
|
break;
|
|
361
502
|
default:
|
|
@@ -363,11 +504,13 @@ export function generateCommitMessage(operation, ref, detail) {
|
|
|
363
504
|
if (ref)
|
|
364
505
|
parts.push(` @${ref}`);
|
|
365
506
|
}
|
|
366
|
-
return parts.join(
|
|
507
|
+
return parts.join("");
|
|
367
508
|
}
|
|
368
509
|
/**
|
|
369
510
|
* Resolve a path relative to shadow worktree if enabled.
|
|
370
511
|
* Falls back to original path if shadow is not enabled.
|
|
512
|
+
*
|
|
513
|
+
* Uses the worktreeDir from shadowConfig for custom directory support.
|
|
371
514
|
*/
|
|
372
515
|
export function resolveShadowPath(originalPath, shadowConfig, projectRoot) {
|
|
373
516
|
if (!shadowConfig?.enabled) {
|
|
@@ -375,17 +518,21 @@ export function resolveShadowPath(originalPath, shadowConfig, projectRoot) {
|
|
|
375
518
|
}
|
|
376
519
|
// If the path is within the project root, rewrite to shadow worktree
|
|
377
520
|
const relativePath = path.relative(projectRoot, originalPath);
|
|
378
|
-
//
|
|
379
|
-
|
|
521
|
+
// Get the directory name from the worktree path (supports custom directories)
|
|
522
|
+
const worktreeDirName = path.basename(shadowConfig.worktreeDir);
|
|
523
|
+
// Skip if path is outside project or already in shadow worktree
|
|
524
|
+
if (relativePath.startsWith("..") ||
|
|
525
|
+
relativePath.startsWith(worktreeDirName)) {
|
|
380
526
|
return originalPath;
|
|
381
527
|
}
|
|
382
|
-
// Handle spec/ ->
|
|
383
|
-
if (relativePath.startsWith(
|
|
528
|
+
// Handle spec/ -> shadow worktree mapping
|
|
529
|
+
if (relativePath.startsWith("spec/") || relativePath.startsWith("spec\\")) {
|
|
384
530
|
const specRelative = relativePath.slice(5); // Remove 'spec/'
|
|
385
531
|
return path.join(shadowConfig.worktreeDir, specRelative);
|
|
386
532
|
}
|
|
387
|
-
// For task/inbox files at root, move to
|
|
388
|
-
if (relativePath.endsWith(
|
|
533
|
+
// For task/inbox files at root, move to shadow worktree
|
|
534
|
+
if (relativePath.endsWith(".tasks.yaml") ||
|
|
535
|
+
relativePath.endsWith(".inbox.yaml")) {
|
|
389
536
|
return path.join(shadowConfig.worktreeDir, relativePath);
|
|
390
537
|
}
|
|
391
538
|
return originalPath;
|
|
@@ -402,12 +549,16 @@ export function resolveShadowPath(originalPath, shadowConfig, projectRoot) {
|
|
|
402
549
|
* @returns true if committed, false if shadow not enabled or nothing to commit
|
|
403
550
|
*/
|
|
404
551
|
export async function commitIfShadow(shadowConfig, operation, ref, detail, verbose) {
|
|
552
|
+
// Suppress auto-commits during atomic batch execution
|
|
553
|
+
if (isBatchMode()) {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
405
556
|
if (!shadowConfig?.enabled) {
|
|
406
557
|
return false;
|
|
407
558
|
}
|
|
408
559
|
const message = generateCommitMessage(operation, ref, detail);
|
|
409
560
|
const committed = await shadowAutoCommit(shadowConfig.worktreeDir, message, verbose);
|
|
410
|
-
// AC-1
|
|
561
|
+
// AC: @shadow-sync ac-1 - Fire-and-forget push after each commit
|
|
411
562
|
if (committed) {
|
|
412
563
|
shadowPushAsync(shadowConfig.worktreeDir, verbose);
|
|
413
564
|
}
|
|
@@ -438,7 +589,7 @@ export function formatShadowError(error) {
|
|
|
438
589
|
/**
|
|
439
590
|
* Check if a remote exists (default: origin)
|
|
440
591
|
*/
|
|
441
|
-
export async function hasRemote(projectRoot, remoteName =
|
|
592
|
+
export async function hasRemote(projectRoot, remoteName = "origin") {
|
|
442
593
|
try {
|
|
443
594
|
const { stdout } = await execAsync(`git remote get-url ${remoteName}`, {
|
|
444
595
|
cwd: projectRoot,
|
|
@@ -452,11 +603,11 @@ export async function hasRemote(projectRoot, remoteName = 'origin') {
|
|
|
452
603
|
/**
|
|
453
604
|
* Check if a branch exists on a remote
|
|
454
605
|
*/
|
|
455
|
-
export async function remoteBranchExists(projectRoot, branchName, remoteName =
|
|
606
|
+
export async function remoteBranchExists(projectRoot, branchName, remoteName = "origin") {
|
|
456
607
|
try {
|
|
457
608
|
execSync(`git show-ref --verify --quiet refs/remotes/${remoteName}/${branchName}`, {
|
|
458
609
|
cwd: projectRoot,
|
|
459
|
-
stdio: [
|
|
610
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
460
611
|
});
|
|
461
612
|
return true;
|
|
462
613
|
}
|
|
@@ -468,7 +619,7 @@ export async function remoteBranchExists(projectRoot, branchName, remoteName = '
|
|
|
468
619
|
* Fetch from remote to ensure refs are up to date.
|
|
469
620
|
* Returns true if fetch succeeded, false otherwise.
|
|
470
621
|
*/
|
|
471
|
-
export async function fetchRemote(projectRoot, remoteName =
|
|
622
|
+
export async function fetchRemote(projectRoot, remoteName = "origin") {
|
|
472
623
|
try {
|
|
473
624
|
await execAsync(`git fetch ${remoteName}`, {
|
|
474
625
|
cwd: projectRoot,
|
|
@@ -482,10 +633,18 @@ export async function fetchRemote(projectRoot, remoteName = 'origin') {
|
|
|
482
633
|
/**
|
|
483
634
|
* Push shadow branch to remote with tracking.
|
|
484
635
|
* Returns true if push succeeded, false otherwise.
|
|
636
|
+
*
|
|
637
|
+
* AC: @config-shadow ac-3 — uses configured remote name
|
|
638
|
+
* AC: @config-shadow ac-7 — defaults to origin when not provided
|
|
639
|
+
*
|
|
640
|
+
* @param worktreeDir Path to shadow worktree
|
|
641
|
+
* @param remoteName Remote name (default: origin)
|
|
642
|
+
* @param options Optional shadow configuration
|
|
485
643
|
*/
|
|
486
|
-
export async function pushShadowBranch(worktreeDir, remoteName =
|
|
644
|
+
export async function pushShadowBranch(worktreeDir, remoteName = "origin", options) {
|
|
645
|
+
const branchName = getBranchName(options);
|
|
487
646
|
try {
|
|
488
|
-
await execAsync(`git push -u ${remoteName} ${
|
|
647
|
+
await execAsync(`git push -u ${remoteName} ${branchName}`, {
|
|
489
648
|
cwd: worktreeDir,
|
|
490
649
|
});
|
|
491
650
|
return true;
|
|
@@ -497,10 +656,16 @@ export async function pushShadowBranch(worktreeDir, remoteName = 'origin') {
|
|
|
497
656
|
/**
|
|
498
657
|
* Check if shadow branch has remote tracking configured.
|
|
499
658
|
* AC-4: Used to determine whether sync should be attempted.
|
|
659
|
+
*
|
|
660
|
+
* AC: @config-shadow ac-7 — backward compat when called without config
|
|
661
|
+
*
|
|
662
|
+
* @param worktreeDir Path to shadow worktree
|
|
663
|
+
* @param options Optional shadow configuration
|
|
500
664
|
*/
|
|
501
|
-
export async function hasRemoteTracking(worktreeDir) {
|
|
665
|
+
export async function hasRemoteTracking(worktreeDir, options) {
|
|
666
|
+
const branchName = getBranchName(options);
|
|
502
667
|
try {
|
|
503
|
-
const { stdout } = await execAsync(`git config branch.${
|
|
668
|
+
const { stdout } = await execAsync(`git config branch.${branchName}.remote`, { cwd: worktreeDir });
|
|
504
669
|
return stdout.trim().length > 0;
|
|
505
670
|
}
|
|
506
671
|
catch {
|
|
@@ -512,27 +677,80 @@ export async function hasRemoteTracking(worktreeDir) {
|
|
|
512
677
|
* AC-8: If shadow has no tracking but main branch has origin remote,
|
|
513
678
|
* automatically configure tracking to origin/kspec-meta.
|
|
514
679
|
*
|
|
515
|
-
* @
|
|
680
|
+
* AC: @config-shadow ac-3 ac-4 ac-5 — handles different remote types
|
|
681
|
+
* AC: @config-shadow ac-6 — error with guidance if named remote doesn't exist
|
|
682
|
+
* AC: @config-shadow ac-7 — backward compat when called without config
|
|
683
|
+
*
|
|
684
|
+
* @param worktreeDir Path to shadow worktree
|
|
516
685
|
* @param projectRoot Git repository root
|
|
517
|
-
* @
|
|
686
|
+
* @param options Optional shadow configuration
|
|
687
|
+
* @returns Result with success status and error details if applicable
|
|
518
688
|
*/
|
|
519
|
-
export async function ensureRemoteTracking(worktreeDir, projectRoot) {
|
|
689
|
+
export async function ensureRemoteTracking(worktreeDir, projectRoot, options) {
|
|
690
|
+
const branchName = getBranchName(options);
|
|
520
691
|
// Check if already has tracking
|
|
521
|
-
if (await hasRemoteTracking(worktreeDir)) {
|
|
522
|
-
return true;
|
|
692
|
+
if (await hasRemoteTracking(worktreeDir, options)) {
|
|
693
|
+
return { success: true };
|
|
694
|
+
}
|
|
695
|
+
// Determine remote name to use
|
|
696
|
+
let remoteName = "origin";
|
|
697
|
+
if (options?.remote) {
|
|
698
|
+
const remoteType = options.remoteType ?? "named";
|
|
699
|
+
if (remoteType === "named") {
|
|
700
|
+
// AC: ac-3 — use the named remote directly
|
|
701
|
+
remoteName = options.remote;
|
|
702
|
+
// AC: ac-6 — verify the named remote exists with guidance
|
|
703
|
+
if (!(await hasRemote(projectRoot, remoteName))) {
|
|
704
|
+
// Named remote doesn't exist - provide helpful guidance
|
|
705
|
+
return {
|
|
706
|
+
success: false,
|
|
707
|
+
missingRemote: remoteName,
|
|
708
|
+
guidance: `Remote '${remoteName}' does not exist. To fix this:\n` +
|
|
709
|
+
` 1. Add the remote: git remote add ${remoteName} <url>\n` +
|
|
710
|
+
` 2. Or update kspec.config.yaml to use an existing remote\n` +
|
|
711
|
+
` 3. Or remove shadow.remote to use the default 'origin' remote`,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
else if (remoteType === "path" || remoteType === "url") {
|
|
716
|
+
// AC: ac-4 ac-5 — add a git remote for path/URL if not already present
|
|
717
|
+
const specRemoteName = "kspec-specs";
|
|
718
|
+
const hasSpecsRemote = await hasRemote(projectRoot, specRemoteName);
|
|
719
|
+
if (!hasSpecsRemote) {
|
|
720
|
+
try {
|
|
721
|
+
// Expand tilde for paths if needed
|
|
722
|
+
let remoteTarget = options.remote;
|
|
723
|
+
if (remoteType === "path" && remoteTarget.startsWith("~")) {
|
|
724
|
+
remoteTarget = remoteTarget.replace(/^~/, process.env.HOME || process.env.USERPROFILE || "~");
|
|
725
|
+
}
|
|
726
|
+
await execAsync(`git remote add ${specRemoteName} "${remoteTarget}"`, {
|
|
727
|
+
cwd: projectRoot,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
catch {
|
|
731
|
+
// Remote add failed - may already exist with different URL
|
|
732
|
+
return { success: false };
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
remoteName = specRemoteName;
|
|
736
|
+
}
|
|
523
737
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
738
|
+
else {
|
|
739
|
+
// No remote configured - check if main branch has origin
|
|
740
|
+
if (!(await hasRemote(projectRoot))) {
|
|
741
|
+
return { success: false };
|
|
742
|
+
}
|
|
527
743
|
}
|
|
528
|
-
// Set up tracking for shadow branch
|
|
744
|
+
// Set up tracking for shadow branch
|
|
529
745
|
try {
|
|
530
|
-
await execAsync(`git config branch.${
|
|
531
|
-
|
|
532
|
-
|
|
746
|
+
await execAsync(`git config branch.${branchName}.remote ${remoteName}`, {
|
|
747
|
+
cwd: worktreeDir,
|
|
748
|
+
});
|
|
749
|
+
await execAsync(`git config branch.${branchName}.merge refs/heads/${branchName}`, { cwd: worktreeDir });
|
|
750
|
+
return { success: true };
|
|
533
751
|
}
|
|
534
752
|
catch {
|
|
535
|
-
return false;
|
|
753
|
+
return { success: false };
|
|
536
754
|
}
|
|
537
755
|
}
|
|
538
756
|
/**
|
|
@@ -541,36 +759,46 @@ export async function ensureRemoteTracking(worktreeDir, projectRoot) {
|
|
|
541
759
|
* AC-8: Automatically sets up tracking if main branch has remote.
|
|
542
760
|
* Silently ignores errors - the local commit succeeded regardless.
|
|
543
761
|
*
|
|
544
|
-
* @
|
|
762
|
+
* AC: @config-shadow ac-7 — backward compat when called without config
|
|
763
|
+
*
|
|
764
|
+
* @param worktreeDir Path to shadow worktree
|
|
545
765
|
* @param verbose Enable debug output
|
|
766
|
+
* @param options Optional shadow configuration
|
|
546
767
|
*/
|
|
547
|
-
export async function shadowPushAsync(worktreeDir, verbose) {
|
|
768
|
+
export async function shadowPushAsync(worktreeDir, verbose, options) {
|
|
548
769
|
const debug = isDebugMode(verbose);
|
|
549
|
-
// AC-8
|
|
770
|
+
// AC: @shadow-sync ac-8 - Auto-configure tracking if main has remote but shadow doesn't
|
|
550
771
|
const projectRoot = path.dirname(worktreeDir);
|
|
551
|
-
await ensureRemoteTracking(worktreeDir, projectRoot);
|
|
772
|
+
const trackingResult = await ensureRemoteTracking(worktreeDir, projectRoot, options);
|
|
773
|
+
// AC: @config-shadow ac-6 — log guidance if named remote doesn't exist
|
|
774
|
+
if (!trackingResult.success && trackingResult.missingRemote) {
|
|
775
|
+
if (debug) {
|
|
776
|
+
console.error(`[DEBUG] Shadow push: ${trackingResult.guidance}`);
|
|
777
|
+
}
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
552
780
|
// Check if tracking is configured before attempting push
|
|
553
|
-
if (!(await hasRemoteTracking(worktreeDir))) {
|
|
781
|
+
if (!(await hasRemoteTracking(worktreeDir, options))) {
|
|
554
782
|
if (debug) {
|
|
555
|
-
console.error(
|
|
783
|
+
console.error("[DEBUG] Shadow push: No remote tracking configured, skipping");
|
|
556
784
|
}
|
|
557
|
-
return; // AC-4
|
|
785
|
+
return; // AC: @shadow-sync ac-4 - silently skip if no tracking
|
|
558
786
|
}
|
|
559
787
|
try {
|
|
560
788
|
if (debug) {
|
|
561
789
|
console.error(`[DEBUG] Shadow push: git push (cwd: ${worktreeDir})`);
|
|
562
790
|
}
|
|
563
791
|
// Don't await - fire and forget
|
|
564
|
-
execAsync(
|
|
792
|
+
execAsync("git push", { cwd: worktreeDir }).catch((err) => {
|
|
565
793
|
if (debug) {
|
|
566
|
-
console.error(
|
|
794
|
+
console.error("[DEBUG] Shadow push failed:", err);
|
|
567
795
|
}
|
|
568
796
|
// Silently ignore push failures - local state is correct
|
|
569
797
|
});
|
|
570
798
|
}
|
|
571
799
|
catch (err) {
|
|
572
800
|
if (debug) {
|
|
573
|
-
console.error(
|
|
801
|
+
console.error("[DEBUG] Shadow push error:", err);
|
|
574
802
|
}
|
|
575
803
|
}
|
|
576
804
|
}
|
|
@@ -580,26 +808,37 @@ export async function shadowPushAsync(worktreeDir, verbose) {
|
|
|
580
808
|
* AC-6: Uses --ff-only first, falls back to --rebase.
|
|
581
809
|
* AC-3: On conflict, returns failure with suggestion.
|
|
582
810
|
* AC-8: Automatically sets up tracking if main branch has remote.
|
|
811
|
+
*
|
|
812
|
+
* AC: @config-shadow ac-7 — backward compat when called without config
|
|
813
|
+
*
|
|
814
|
+
* @param worktreeDir Path to shadow worktree
|
|
815
|
+
* @param options Optional shadow configuration
|
|
583
816
|
*/
|
|
584
|
-
export async function shadowPull(worktreeDir) {
|
|
817
|
+
export async function shadowPull(worktreeDir, options) {
|
|
818
|
+
const branchName = getBranchName(options);
|
|
585
819
|
const result = {
|
|
586
820
|
success: false,
|
|
587
821
|
pulled: false,
|
|
588
822
|
pushed: false,
|
|
589
823
|
hadConflict: false,
|
|
590
824
|
};
|
|
591
|
-
// AC-8
|
|
825
|
+
// AC: @shadow-sync ac-8 - Auto-configure tracking if main has remote but shadow doesn't
|
|
592
826
|
const projectRoot = path.dirname(worktreeDir);
|
|
593
|
-
await ensureRemoteTracking(worktreeDir, projectRoot);
|
|
594
|
-
// AC
|
|
595
|
-
if (!
|
|
827
|
+
const trackingResult = await ensureRemoteTracking(worktreeDir, projectRoot, options);
|
|
828
|
+
// AC: @config-shadow ac-6 — error with guidance if named remote doesn't exist
|
|
829
|
+
if (!trackingResult.success && trackingResult.missingRemote) {
|
|
830
|
+
result.error = trackingResult.guidance;
|
|
831
|
+
return result;
|
|
832
|
+
}
|
|
833
|
+
// AC: @shadow-sync ac-4 - Skip if no remote tracking
|
|
834
|
+
if (!(await hasRemoteTracking(worktreeDir, options))) {
|
|
596
835
|
result.success = true;
|
|
597
836
|
return result;
|
|
598
837
|
}
|
|
599
838
|
// Check if remote branch exists before attempting pull
|
|
600
839
|
// Fetch first to ensure refs are up to date
|
|
601
840
|
await fetchRemote(projectRoot);
|
|
602
|
-
const remoteHasBranch = await remoteBranchExists(projectRoot,
|
|
841
|
+
const remoteHasBranch = await remoteBranchExists(projectRoot, branchName);
|
|
603
842
|
if (!remoteHasBranch) {
|
|
604
843
|
// Remote branch doesn't exist yet - nothing to pull, but success
|
|
605
844
|
result.success = true;
|
|
@@ -607,7 +846,7 @@ export async function shadowPull(worktreeDir) {
|
|
|
607
846
|
}
|
|
608
847
|
try {
|
|
609
848
|
// Try fast-forward only first (cleanest)
|
|
610
|
-
await execAsync(
|
|
849
|
+
await execAsync("git pull --ff-only", { cwd: worktreeDir });
|
|
611
850
|
result.success = true;
|
|
612
851
|
result.pulled = true;
|
|
613
852
|
return result;
|
|
@@ -616,8 +855,8 @@ export async function shadowPull(worktreeDir) {
|
|
|
616
855
|
// Fast-forward failed, try rebase
|
|
617
856
|
}
|
|
618
857
|
try {
|
|
619
|
-
// AC-6
|
|
620
|
-
await execAsync(
|
|
858
|
+
// AC: @shadow-sync ac-6 - Fall back to rebase
|
|
859
|
+
await execAsync("git pull --rebase", { cwd: worktreeDir });
|
|
621
860
|
result.success = true;
|
|
622
861
|
result.pulled = true;
|
|
623
862
|
return result;
|
|
@@ -625,31 +864,36 @@ export async function shadowPull(worktreeDir) {
|
|
|
625
864
|
catch {
|
|
626
865
|
// Rebase failed - likely conflict
|
|
627
866
|
}
|
|
628
|
-
// AC-3
|
|
867
|
+
// AC: @shadow-sync ac-3 - Conflict detected - abort rebase and report
|
|
629
868
|
try {
|
|
630
|
-
await execAsync(
|
|
869
|
+
await execAsync("git rebase --abort", { cwd: worktreeDir });
|
|
631
870
|
}
|
|
632
871
|
catch {
|
|
633
872
|
// May not be in rebase state, ignore
|
|
634
873
|
}
|
|
635
874
|
result.hadConflict = true;
|
|
636
|
-
result.error =
|
|
875
|
+
result.error = "Sync conflict detected. Run `kspec shadow resolve` to fix.";
|
|
637
876
|
return result;
|
|
638
877
|
}
|
|
639
878
|
/**
|
|
640
879
|
* Full sync operation: pull then push.
|
|
641
880
|
* Used by session start and explicit sync commands.
|
|
881
|
+
*
|
|
882
|
+
* AC: @config-shadow ac-7 — backward compat when called without config
|
|
883
|
+
*
|
|
884
|
+
* @param worktreeDir Path to shadow worktree
|
|
885
|
+
* @param options Optional shadow configuration
|
|
642
886
|
*/
|
|
643
|
-
export async function shadowSync(worktreeDir) {
|
|
887
|
+
export async function shadowSync(worktreeDir, options) {
|
|
644
888
|
// First pull
|
|
645
|
-
const pullResult = await shadowPull(worktreeDir);
|
|
889
|
+
const pullResult = await shadowPull(worktreeDir, options);
|
|
646
890
|
if (!pullResult.success) {
|
|
647
891
|
return pullResult;
|
|
648
892
|
}
|
|
649
893
|
// Then push (only if tracking configured, checked inside)
|
|
650
|
-
if (await hasRemoteTracking(worktreeDir)) {
|
|
894
|
+
if (await hasRemoteTracking(worktreeDir, options)) {
|
|
651
895
|
try {
|
|
652
|
-
await execAsync(
|
|
896
|
+
await execAsync("git push", { cwd: worktreeDir });
|
|
653
897
|
pullResult.pushed = true;
|
|
654
898
|
}
|
|
655
899
|
catch {
|
|
@@ -665,7 +909,7 @@ export async function shadowSync(worktreeDir) {
|
|
|
665
909
|
async function hasUncommittedGitignore(projectRoot) {
|
|
666
910
|
try {
|
|
667
911
|
// Check both staged and unstaged changes to .gitignore
|
|
668
|
-
const { stdout } = await execAsync(
|
|
912
|
+
const { stdout } = await execAsync("git status --porcelain .gitignore", {
|
|
669
913
|
cwd: projectRoot,
|
|
670
914
|
});
|
|
671
915
|
return stdout.trim().length > 0;
|
|
@@ -675,41 +919,51 @@ async function hasUncommittedGitignore(projectRoot) {
|
|
|
675
919
|
}
|
|
676
920
|
}
|
|
677
921
|
/**
|
|
678
|
-
* Commit only .gitignore with a message
|
|
922
|
+
* Commit only .gitignore with a message.
|
|
923
|
+
*
|
|
924
|
+
* @param projectRoot Git repository root
|
|
925
|
+
* @param directoryName Shadow directory name (for commit message)
|
|
679
926
|
*/
|
|
680
|
-
async function commitGitignore(projectRoot) {
|
|
681
|
-
await execAsync(
|
|
682
|
-
await execAsync(
|
|
927
|
+
async function commitGitignore(projectRoot, directoryName) {
|
|
928
|
+
await execAsync("git add .gitignore", { cwd: projectRoot });
|
|
929
|
+
await execAsync(`git commit -m "chore: add ${directoryName}/ to .gitignore for shadow branch"`, {
|
|
683
930
|
cwd: projectRoot,
|
|
684
931
|
});
|
|
685
932
|
}
|
|
686
933
|
/**
|
|
687
|
-
* Add
|
|
934
|
+
* Add shadow directory to .gitignore if not already present.
|
|
688
935
|
* Fails if .gitignore has uncommitted changes.
|
|
689
936
|
* Commits the change after adding.
|
|
937
|
+
*
|
|
938
|
+
* AC: @config-shadow ac-2 — uses configured directory name
|
|
939
|
+
* AC: @config-shadow ac-7 — defaults to .kspec when not provided
|
|
940
|
+
*
|
|
941
|
+
* @param projectRoot Git repository root
|
|
942
|
+
* @param options Optional shadow configuration
|
|
690
943
|
*/
|
|
691
|
-
async function ensureGitignore(projectRoot) {
|
|
692
|
-
const
|
|
693
|
-
const
|
|
944
|
+
async function ensureGitignore(projectRoot, options) {
|
|
945
|
+
const directoryName = getDirectoryName(options);
|
|
946
|
+
const gitignorePath = path.join(projectRoot, ".gitignore");
|
|
947
|
+
const entry = `${directoryName}/`;
|
|
694
948
|
// Fail fast if .gitignore has uncommitted changes
|
|
695
949
|
if (await hasUncommittedGitignore(projectRoot)) {
|
|
696
|
-
throw new ShadowError(
|
|
950
|
+
throw new ShadowError(".gitignore has uncommitted changes", "GIT_ERROR", "Commit or stash your .gitignore changes before running kspec init.");
|
|
697
951
|
}
|
|
698
952
|
try {
|
|
699
|
-
let content =
|
|
953
|
+
let content = "";
|
|
700
954
|
try {
|
|
701
|
-
content = await fs.readFile(gitignorePath,
|
|
955
|
+
content = await fs.readFile(gitignorePath, "utf-8");
|
|
702
956
|
}
|
|
703
957
|
catch {
|
|
704
958
|
// File doesn't exist, will create
|
|
705
959
|
}
|
|
706
960
|
// Check if already present (handle various formats)
|
|
707
|
-
const lines = content.split(
|
|
961
|
+
const lines = content.split("\n");
|
|
708
962
|
const patterns = [
|
|
709
|
-
|
|
710
|
-
`${
|
|
711
|
-
`/${
|
|
712
|
-
`/${
|
|
963
|
+
directoryName,
|
|
964
|
+
`${directoryName}/`,
|
|
965
|
+
`/${directoryName}`,
|
|
966
|
+
`/${directoryName}/`,
|
|
713
967
|
];
|
|
714
968
|
for (const line of lines) {
|
|
715
969
|
const trimmed = line.trim();
|
|
@@ -718,19 +972,19 @@ async function ensureGitignore(projectRoot) {
|
|
|
718
972
|
}
|
|
719
973
|
}
|
|
720
974
|
// Add to gitignore
|
|
721
|
-
const newContent = content.endsWith(
|
|
975
|
+
const newContent = content.endsWith("\n") || content === ""
|
|
722
976
|
? `${content}${entry}\n`
|
|
723
977
|
: `${content}\n${entry}\n`;
|
|
724
|
-
await fs.writeFile(gitignorePath, newContent,
|
|
978
|
+
await fs.writeFile(gitignorePath, newContent, "utf-8");
|
|
725
979
|
// Commit the change
|
|
726
|
-
await commitGitignore(projectRoot);
|
|
980
|
+
await commitGitignore(projectRoot, directoryName);
|
|
727
981
|
return true;
|
|
728
982
|
}
|
|
729
983
|
catch (error) {
|
|
730
984
|
if (error instanceof ShadowError) {
|
|
731
985
|
throw error;
|
|
732
986
|
}
|
|
733
|
-
throw new ShadowError(`Failed to update .gitignore: ${error}`,
|
|
987
|
+
throw new ShadowError(`Failed to update .gitignore: ${error}`, "GIT_ERROR", "Check file permissions for .gitignore");
|
|
734
988
|
}
|
|
735
989
|
}
|
|
736
990
|
/**
|
|
@@ -752,12 +1006,6 @@ project:
|
|
|
752
1006
|
# Module includes
|
|
753
1007
|
includes:
|
|
754
1008
|
- modules/main.yaml
|
|
755
|
-
|
|
756
|
-
# Configuration
|
|
757
|
-
config:
|
|
758
|
-
validation:
|
|
759
|
-
strict_refs: true
|
|
760
|
-
require_acceptance: false
|
|
761
1009
|
`;
|
|
762
1010
|
}
|
|
763
1011
|
/**
|
|
@@ -790,6 +1038,13 @@ function generateShadowInbox() {
|
|
|
790
1038
|
items: []
|
|
791
1039
|
`;
|
|
792
1040
|
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Get the kspec package root directory.
|
|
1043
|
+
* Navigates from dist/parser/ to package root.
|
|
1044
|
+
*/
|
|
1045
|
+
function getPackageRoot() {
|
|
1046
|
+
return path.resolve(__dirname, "..", "..");
|
|
1047
|
+
}
|
|
793
1048
|
/**
|
|
794
1049
|
* Install pre-commit hook to protect kspec-meta branch.
|
|
795
1050
|
* Hook prevents direct commits to shadow branch unless KSPEC_SHADOW_COMMIT=1.
|
|
@@ -797,15 +1052,19 @@ items: []
|
|
|
797
1052
|
* Note: Git worktrees use hooks from the main .git/hooks directory (via commondir),
|
|
798
1053
|
* not from .git/worktrees/-kspec/hooks. So we install to main hooks directory.
|
|
799
1054
|
*
|
|
1055
|
+
* The hook source is located in the kspec package's templates/hooks/ directory.
|
|
1056
|
+
*
|
|
800
1057
|
* @param projectRoot Git repository root
|
|
801
1058
|
* @returns true if hook was installed, false if already exists
|
|
802
1059
|
*/
|
|
803
1060
|
async function installShadowHook(projectRoot) {
|
|
804
|
-
const hooksDir = path.join(projectRoot,
|
|
805
|
-
const hookPath = path.join(hooksDir,
|
|
806
|
-
|
|
1061
|
+
const hooksDir = path.join(projectRoot, ".git", "hooks");
|
|
1062
|
+
const hookPath = path.join(hooksDir, "pre-commit");
|
|
1063
|
+
// Look for hook in package templates directory
|
|
1064
|
+
const packageRoot = getPackageRoot();
|
|
1065
|
+
const sourceHookPath = path.join(packageRoot, "templates", "hooks", "pre-commit");
|
|
807
1066
|
try {
|
|
808
|
-
// Check if source hook exists
|
|
1067
|
+
// Check if source hook exists in package templates
|
|
809
1068
|
try {
|
|
810
1069
|
await fs.access(sourceHookPath);
|
|
811
1070
|
}
|
|
@@ -822,12 +1081,12 @@ async function installShadowHook(projectRoot) {
|
|
|
822
1081
|
catch {
|
|
823
1082
|
// Hook doesn't exist - install it
|
|
824
1083
|
}
|
|
825
|
-
// Copy hook from
|
|
826
|
-
const hookContent = await fs.readFile(sourceHookPath,
|
|
1084
|
+
// Copy hook from package templates
|
|
1085
|
+
const hookContent = await fs.readFile(sourceHookPath, "utf-8");
|
|
827
1086
|
await fs.writeFile(hookPath, hookContent, { mode: 0o755 });
|
|
828
1087
|
return true;
|
|
829
1088
|
}
|
|
830
|
-
catch (
|
|
1089
|
+
catch (_error) {
|
|
831
1090
|
// Silently fail - hook installation is optional
|
|
832
1091
|
return false;
|
|
833
1092
|
}
|
|
@@ -838,13 +1097,87 @@ async function installShadowHook(projectRoot) {
|
|
|
838
1097
|
function toSlug(projectName) {
|
|
839
1098
|
return projectName
|
|
840
1099
|
.toLowerCase()
|
|
841
|
-
.replace(/[^a-z0-9]+/g,
|
|
842
|
-
.replace(/^-|-$/g,
|
|
1100
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
1101
|
+
.replace(/^-|-$/g, "");
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Configure git merge driver for kspec YAML files.
|
|
1105
|
+
* AC: @yaml-merge-driver ac-12
|
|
1106
|
+
*
|
|
1107
|
+
* Configures the merge driver in .git/config and adds .gitattributes in the shadow branch.
|
|
1108
|
+
*
|
|
1109
|
+
* @param projectRoot Git repository root
|
|
1110
|
+
* @param worktreeDir Path to shadow worktree directory
|
|
1111
|
+
* @returns true if configuration was successful
|
|
1112
|
+
*/
|
|
1113
|
+
async function configureMergeDriver(projectRoot, worktreeDir) {
|
|
1114
|
+
try {
|
|
1115
|
+
// Step 1: Configure merge driver in .git/config
|
|
1116
|
+
const kspecPath = execSync("which kspec", {
|
|
1117
|
+
encoding: "utf-8",
|
|
1118
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1119
|
+
}).trim();
|
|
1120
|
+
// Add merge driver configuration to git config
|
|
1121
|
+
try {
|
|
1122
|
+
execSync(`git config merge.kspec.name "Kspec YAML semantic merge driver"`, {
|
|
1123
|
+
cwd: projectRoot,
|
|
1124
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1125
|
+
});
|
|
1126
|
+
execSync(`git config merge.kspec.driver "${kspecPath} merge-driver %O %A %B --non-interactive"`, {
|
|
1127
|
+
cwd: projectRoot,
|
|
1128
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
catch (error) {
|
|
1132
|
+
// Config may fail if already set - check if it's set correctly
|
|
1133
|
+
try {
|
|
1134
|
+
const existingDriver = execSync("git config merge.kspec.driver", {
|
|
1135
|
+
cwd: projectRoot,
|
|
1136
|
+
encoding: "utf-8",
|
|
1137
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1138
|
+
}).trim();
|
|
1139
|
+
if (!existingDriver.includes("kspec merge-driver")) {
|
|
1140
|
+
throw new Error("Merge driver config exists but is incorrect");
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
catch {
|
|
1144
|
+
throw error; // Re-throw original error
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
// Step 2: Create .gitattributes in shadow branch
|
|
1148
|
+
const gitattributesPath = path.join(worktreeDir, ".gitattributes");
|
|
1149
|
+
// Check if .gitattributes exists
|
|
1150
|
+
let existingContent = "";
|
|
1151
|
+
try {
|
|
1152
|
+
existingContent = await fs.readFile(gitattributesPath, "utf-8");
|
|
1153
|
+
}
|
|
1154
|
+
catch {
|
|
1155
|
+
// File doesn't exist, that's fine
|
|
1156
|
+
}
|
|
1157
|
+
// Check if merge driver is already configured
|
|
1158
|
+
if (!existingContent.includes("merge=kspec")) {
|
|
1159
|
+
const attributesContent = existingContent
|
|
1160
|
+
? existingContent + "\n"
|
|
1161
|
+
: "# Git attributes for kspec\n\n";
|
|
1162
|
+
await fs.writeFile(gitattributesPath, attributesContent + "*.yaml merge=kspec\n*.yml merge=kspec\n", "utf-8");
|
|
1163
|
+
// Commit .gitattributes to shadow branch
|
|
1164
|
+
await shadowAutoCommit(worktreeDir, "Configure kspec merge driver");
|
|
1165
|
+
}
|
|
1166
|
+
return true;
|
|
1167
|
+
}
|
|
1168
|
+
catch (_error) {
|
|
1169
|
+
// Silently fail - merge driver configuration is optional
|
|
1170
|
+
return false;
|
|
1171
|
+
}
|
|
843
1172
|
}
|
|
844
1173
|
/**
|
|
845
1174
|
* Initialize shadow branch and worktree.
|
|
846
1175
|
* Creates orphan branch, worktree, updates gitignore, and creates initial structure.
|
|
847
1176
|
*
|
|
1177
|
+
* AC: @config-shadow ac-1 — creates orphan branch with configured name
|
|
1178
|
+
* AC: @config-shadow ac-2 — creates worktree at configured directory
|
|
1179
|
+
* AC: @config-shadow ac-7 — defaults to constants when options not provided
|
|
1180
|
+
*
|
|
848
1181
|
* @param projectRoot Git repository root
|
|
849
1182
|
* @param options Initialization options
|
|
850
1183
|
* @returns Result indicating what was created
|
|
@@ -862,12 +1195,15 @@ export async function initializeShadow(projectRoot, options = {}) {
|
|
|
862
1195
|
};
|
|
863
1196
|
// Check if we're in a git repo
|
|
864
1197
|
if (!(await isGitRepo(projectRoot))) {
|
|
865
|
-
result.error =
|
|
1198
|
+
result.error = "Not a git repository";
|
|
866
1199
|
return result;
|
|
867
1200
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
const
|
|
1201
|
+
// AC: ac-1 ac-2 — use configured branch/directory or defaults
|
|
1202
|
+
const branchName = getBranchName(options.shadow);
|
|
1203
|
+
const directoryName = getDirectoryName(options.shadow);
|
|
1204
|
+
const worktreeDir = path.join(projectRoot, directoryName);
|
|
1205
|
+
// Check current status with configured options
|
|
1206
|
+
const status = await getShadowStatus(projectRoot, options.shadow);
|
|
871
1207
|
// Handle existing shadow branch
|
|
872
1208
|
if (status.healthy && !options.force) {
|
|
873
1209
|
result.alreadyExists = true;
|
|
@@ -875,20 +1211,27 @@ export async function initializeShadow(projectRoot, options = {}) {
|
|
|
875
1211
|
return result;
|
|
876
1212
|
}
|
|
877
1213
|
// Derive project name if not provided
|
|
878
|
-
const projectName = options.projectName ||
|
|
879
|
-
|
|
880
|
-
|
|
1214
|
+
const projectName = options.projectName ||
|
|
1215
|
+
path
|
|
1216
|
+
.basename(projectRoot)
|
|
1217
|
+
.replace(/[-_]/g, " ")
|
|
1218
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
881
1219
|
const slug = toSlug(projectName);
|
|
1220
|
+
// Determine remote name to use
|
|
1221
|
+
let remoteName = "origin";
|
|
1222
|
+
if (options.shadow?.remote && options.shadow.remoteType === "named") {
|
|
1223
|
+
remoteName = options.shadow.remote;
|
|
1224
|
+
}
|
|
882
1225
|
// Check for remote shadow branch (AC-4: fetch to ensure refs are up to date)
|
|
883
|
-
const remoteExists = await hasRemote(projectRoot);
|
|
1226
|
+
const remoteExists = await hasRemote(projectRoot, remoteName);
|
|
884
1227
|
let remoteHasShadow = false;
|
|
885
1228
|
if (remoteExists) {
|
|
886
|
-
await fetchRemote(projectRoot); // Best effort, ignore failures
|
|
887
|
-
remoteHasShadow = await remoteBranchExists(projectRoot,
|
|
1229
|
+
await fetchRemote(projectRoot, remoteName); // Best effort, ignore failures
|
|
1230
|
+
remoteHasShadow = await remoteBranchExists(projectRoot, branchName, remoteName);
|
|
888
1231
|
}
|
|
889
1232
|
try {
|
|
890
|
-
// Step 1: Update .gitignore first (before creating
|
|
891
|
-
result.gitignoreUpdated = await ensureGitignore(projectRoot);
|
|
1233
|
+
// Step 1: Update .gitignore first (before creating worktree)
|
|
1234
|
+
result.gitignoreUpdated = await ensureGitignore(projectRoot, options.shadow);
|
|
892
1235
|
// Step 2: Create worktree with orphan branch (or attach to existing branch)
|
|
893
1236
|
if (!status.worktreeExists || !status.worktreeLinked) {
|
|
894
1237
|
// Remove existing directory if present but not linked
|
|
@@ -897,7 +1240,7 @@ export async function initializeShadow(projectRoot, options = {}) {
|
|
|
897
1240
|
}
|
|
898
1241
|
// Remove stale worktree reference if any
|
|
899
1242
|
try {
|
|
900
|
-
await execAsync(`git worktree remove ${
|
|
1243
|
+
await execAsync(`git worktree remove "${directoryName}" --force`, {
|
|
901
1244
|
cwd: projectRoot,
|
|
902
1245
|
});
|
|
903
1246
|
}
|
|
@@ -905,27 +1248,28 @@ export async function initializeShadow(projectRoot, options = {}) {
|
|
|
905
1248
|
// Ignore - worktree may not exist in git's list
|
|
906
1249
|
}
|
|
907
1250
|
if (remoteHasShadow) {
|
|
908
|
-
// AC-1
|
|
909
|
-
await execAsync(`git worktree add ${
|
|
1251
|
+
// AC: @shadow-init-remote ac-1 - Remote has shadow branch - create worktree from it with tracking
|
|
1252
|
+
await execAsync(`git worktree add "${directoryName}" ${branchName}`, { cwd: projectRoot });
|
|
910
1253
|
// Set up tracking for the branch
|
|
911
|
-
await execAsync(`git branch --set-upstream-to
|
|
1254
|
+
await execAsync(`git branch --set-upstream-to=${remoteName}/${branchName} ${branchName}`, { cwd: projectRoot });
|
|
912
1255
|
result.createdFromRemote = true;
|
|
913
1256
|
}
|
|
914
1257
|
else if (!status.branchExists) {
|
|
915
|
-
// AC-2
|
|
916
|
-
|
|
1258
|
+
// AC: @shadow-init-remote ac-2 ac-3 - No remote branch or no remote - create orphan branch
|
|
1259
|
+
// AC: @config-shadow ac-1 — use configured branch name
|
|
1260
|
+
await execAsync(`git worktree add --orphan -b ${branchName} "${directoryName}"`, { cwd: projectRoot });
|
|
917
1261
|
result.branchCreated = true;
|
|
918
1262
|
}
|
|
919
1263
|
else {
|
|
920
1264
|
// Attach to existing local branch
|
|
921
|
-
await execAsync(`git worktree add ${
|
|
1265
|
+
await execAsync(`git worktree add "${directoryName}" ${branchName}`, { cwd: projectRoot });
|
|
922
1266
|
}
|
|
923
1267
|
result.worktreeCreated = true;
|
|
924
1268
|
}
|
|
925
1269
|
// Step 3: Create initial structure if empty (only for new branches, not remote)
|
|
926
1270
|
const manifestPath = path.join(worktreeDir, `${slug}.yaml`);
|
|
927
|
-
const modulesDir = path.join(worktreeDir,
|
|
928
|
-
const moduleFilePath = path.join(modulesDir,
|
|
1271
|
+
const modulesDir = path.join(worktreeDir, "modules");
|
|
1272
|
+
const moduleFilePath = path.join(modulesDir, "main.yaml");
|
|
929
1273
|
const tasksPath = path.join(worktreeDir, `${slug}.tasks.yaml`);
|
|
930
1274
|
const inboxPath = path.join(worktreeDir, `${slug}.inbox.yaml`);
|
|
931
1275
|
let filesCreated = false;
|
|
@@ -933,18 +1277,24 @@ export async function initializeShadow(projectRoot, options = {}) {
|
|
|
933
1277
|
try {
|
|
934
1278
|
// Look for any .yaml manifest file (project name may differ)
|
|
935
1279
|
const files = await fs.readdir(worktreeDir);
|
|
936
|
-
const hasManifest = files.some(f => f.endsWith(
|
|
1280
|
+
const hasManifest = files.some((f) => f.endsWith(".yaml") &&
|
|
1281
|
+
!f.includes(".tasks.") &&
|
|
1282
|
+
!f.includes(".inbox."));
|
|
937
1283
|
if (!hasManifest) {
|
|
938
|
-
throw new Error(
|
|
1284
|
+
throw new Error("No manifest found");
|
|
939
1285
|
}
|
|
940
1286
|
}
|
|
941
1287
|
catch {
|
|
942
1288
|
// Manifest doesn't exist, create initial structure
|
|
943
1289
|
await fs.mkdir(modulesDir, { recursive: true });
|
|
944
|
-
await fs.writeFile(manifestPath, generateShadowManifest(projectName),
|
|
945
|
-
await fs.writeFile(moduleFilePath, generateShadowModule(projectName),
|
|
946
|
-
await fs.writeFile(tasksPath, generateShadowTasks(projectName),
|
|
947
|
-
await fs.writeFile(inboxPath, generateShadowInbox(),
|
|
1290
|
+
await fs.writeFile(manifestPath, generateShadowManifest(projectName), "utf-8");
|
|
1291
|
+
await fs.writeFile(moduleFilePath, generateShadowModule(projectName), "utf-8");
|
|
1292
|
+
await fs.writeFile(tasksPath, generateShadowTasks(projectName), "utf-8");
|
|
1293
|
+
await fs.writeFile(inboxPath, generateShadowInbox(), "utf-8");
|
|
1294
|
+
// AC: @artifacts-directory ac-init-creates, ac-gitignore-entry
|
|
1295
|
+
const artifactsDir = path.join(worktreeDir, "artifacts");
|
|
1296
|
+
await fs.mkdir(artifactsDir, { recursive: true });
|
|
1297
|
+
await fs.writeFile(path.join(worktreeDir, ".gitignore"), "# Ephemeral artifacts - reports, exports, generated files\n# Not tracked in shadow branch\nartifacts/\n", "utf-8");
|
|
948
1298
|
filesCreated = true;
|
|
949
1299
|
}
|
|
950
1300
|
// Step 4: Initial commit if files were created
|
|
@@ -953,10 +1303,13 @@ export async function initializeShadow(projectRoot, options = {}) {
|
|
|
953
1303
|
}
|
|
954
1304
|
// Step 5: AC-2: Push new branch to remote to establish tracking
|
|
955
1305
|
if (result.branchCreated && remoteExists && !remoteHasShadow) {
|
|
956
|
-
result.pushedToRemote = await pushShadowBranch(worktreeDir);
|
|
1306
|
+
result.pushedToRemote = await pushShadowBranch(worktreeDir, remoteName, options.shadow);
|
|
957
1307
|
}
|
|
958
1308
|
// Step 6: Install pre-commit hook to protect shadow branch
|
|
959
1309
|
await installShadowHook(projectRoot);
|
|
1310
|
+
// Step 7: Configure merge driver for semantic YAML merging
|
|
1311
|
+
// AC: @yaml-merge-driver ac-12
|
|
1312
|
+
await configureMergeDriver(projectRoot, worktreeDir);
|
|
960
1313
|
result.success = true;
|
|
961
1314
|
return result;
|
|
962
1315
|
}
|
|
@@ -969,11 +1322,16 @@ export async function initializeShadow(projectRoot, options = {}) {
|
|
|
969
1322
|
* Repair a broken shadow branch setup.
|
|
970
1323
|
* Handles cases where worktree is disconnected or directory is missing.
|
|
971
1324
|
*
|
|
1325
|
+
* AC: @config-shadow ac-7 — backward compat when called without config
|
|
1326
|
+
*
|
|
972
1327
|
* @param projectRoot Git repository root
|
|
1328
|
+
* @param options Optional shadow configuration
|
|
973
1329
|
* @returns Result indicating what was repaired
|
|
974
1330
|
*/
|
|
975
|
-
export async function repairShadow(projectRoot) {
|
|
976
|
-
const
|
|
1331
|
+
export async function repairShadow(projectRoot, options) {
|
|
1332
|
+
const branchName = getBranchName(options);
|
|
1333
|
+
const directoryName = getDirectoryName(options);
|
|
1334
|
+
const status = await getShadowStatus(projectRoot, options);
|
|
977
1335
|
if (status.healthy) {
|
|
978
1336
|
return {
|
|
979
1337
|
success: true,
|
|
@@ -997,15 +1355,15 @@ export async function repairShadow(projectRoot) {
|
|
|
997
1355
|
alreadyExists: false,
|
|
998
1356
|
createdFromRemote: false,
|
|
999
1357
|
pushedToRemote: false,
|
|
1000
|
-
error:
|
|
1358
|
+
error: "Shadow branch does not exist. Run `kspec init` instead.",
|
|
1001
1359
|
};
|
|
1002
1360
|
}
|
|
1003
1361
|
// Branch exists but worktree is broken - repair it
|
|
1004
|
-
const worktreeDir = path.join(projectRoot,
|
|
1362
|
+
const worktreeDir = path.join(projectRoot, directoryName);
|
|
1005
1363
|
try {
|
|
1006
1364
|
// Remove stale worktree reference
|
|
1007
1365
|
try {
|
|
1008
|
-
await execAsync(`git worktree remove ${
|
|
1366
|
+
await execAsync(`git worktree remove "${directoryName}" --force`, {
|
|
1009
1367
|
cwd: projectRoot,
|
|
1010
1368
|
});
|
|
1011
1369
|
}
|
|
@@ -1016,15 +1374,18 @@ export async function repairShadow(projectRoot) {
|
|
|
1016
1374
|
await fs.rm(worktreeDir, { recursive: true, force: true });
|
|
1017
1375
|
// Prune stale worktree references (cleans up orphaned entries)
|
|
1018
1376
|
try {
|
|
1019
|
-
await execAsync(
|
|
1377
|
+
await execAsync("git worktree prune", { cwd: projectRoot });
|
|
1020
1378
|
}
|
|
1021
1379
|
catch {
|
|
1022
1380
|
// Ignore - prune is best-effort
|
|
1023
1381
|
}
|
|
1024
1382
|
// Recreate worktree
|
|
1025
|
-
await execAsync(`git worktree add ${
|
|
1383
|
+
await execAsync(`git worktree add "${directoryName}" ${branchName}`, { cwd: projectRoot });
|
|
1026
1384
|
// Install pre-commit hook
|
|
1027
1385
|
await installShadowHook(projectRoot);
|
|
1386
|
+
// AC: @artifacts-directory ac-repair-recreates
|
|
1387
|
+
const artifactsDir = path.join(worktreeDir, "artifacts");
|
|
1388
|
+
await fs.mkdir(artifactsDir, { recursive: true });
|
|
1028
1389
|
return {
|
|
1029
1390
|
success: true,
|
|
1030
1391
|
branchCreated: false,
|