@kynetic-ai/spec 0.10.0 → 0.12.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 +55 -455
- package/dist/agent-runtime/bootstrap.d.ts +31 -0
- package/dist/agent-runtime/bootstrap.d.ts.map +1 -0
- package/dist/agent-runtime/bootstrap.js +302 -0
- package/dist/agent-runtime/bootstrap.js.map +1 -0
- package/dist/agent-runtime/dispatch.d.ts +150 -10
- package/dist/agent-runtime/dispatch.d.ts.map +1 -1
- package/dist/agent-runtime/dispatch.js +1248 -244
- package/dist/agent-runtime/dispatch.js.map +1 -1
- package/dist/agent-runtime/invocation.d.ts +28 -1
- package/dist/agent-runtime/invocation.d.ts.map +1 -1
- package/dist/agent-runtime/invocation.js +172 -60
- package/dist/agent-runtime/invocation.js.map +1 -1
- package/dist/agent-runtime/prompts.d.ts +9 -0
- package/dist/agent-runtime/prompts.d.ts.map +1 -1
- package/dist/agent-runtime/prompts.js +42 -7
- package/dist/agent-runtime/prompts.js.map +1 -1
- package/dist/agent-runtime/session-event-accumulator.d.ts +83 -0
- package/dist/agent-runtime/session-event-accumulator.d.ts.map +1 -0
- package/dist/agent-runtime/session-event-accumulator.js +203 -0
- package/dist/agent-runtime/session-event-accumulator.js.map +1 -0
- package/dist/agent-runtime/session-event-types.d.ts +67 -0
- package/dist/agent-runtime/session-event-types.d.ts.map +1 -0
- package/dist/agent-runtime/session-event-types.js +13 -0
- package/dist/agent-runtime/session-event-types.js.map +1 -0
- package/dist/agent-runtime/workspace.d.ts +244 -0
- package/dist/agent-runtime/workspace.d.ts.map +1 -0
- package/dist/agent-runtime/workspace.js +2025 -0
- package/dist/agent-runtime/workspace.js.map +1 -0
- package/dist/agents/adapters.d.ts.map +1 -1
- package/dist/agents/adapters.js +58 -13
- package/dist/agents/adapters.js.map +1 -1
- package/dist/agents/spawner.d.ts +8 -0
- package/dist/agents/spawner.d.ts.map +1 -1
- package/dist/agents/spawner.js +25 -3
- package/dist/agents/spawner.js.map +1 -1
- package/dist/cli/batch-exec.js +1 -1
- package/dist/cli/batch-exec.js.map +1 -1
- package/dist/cli/command-annotations.d.ts +15 -3
- package/dist/cli/command-annotations.d.ts.map +1 -1
- package/dist/cli/command-annotations.js +23 -3
- package/dist/cli/command-annotations.js.map +1 -1
- package/dist/cli/commands/agent.d.ts +2 -0
- package/dist/cli/commands/agent.d.ts.map +1 -1
- package/dist/cli/commands/agent.js +144 -27
- package/dist/cli/commands/agent.js.map +1 -1
- package/dist/cli/commands/agents.d.ts.map +1 -1
- package/dist/cli/commands/agents.js +5 -5
- package/dist/cli/commands/agents.js.map +1 -1
- package/dist/cli/commands/derive.d.ts.map +1 -1
- package/dist/cli/commands/derive.js +118 -3
- package/dist/cli/commands/derive.js.map +1 -1
- package/dist/cli/commands/guard.d.ts.map +1 -1
- package/dist/cli/commands/guard.js +8 -6
- package/dist/cli/commands/guard.js.map +1 -1
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +20 -0
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/item.d.ts.map +1 -1
- package/dist/cli/commands/item.js +205 -47
- package/dist/cli/commands/item.js.map +1 -1
- package/dist/cli/commands/log.d.ts.map +1 -1
- package/dist/cli/commands/log.js +24 -10
- package/dist/cli/commands/log.js.map +1 -1
- package/dist/cli/commands/meta.d.ts.map +1 -1
- package/dist/cli/commands/meta.js +10 -1
- package/dist/cli/commands/meta.js.map +1 -1
- package/dist/cli/commands/plan-import.d.ts +3 -3
- package/dist/cli/commands/plan-import.d.ts.map +1 -1
- package/dist/cli/commands/plan-import.js +213 -528
- package/dist/cli/commands/plan-import.js.map +1 -1
- package/dist/cli/commands/plan.d.ts.map +1 -1
- package/dist/cli/commands/plan.js +533 -83
- package/dist/cli/commands/plan.js.map +1 -1
- package/dist/cli/commands/review.d.ts +14 -0
- package/dist/cli/commands/review.d.ts.map +1 -0
- package/dist/cli/commands/review.js +1142 -0
- package/dist/cli/commands/review.js.map +1 -0
- package/dist/cli/commands/serve.d.ts +1 -0
- package/dist/cli/commands/serve.d.ts.map +1 -1
- package/dist/cli/commands/serve.js +33 -10
- package/dist/cli/commands/serve.js.map +1 -1
- package/dist/cli/commands/session/checkpoint.d.ts +2 -4
- package/dist/cli/commands/session/checkpoint.d.ts.map +1 -1
- package/dist/cli/commands/session/checkpoint.js +6 -107
- package/dist/cli/commands/session/checkpoint.js.map +1 -1
- package/dist/cli/commands/session/commands.d.ts.map +1 -1
- package/dist/cli/commands/session/commands.js +33 -23
- package/dist/cli/commands/session/commands.js.map +1 -1
- package/dist/cli/commands/session/compact.js +4 -4
- package/dist/cli/commands/session/compact.js.map +1 -1
- package/dist/cli/commands/session/create.js +2 -2
- package/dist/cli/commands/session/create.js.map +1 -1
- package/dist/cli/commands/session/format.d.ts.map +1 -1
- package/dist/cli/commands/session/format.js +1 -6
- package/dist/cli/commands/session/format.js.map +1 -1
- package/dist/cli/commands/session/log.d.ts +32 -7
- package/dist/cli/commands/session/log.d.ts.map +1 -1
- package/dist/cli/commands/session/log.js +166 -60
- package/dist/cli/commands/session/log.js.map +1 -1
- package/dist/cli/commands/session/migrate.d.ts +9 -0
- package/dist/cli/commands/session/migrate.d.ts.map +1 -0
- package/dist/cli/commands/session/migrate.js +46 -0
- package/dist/cli/commands/session/migrate.js.map +1 -0
- package/dist/cli/commands/session/stale-close.d.ts.map +1 -1
- package/dist/cli/commands/session/stale-close.js +5 -8
- package/dist/cli/commands/session/stale-close.js.map +1 -1
- package/dist/cli/commands/session/types.d.ts +1 -1
- package/dist/cli/commands/session/types.d.ts.map +1 -1
- package/dist/cli/commands/setup.d.ts +2 -2
- package/dist/cli/commands/setup.d.ts.map +1 -1
- package/dist/cli/commands/setup.js +287 -257
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/shadow.d.ts.map +1 -1
- package/dist/cli/commands/shadow.js +147 -31
- package/dist/cli/commands/shadow.js.map +1 -1
- package/dist/cli/commands/skill-crud.d.ts +7 -0
- package/dist/cli/commands/skill-crud.d.ts.map +1 -1
- package/dist/cli/commands/skill-crud.js +41 -18
- package/dist/cli/commands/skill-crud.js.map +1 -1
- package/dist/cli/commands/skill-diff.d.ts.map +1 -1
- package/dist/cli/commands/skill-diff.js +29 -3
- package/dist/cli/commands/skill-diff.js.map +1 -1
- package/dist/cli/commands/skill-install.d.ts.map +1 -1
- package/dist/cli/commands/skill-install.js +5 -4
- package/dist/cli/commands/skill-install.js.map +1 -1
- package/dist/cli/commands/task.d.ts.map +1 -1
- package/dist/cli/commands/task.js +359 -49
- package/dist/cli/commands/task.js.map +1 -1
- package/dist/cli/commands/trait.d.ts.map +1 -1
- package/dist/cli/commands/trait.js +5 -27
- package/dist/cli/commands/trait.js.map +1 -1
- package/dist/cli/commands/validate.d.ts.map +1 -1
- package/dist/cli/commands/validate.js +113 -52
- package/dist/cli/commands/validate.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +69 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/output.d.ts +26 -0
- package/dist/cli/output.d.ts.map +1 -1
- package/dist/cli/output.js +108 -1
- package/dist/cli/output.js.map +1 -1
- package/dist/cli/sync-mode.d.ts +44 -0
- package/dist/cli/sync-mode.d.ts.map +1 -0
- package/dist/cli/sync-mode.js +64 -0
- package/dist/cli/sync-mode.js.map +1 -0
- package/dist/daemon/middleware/project-context.ts +25 -7
- package/dist/daemon/project-context.ts +18 -0
- package/dist/daemon/routes/agent-dispatch.ts +107 -23
- package/dist/daemon/routes/aggregation.ts +184 -0
- package/dist/daemon/routes/inbox.ts +5 -0
- package/dist/daemon/routes/items.ts +167 -0
- package/dist/daemon/routes/meta.ts +141 -1
- package/dist/daemon/routes/plans.ts +147 -0
- package/dist/daemon/routes/projects.ts +28 -6
- package/dist/daemon/routes/ref-resolution.ts +119 -0
- package/dist/daemon/routes/refs.ts +42 -0
- package/dist/daemon/routes/session-related.ts +140 -0
- package/dist/daemon/routes/sessions.ts +581 -0
- package/dist/daemon/routes/tasks.ts +257 -2
- package/dist/daemon/routes/triage.ts +40 -1
- package/dist/daemon/routes/validation.ts +1 -1
- package/dist/daemon/server.ts +165 -50
- package/dist/daemon/session-sync.ts +11 -0
- package/dist/daemon/shadow-sync.ts +11 -0
- package/dist/daemon/watcher.ts +56 -5
- package/dist/daemon/websocket/project-resolution.ts +77 -0
- package/dist/export/json.d.ts.map +1 -1
- package/dist/export/json.js +104 -1
- package/dist/export/json.js.map +1 -1
- package/dist/export/types.d.ts +52 -1
- package/dist/export/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/parser/agent-detection.d.ts +1 -1
- package/dist/parser/agent-detection.d.ts.map +1 -1
- package/dist/parser/agent-detection.js +10 -0
- package/dist/parser/agent-detection.js.map +1 -1
- package/dist/parser/alignment.d.ts.map +1 -1
- package/dist/parser/alignment.js +4 -2
- package/dist/parser/alignment.js.map +1 -1
- package/dist/parser/config.d.ts +397 -2
- package/dist/parser/config.d.ts.map +1 -1
- package/dist/parser/config.js +125 -3
- package/dist/parser/config.js.map +1 -1
- package/dist/parser/dispatch-workspaces.d.ts +18 -0
- package/dist/parser/dispatch-workspaces.d.ts.map +1 -0
- package/dist/parser/dispatch-workspaces.js +209 -0
- package/dist/parser/dispatch-workspaces.js.map +1 -0
- package/dist/parser/doctor.d.ts.map +1 -1
- package/dist/parser/doctor.js +27 -8
- package/dist/parser/doctor.js.map +1 -1
- package/dist/parser/file-lock.d.ts.map +1 -1
- package/dist/parser/file-lock.js +9 -2
- package/dist/parser/file-lock.js.map +1 -1
- package/dist/parser/index.d.ts +6 -0
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +6 -0
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/plans.d.ts.map +1 -1
- package/dist/parser/plans.js +1 -0
- package/dist/parser/plans.js.map +1 -1
- package/dist/parser/refs.d.ts +8 -1
- package/dist/parser/refs.d.ts.map +1 -1
- package/dist/parser/refs.js +27 -1
- package/dist/parser/refs.js.map +1 -1
- package/dist/parser/review-operations.d.ts +72 -0
- package/dist/parser/review-operations.d.ts.map +1 -0
- package/dist/parser/review-operations.js +185 -0
- package/dist/parser/review-operations.js.map +1 -0
- package/dist/parser/review-task-integration.d.ts +78 -0
- package/dist/parser/review-task-integration.d.ts.map +1 -0
- package/dist/parser/review-task-integration.js +173 -0
- package/dist/parser/review-task-integration.js.map +1 -0
- package/dist/parser/review-threads.d.ts +101 -0
- package/dist/parser/review-threads.d.ts.map +1 -0
- package/dist/parser/review-threads.js +222 -0
- package/dist/parser/review-threads.js.map +1 -0
- package/dist/parser/review-validation.d.ts +69 -0
- package/dist/parser/review-validation.d.ts.map +1 -0
- package/dist/parser/review-validation.js +207 -0
- package/dist/parser/review-validation.js.map +1 -0
- package/dist/parser/reviews.d.ts +58 -0
- package/dist/parser/reviews.d.ts.map +1 -0
- package/dist/parser/reviews.js +230 -0
- package/dist/parser/reviews.js.map +1 -0
- package/dist/parser/session-branch.d.ts +91 -0
- package/dist/parser/session-branch.d.ts.map +1 -0
- package/dist/parser/session-branch.js +565 -0
- package/dist/parser/session-branch.js.map +1 -0
- package/dist/parser/session-sync-scheduler.d.ts +53 -0
- package/dist/parser/session-sync-scheduler.d.ts.map +1 -0
- package/dist/parser/session-sync-scheduler.js +100 -0
- package/dist/parser/session-sync-scheduler.js.map +1 -0
- package/dist/parser/setup-status.d.ts +7 -1
- package/dist/parser/setup-status.d.ts.map +1 -1
- package/dist/parser/setup-status.js +104 -39
- package/dist/parser/setup-status.js.map +1 -1
- package/dist/parser/shadow-sync-scheduler.d.ts +71 -0
- package/dist/parser/shadow-sync-scheduler.d.ts.map +1 -0
- package/dist/parser/shadow-sync-scheduler.js +139 -0
- package/dist/parser/shadow-sync-scheduler.js.map +1 -0
- package/dist/parser/shadow.d.ts +121 -14
- package/dist/parser/shadow.d.ts.map +1 -1
- package/dist/parser/shadow.js +752 -27
- package/dist/parser/shadow.js.map +1 -1
- package/dist/parser/skill-render.d.ts +24 -0
- package/dist/parser/skill-render.d.ts.map +1 -1
- package/dist/parser/skill-render.js +98 -26
- package/dist/parser/skill-render.js.map +1 -1
- package/dist/parser/validate.d.ts +43 -3
- package/dist/parser/validate.d.ts.map +1 -1
- package/dist/parser/validate.js +204 -30
- package/dist/parser/validate.js.map +1 -1
- package/dist/parser/yaml.d.ts +47 -11
- package/dist/parser/yaml.d.ts.map +1 -1
- package/dist/parser/yaml.js +329 -149
- package/dist/parser/yaml.js.map +1 -1
- package/dist/review/checks.d.ts +97 -0
- package/dist/review/checks.d.ts.map +1 -0
- package/dist/review/checks.js +175 -0
- package/dist/review/checks.js.map +1 -0
- package/dist/review/index.d.ts +3 -0
- package/dist/review/index.d.ts.map +1 -0
- package/dist/review/index.js +3 -0
- package/dist/review/index.js.map +1 -0
- package/dist/review/subject-bindings.d.ts +83 -0
- package/dist/review/subject-bindings.d.ts.map +1 -0
- package/dist/review/subject-bindings.js +175 -0
- package/dist/review/subject-bindings.js.map +1 -0
- package/dist/schema/common.d.ts +26 -0
- package/dist/schema/common.d.ts.map +1 -1
- package/dist/schema/common.js +13 -0
- package/dist/schema/common.js.map +1 -1
- package/dist/schema/dispatch-workspace.d.ts +2643 -0
- package/dist/schema/dispatch-workspace.d.ts.map +1 -0
- package/dist/schema/dispatch-workspace.js +187 -0
- package/dist/schema/dispatch-workspace.js.map +1 -0
- package/dist/schema/inbox.d.ts +8 -8
- package/dist/schema/index.d.ts +2 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +2 -0
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/meta.d.ts +663 -116
- package/dist/schema/meta.d.ts.map +1 -1
- package/dist/schema/meta.js +28 -0
- package/dist/schema/meta.js.map +1 -1
- package/dist/schema/plan.d.ts +30 -19
- package/dist/schema/plan.d.ts.map +1 -1
- package/dist/schema/plan.js +3 -1
- package/dist/schema/plan.js.map +1 -1
- package/dist/schema/review-records.d.ts +2676 -0
- package/dist/schema/review-records.d.ts.map +1 -0
- package/dist/schema/review-records.js +232 -0
- package/dist/schema/review-records.js.map +1 -0
- package/dist/schema/spec.d.ts +32 -14
- package/dist/schema/spec.d.ts.map +1 -1
- package/dist/schema/spec.js +5 -0
- package/dist/schema/spec.js.map +1 -1
- package/dist/schema/task.d.ts +187 -29
- package/dist/schema/task.d.ts.map +1 -1
- package/dist/schema/task.js +12 -2
- package/dist/schema/task.js.map +1 -1
- package/dist/schema/triage.d.ts +22 -22
- package/dist/sessions/cache.d.ts +119 -0
- package/dist/sessions/cache.d.ts.map +1 -0
- package/dist/sessions/cache.js +284 -0
- package/dist/sessions/cache.js.map +1 -0
- package/dist/sessions/index.d.ts +1 -0
- package/dist/sessions/index.d.ts.map +1 -1
- package/dist/sessions/index.js +2 -0
- package/dist/sessions/index.js.map +1 -1
- package/dist/sessions/legacy.d.ts +77 -0
- package/dist/sessions/legacy.d.ts.map +1 -0
- package/dist/sessions/legacy.js +146 -0
- package/dist/sessions/legacy.js.map +1 -0
- package/dist/sessions/store.d.ts +115 -71
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +357 -182
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/types.d.ts +44 -16
- package/dist/sessions/types.d.ts.map +1 -1
- package/dist/sessions/types.js +11 -2
- package/dist/sessions/types.js.map +1 -1
- package/dist/strings/errors.d.ts +32 -0
- package/dist/strings/errors.d.ts.map +1 -1
- package/dist/strings/errors.js +17 -0
- package/dist/strings/errors.js.map +1 -1
- package/dist/strings/labels.d.ts +1 -0
- package/dist/strings/labels.d.ts.map +1 -1
- package/dist/strings/labels.js +1 -0
- package/dist/strings/labels.js.map +1 -1
- package/dist/utils/activity.d.ts +101 -0
- package/dist/utils/activity.d.ts.map +1 -0
- package/dist/utils/activity.js +408 -0
- package/dist/utils/activity.js.map +1 -0
- package/dist/utils/git.d.ts +31 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +87 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/web-ui/_app/immutable/assets/0.tmlwn-Ih.css +1 -0
- package/dist/web-ui/_app/immutable/assets/9.BwwJybWx.css +1 -0
- package/dist/web-ui/_app/immutable/chunks/2KqE8gtn.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/70-t_QvE.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/AiWQj974.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/B25nWFyA.js +5 -0
- package/dist/web-ui/_app/immutable/chunks/B2bcA_Q_.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/B5e5HYyB.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/B7-5z6eA.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/B7bGmhK0.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/B8tYZKAE.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BFGAyJjD.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BG0850zf.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BG8eSzAd.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BIMxXS8I.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BSzL1fpU.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BYtjHfeq.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/{D1ArdqNb.js → Bp5pFYXL.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/BsJFsuAT.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BvpNHcD6.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BypqA25-.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/C0w6WDm5.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/C5_PAZ0y.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CDRO15Iv.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CF1CoqD5.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CS2sa4_m.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CWUQwB9H.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CY5FDdSU.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/C_7MTDoj.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CaAJD3dl.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/{i-XnOIX0.js → ChB5iyEL.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/ChQD-6N8.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/{BCkp8Hs8.js → CqbsoCwA.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/DCeJW50p.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DJtZNgcs.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DKIeaprD.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DLd2uVIA.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DW_subyT.js +2 -0
- package/dist/web-ui/_app/immutable/chunks/DbU6lVn0.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/Dc7ZCC5m.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/Dd5umPsk.js +2 -0
- package/dist/web-ui/_app/immutable/chunks/Dg_zDpDS.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/Dgqu8Yuc.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DmxsPZTB.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DphTaFUB.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DqK4iHp0.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DqT6OH_u.js +2 -0
- package/dist/web-ui/_app/immutable/chunks/Ds9I9wQb.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/Du5ng3u4.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DxJw79Wi.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/GFTX8GgV.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/HNjs76Zz.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/HVMjDi4_.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/P0A_fJvS.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/T3vGWjIL.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/VTmrX9Qu.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/Xvwhx_F1.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/Yyz1XMQA.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/dh5HeqUr.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/fZMteyca.js +62 -0
- package/dist/web-ui/_app/immutable/chunks/{D28BF5MJ.js → gPrj-hqC.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/htcWMiYN.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/oTsvd9y4.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/qJfLUwU4.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/xCtiO_JE.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/y4GeEH6k.js +1 -0
- package/dist/web-ui/_app/immutable/entry/app.C4h_eOn6.js +2 -0
- package/dist/web-ui/_app/immutable/entry/start.CQFTf9ep.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/0.Dh1xO970.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/1.l75D3Opx.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/10.DBidBPc-.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/11.Ab0gUKWe.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/12.CMsnoxfs.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/13.D8YKuknB.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/14.DZ0aan7y.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/15.CUIKreDL.js +2 -0
- package/dist/web-ui/_app/immutable/nodes/16.BWc8--BO.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/2.CDUonbuh.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/3.Ctg3M00i.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/4.Ci-JDwbA.js +2 -0
- package/dist/web-ui/_app/immutable/nodes/5.CTyEDAq0.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/6.BTZZqsAb.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/7.BI52g_Jo.js +137 -0
- package/dist/web-ui/_app/immutable/nodes/8.3hZPaB9x.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/9.DS49kvwl.js +29 -0
- package/dist/web-ui/_app/version.json +1 -1
- package/dist/web-ui/favicon-192.png +0 -0
- package/dist/web-ui/favicon-32.png +0 -0
- package/dist/web-ui/favicon.ico +0 -0
- package/dist/web-ui/index.html +14 -11
- package/package.json +14 -7
- package/plugin/.claude-plugin/marketplace.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/plugins/kspec/skills/merge/SKILL.md +127 -0
- package/plugin/plugins/kspec/skills/plan/SKILL.md +55 -26
- package/plugin/plugins/kspec/skills/review/SKILL.md +350 -133
- package/plugin/plugins/kspec/skills/task-work/SKILL.md +96 -106
- package/templates/agents-sections/04-pr-workflow.md +15 -12
- package/templates/agents-sections/06-ralph-loop.md +15 -10
- package/templates/skills/manifest.yaml +25 -7
- package/templates/skills/merge/SKILL.md +120 -0
- package/templates/skills/plan/SKILL.md +55 -26
- package/templates/skills/review/SKILL.md +346 -130
- package/templates/skills/task-work/SKILL.md +93 -103
- package/dist/web-ui/_app/immutable/assets/0.BxCxvrZR.css +0 -1
- package/dist/web-ui/_app/immutable/chunks/B-CZR0q8.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/B1IR5Su5.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/B_Cvvtc4.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/BtFaGGII.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/Bu8JVsCH.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/C87u-CNA.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/CrFkBTYp.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/D6RtLpzL.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/D7FHSgx2.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/DBXrsxZQ.js +0 -2
- package/dist/web-ui/_app/immutable/chunks/Da_hHMuA.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/Do6LchSF.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/DoNPtcAw.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/DtUbXRZz.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/DyFPRlLl.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/DzAP8lRM.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/DzVXElzN.js +0 -2
- package/dist/web-ui/_app/immutable/chunks/aoPBFken.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/laxtrUO3.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/q1nIWgqB.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/sTLbk5Nm.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/vwKgQu5P.js +0 -5
- package/dist/web-ui/_app/immutable/entry/app.BCwMcqnT.js +0 -2
- package/dist/web-ui/_app/immutable/entry/start.wKCQH-tt.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/0.CjGVMG74.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/1.B6_AIPan.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/2.q4oCS7Ws.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/3.rTKZf9o2.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/4.DVIDRu1d.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/5.8PtPXIOd.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/6.ZZrTemy_.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/7.IP-gxCxi.js +0 -1
package/dist/parser/shadow.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { execFile, spawn, spawnSync } from "node:child_process";
|
|
12
12
|
import * as fs from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
13
14
|
import * as path from "node:path";
|
|
14
15
|
import { promisify } from "node:util";
|
|
15
16
|
import { fileURLToPath } from "node:url";
|
|
@@ -38,6 +39,28 @@ async function runGitAsync(cwd, args, env) {
|
|
|
38
39
|
});
|
|
39
40
|
return { stdout: stdout.toString(), stderr: stderr.toString() };
|
|
40
41
|
}
|
|
42
|
+
async function stashBrokenWorktreeDir(worktreeDir) {
|
|
43
|
+
const stat = await fs.stat(worktreeDir).catch(() => null);
|
|
44
|
+
if (!stat) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const backupDir = `${worktreeDir}.repair-backup-${Date.now()}`;
|
|
48
|
+
await fs.rename(worktreeDir, backupDir);
|
|
49
|
+
return backupDir;
|
|
50
|
+
}
|
|
51
|
+
async function restoreStashedWorktreeDir(backupDir, worktreeDir) {
|
|
52
|
+
if (!backupDir) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
await fs.rm(worktreeDir, { recursive: true, force: true });
|
|
56
|
+
await fs.rename(backupDir, worktreeDir);
|
|
57
|
+
}
|
|
58
|
+
async function discardStashedWorktreeDir(backupDir) {
|
|
59
|
+
if (!backupDir) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
await fs.rm(backupDir, { recursive: true, force: true });
|
|
63
|
+
}
|
|
41
64
|
function runGitSync(cwd, args) {
|
|
42
65
|
const result = spawnSync("git", args, {
|
|
43
66
|
cwd,
|
|
@@ -49,6 +72,129 @@ function runGitSync(cwd, args) {
|
|
|
49
72
|
stdout: (result.stdout || "").toString(),
|
|
50
73
|
};
|
|
51
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Parse git version from `git --version` output.
|
|
77
|
+
* Returns [major, minor, patch] or null if unparseable.
|
|
78
|
+
*/
|
|
79
|
+
export function getGitVersion(cwd) {
|
|
80
|
+
const result = spawnSync("git", ["--version"], {
|
|
81
|
+
cwd,
|
|
82
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
83
|
+
encoding: "utf-8",
|
|
84
|
+
});
|
|
85
|
+
if (result.error || result.status !== 0)
|
|
86
|
+
return null;
|
|
87
|
+
const match = (result.stdout || "").match(/(\d+)\.(\d+)\.(\d+)/);
|
|
88
|
+
if (!match)
|
|
89
|
+
return null;
|
|
90
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Check if the installed git supports `git worktree add --orphan` (requires >= 2.42.0).
|
|
94
|
+
*/
|
|
95
|
+
export function gitSupportsOrphanWorktree(cwd) {
|
|
96
|
+
const version = getGitVersion(cwd);
|
|
97
|
+
if (!version)
|
|
98
|
+
return false;
|
|
99
|
+
const [major, minor] = version;
|
|
100
|
+
return major > 2 || (major === 2 && minor >= 42);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Fallback for creating an orphan branch when git < 2.42.
|
|
104
|
+
*
|
|
105
|
+
* Strategy:
|
|
106
|
+
* 1. Create a temp bare repo in the OS temp directory
|
|
107
|
+
* 2. Create an orphan branch with an empty commit there
|
|
108
|
+
* 3. Push that branch to the project repo (via file:// protocol)
|
|
109
|
+
* 4. Clean up the temp repo
|
|
110
|
+
* 5. Attach using standard `git worktree add <dir> <branch>`
|
|
111
|
+
*
|
|
112
|
+
* This approach NEVER modifies the project's working tree.
|
|
113
|
+
*
|
|
114
|
+
* AC: @config-shadow ac-10
|
|
115
|
+
*/
|
|
116
|
+
export async function createOrphanBranchFallback(projectRoot, branchName, directoryName) {
|
|
117
|
+
const tmpDir = await fs.mkdtemp(path.join(tmpdir(), "kspec-orphan-"));
|
|
118
|
+
try {
|
|
119
|
+
// 1. Init a bare repo in the temp dir
|
|
120
|
+
await runGitAsync(tmpDir, ["init", "--bare"]);
|
|
121
|
+
// 2. Create the orphan branch using a temporary non-bare clone.
|
|
122
|
+
// We need a working tree to make a commit, so clone the bare repo.
|
|
123
|
+
const workDir = await fs.mkdtemp(path.join(tmpdir(), "kspec-orphan-work-"));
|
|
124
|
+
try {
|
|
125
|
+
await runGitAsync(workDir, ["clone", tmpDir, "."]);
|
|
126
|
+
await runGitAsync(workDir, [
|
|
127
|
+
"config",
|
|
128
|
+
"user.email",
|
|
129
|
+
"kspec@localhost",
|
|
130
|
+
]);
|
|
131
|
+
await runGitAsync(workDir, [
|
|
132
|
+
"config",
|
|
133
|
+
"user.name",
|
|
134
|
+
"kspec",
|
|
135
|
+
]);
|
|
136
|
+
// Create an orphan branch (checkout --orphan works on all git versions)
|
|
137
|
+
await runGitAsync(workDir, [
|
|
138
|
+
"checkout",
|
|
139
|
+
"--orphan",
|
|
140
|
+
branchName,
|
|
141
|
+
]);
|
|
142
|
+
// Remove any files that might have been staged
|
|
143
|
+
try {
|
|
144
|
+
await runGitAsync(workDir, ["rm", "-rf", "."]);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// May fail if nothing to remove (empty repo) - that's fine
|
|
148
|
+
}
|
|
149
|
+
// Create an empty initial commit
|
|
150
|
+
await runGitAsync(workDir, [
|
|
151
|
+
"commit",
|
|
152
|
+
"--allow-empty",
|
|
153
|
+
"-m",
|
|
154
|
+
`Initialize ${branchName}`,
|
|
155
|
+
]);
|
|
156
|
+
// Push the orphan branch back to the bare repo
|
|
157
|
+
await runGitAsync(workDir, [
|
|
158
|
+
"push",
|
|
159
|
+
"origin",
|
|
160
|
+
branchName,
|
|
161
|
+
]);
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
await fs.rm(workDir, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
// 3. Push from the temp bare repo to the project repo
|
|
167
|
+
// Use file:// protocol to ensure git treats it as a proper remote
|
|
168
|
+
await runGitAsync(tmpDir, [
|
|
169
|
+
"push",
|
|
170
|
+
`file://${path.resolve(projectRoot)}`,
|
|
171
|
+
branchName,
|
|
172
|
+
]);
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
// 4. Clean up the temp bare repo
|
|
176
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
177
|
+
}
|
|
178
|
+
// 5. Attach worktree using standard git worktree add (no --orphan flag)
|
|
179
|
+
await runGitAsync(projectRoot, [
|
|
180
|
+
"worktree",
|
|
181
|
+
"add",
|
|
182
|
+
directoryName,
|
|
183
|
+
branchName,
|
|
184
|
+
]);
|
|
185
|
+
// 6. Remove all tracked files from the worktree since the fallback
|
|
186
|
+
// created an empty commit but `git worktree add` may still populate
|
|
187
|
+
// the index from the branch. Clear anything that appeared.
|
|
188
|
+
try {
|
|
189
|
+
const { stdout } = await runGitAsync(path.join(projectRoot, directoryName), ["ls-files"]);
|
|
190
|
+
if (stdout.trim()) {
|
|
191
|
+
await runGitAsync(path.join(projectRoot, directoryName), ["rm", "-rf", "."]);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// Nothing to remove — expected for an empty commit
|
|
196
|
+
}
|
|
197
|
+
}
|
|
52
198
|
// Import getVerboseMode for checking CLI --debug-shadow flag
|
|
53
199
|
// We use a getter function to avoid issues with circular dependencies
|
|
54
200
|
let getVerboseModeFunc = null;
|
|
@@ -76,6 +222,11 @@ export const SHADOW_BRANCH_NAME = "kspec-meta";
|
|
|
76
222
|
* Default shadow worktree directory
|
|
77
223
|
*/
|
|
78
224
|
export const SHADOW_WORKTREE_DIR = ".kspec";
|
|
225
|
+
/**
|
|
226
|
+
* Sessions storage directory name (at project root, separate from shadow worktree).
|
|
227
|
+
* AC: @session-storage-modes ac-sessions-dir
|
|
228
|
+
*/
|
|
229
|
+
export const SESSIONS_WORKTREE_DIR = ".kspec-sessions";
|
|
79
230
|
/**
|
|
80
231
|
* Get effective branch name from options or default.
|
|
81
232
|
* AC: @config-shadow ac-7 — backward compat when called without config
|
|
@@ -90,6 +241,54 @@ function getBranchName(options) {
|
|
|
90
241
|
function getDirectoryName(options) {
|
|
91
242
|
return options?.directory ?? SHADOW_WORKTREE_DIR;
|
|
92
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Get effective remote name from options or default.
|
|
246
|
+
* AC: @config-shadow ac-3 ac-7 — resolves configured remote for fetch/push/pull.
|
|
247
|
+
* Named remotes use the name directly; path/URL remotes use the auto-created "kspec-specs".
|
|
248
|
+
*/
|
|
249
|
+
export function getRemoteName(options) {
|
|
250
|
+
if (!options?.remote)
|
|
251
|
+
return "origin";
|
|
252
|
+
const remoteType = options.remoteType ?? "named";
|
|
253
|
+
if (remoteType === "path" || remoteType === "url")
|
|
254
|
+
return "kspec-specs";
|
|
255
|
+
return options.remote;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Resolve the remote target used for direct ls-remote/fetch queries.
|
|
259
|
+
* Named remotes use the configured name, while path remotes expand "~".
|
|
260
|
+
*/
|
|
261
|
+
function getRemoteQueryTarget(options) {
|
|
262
|
+
if (!options?.remote)
|
|
263
|
+
return "origin";
|
|
264
|
+
const remoteType = options.remoteType ?? "named";
|
|
265
|
+
if (remoteType !== "path") {
|
|
266
|
+
return options.remote;
|
|
267
|
+
}
|
|
268
|
+
if (options.remote.startsWith("~")) {
|
|
269
|
+
return options.remote.replace(/^~/, process.env.HOME || process.env.USERPROFILE || "~");
|
|
270
|
+
}
|
|
271
|
+
return options.remote;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Check whether the shadow branch exists on the configured/default remote.
|
|
275
|
+
*/
|
|
276
|
+
export async function remoteShadowBranchExists(projectRoot, options) {
|
|
277
|
+
const branchName = getBranchName(options);
|
|
278
|
+
const remoteType = options?.remoteType ?? "named";
|
|
279
|
+
const remoteName = getRemoteName(options);
|
|
280
|
+
const remoteQueryTarget = getRemoteQueryTarget(options);
|
|
281
|
+
if (options?.remote) {
|
|
282
|
+
if (remoteType === "named" && !(await hasRemote(projectRoot, remoteName))) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
return remoteBranchExists(projectRoot, branchName, remoteQueryTarget);
|
|
286
|
+
}
|
|
287
|
+
if (!(await hasRemote(projectRoot, remoteName))) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
return remoteBranchExists(projectRoot, branchName, remoteQueryTarget);
|
|
291
|
+
}
|
|
93
292
|
/**
|
|
94
293
|
* Check if debug mode is enabled.
|
|
95
294
|
* Debug mode can be enabled via:
|
|
@@ -127,6 +326,45 @@ export function getGitRoot(dir) {
|
|
|
127
326
|
}
|
|
128
327
|
return result.stdout.trim();
|
|
129
328
|
}
|
|
329
|
+
function isSubmoduleCommonDir(commonDir) {
|
|
330
|
+
const segments = path.normalize(commonDir).split(path.sep).filter(Boolean);
|
|
331
|
+
const gitIndex = segments.lastIndexOf(".git");
|
|
332
|
+
return gitIndex >= 0 && segments[gitIndex + 1] === "modules";
|
|
333
|
+
}
|
|
334
|
+
export function resolveProjectRoots(dir) {
|
|
335
|
+
const result = runGitSync(dir, [
|
|
336
|
+
"rev-parse",
|
|
337
|
+
"--show-toplevel",
|
|
338
|
+
"--git-common-dir",
|
|
339
|
+
]);
|
|
340
|
+
if (!result.ok) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
const [rawTopLevel, rawCommonDir] = result.stdout
|
|
344
|
+
.split(/\r?\n/)
|
|
345
|
+
.map((line) => line.trim())
|
|
346
|
+
.filter(Boolean);
|
|
347
|
+
if (!rawTopLevel || !rawCommonDir) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
const worktreeRoot = path.resolve(rawTopLevel);
|
|
351
|
+
const commonDir = path.isAbsolute(rawCommonDir)
|
|
352
|
+
? path.resolve(rawCommonDir)
|
|
353
|
+
: path.resolve(worktreeRoot, rawCommonDir);
|
|
354
|
+
if (isSubmoduleCommonDir(commonDir) ||
|
|
355
|
+
commonDir === path.join(worktreeRoot, ".git")) {
|
|
356
|
+
return {
|
|
357
|
+
mainRoot: worktreeRoot,
|
|
358
|
+
worktreeRoot,
|
|
359
|
+
isWorktree: false,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
mainRoot: path.dirname(commonDir),
|
|
364
|
+
worktreeRoot,
|
|
365
|
+
isWorktree: true,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
130
368
|
/**
|
|
131
369
|
* Check if a branch exists
|
|
132
370
|
*/
|
|
@@ -188,20 +426,18 @@ export async function detectRunningFromShadowWorktree(cwd, configuredDirectory)
|
|
|
188
426
|
if (!match) {
|
|
189
427
|
return null;
|
|
190
428
|
}
|
|
191
|
-
const gitdir = match[1];
|
|
429
|
+
const gitdir = path.resolve(cwd, match[1]);
|
|
192
430
|
// Check if this is a worktree (pattern: <project>/.git/worktrees/<name>)
|
|
193
431
|
if (gitdir.includes(".git/worktrees/")) {
|
|
194
|
-
const worktreesMatch = gitdir.match(/^(
|
|
432
|
+
const worktreesMatch = gitdir.match(/^(.*?)[/\\]\.git[/\\]worktrees[/\\]/);
|
|
195
433
|
if (worktreesMatch) {
|
|
196
434
|
const mainProjectRoot = worktreesMatch[1];
|
|
197
435
|
const cwdBase = path.basename(cwd);
|
|
198
|
-
const worktreeName = path.basename(gitdir);
|
|
199
436
|
// AC: ac-8 — check multiple patterns for shadow worktree detection
|
|
200
437
|
const directoryToCheck = configuredDirectory || SHADOW_WORKTREE_DIR;
|
|
201
|
-
//
|
|
438
|
+
// Exact shadow directory names are always considered shadow worktrees.
|
|
202
439
|
if (cwdBase === SHADOW_WORKTREE_DIR ||
|
|
203
|
-
cwdBase === directoryToCheck
|
|
204
|
-
worktreeName.includes("kspec")) {
|
|
440
|
+
cwdBase === directoryToCheck) {
|
|
205
441
|
return mainProjectRoot;
|
|
206
442
|
}
|
|
207
443
|
// Additional check: see if this directory has a kspec manifest
|
|
@@ -239,8 +475,8 @@ export async function detectRunningFromShadowWorktree(cwd, configuredDirectory)
|
|
|
239
475
|
* @param startDir Directory to start detection from
|
|
240
476
|
* @param options Optional shadow configuration (branch name, directory)
|
|
241
477
|
*/
|
|
242
|
-
export async function detectShadow(startDir, options) {
|
|
243
|
-
const gitRoot = getGitRoot(startDir);
|
|
478
|
+
export async function detectShadow(startDir, options, mainRoot) {
|
|
479
|
+
const gitRoot = mainRoot ?? getGitRoot(startDir);
|
|
244
480
|
if (!gitRoot) {
|
|
245
481
|
return null;
|
|
246
482
|
}
|
|
@@ -493,7 +729,12 @@ export function generateCommitMessage(operation, ref, detail) {
|
|
|
493
729
|
parts.push(`Note on @${ref}`);
|
|
494
730
|
break;
|
|
495
731
|
case "task-add":
|
|
496
|
-
|
|
732
|
+
if (ref && detail) {
|
|
733
|
+
parts.push(`Add task @${ref}: ${detail}`);
|
|
734
|
+
}
|
|
735
|
+
else {
|
|
736
|
+
parts.push(`Add task: ${detail || ref}`);
|
|
737
|
+
}
|
|
497
738
|
break;
|
|
498
739
|
case "inbox-add":
|
|
499
740
|
parts.push(`Inbox: ${detail?.slice(0, 50)}${(detail?.length || 0) > 50 ? "..." : ""}`);
|
|
@@ -573,6 +814,7 @@ export async function commitIfShadow(shadowConfig, operation, ref, detail, verbo
|
|
|
573
814
|
const message = generateCommitMessage(operation, ref, detail);
|
|
574
815
|
const committed = await shadowAutoCommit(shadowConfig.worktreeDir, message, verbose);
|
|
575
816
|
// AC: @shadow-sync ac-1 - Fire-and-forget push after each commit
|
|
817
|
+
// AC: @shadow-write-sync ac-write-always-syncs — writes always sync via push path
|
|
576
818
|
if (committed) {
|
|
577
819
|
shadowPushAsync(shadowConfig.worktreeDir, verbose);
|
|
578
820
|
}
|
|
@@ -650,6 +892,30 @@ export async function fetchRemote(projectRoot, remoteName = "origin") {
|
|
|
650
892
|
return false;
|
|
651
893
|
}
|
|
652
894
|
}
|
|
895
|
+
/**
|
|
896
|
+
* Check if the local shadow branch has unpushed commits ahead of upstream.
|
|
897
|
+
* Returns true if local is ahead, false otherwise (including when upstream
|
|
898
|
+
* ref doesn't exist or an error occurs).
|
|
899
|
+
*
|
|
900
|
+
* @param worktreeDir Path to shadow worktree
|
|
901
|
+
*/
|
|
902
|
+
export async function isAheadOfUpstream(worktreeDir) {
|
|
903
|
+
try {
|
|
904
|
+
const { stdout } = await runGitAsync(worktreeDir, [
|
|
905
|
+
"rev-list",
|
|
906
|
+
"--left-right",
|
|
907
|
+
"--count",
|
|
908
|
+
"HEAD...@{u}",
|
|
909
|
+
]);
|
|
910
|
+
const [aheadStr] = stdout.trim().split("\t");
|
|
911
|
+
const ahead = parseInt(aheadStr, 10);
|
|
912
|
+
return ahead > 0;
|
|
913
|
+
}
|
|
914
|
+
catch {
|
|
915
|
+
// No upstream ref or other error — not ahead
|
|
916
|
+
return false;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
653
919
|
/**
|
|
654
920
|
* Push shadow branch to remote with tracking.
|
|
655
921
|
* Returns true if push succeeded, false otherwise.
|
|
@@ -817,9 +1083,117 @@ function noteShadowPushSuccess(worktreeDir) {
|
|
|
817
1083
|
shadowPushFailureCounts.delete(worktreeDir);
|
|
818
1084
|
}
|
|
819
1085
|
/**
|
|
820
|
-
*
|
|
1086
|
+
* Pull-rebase from remote before pushing, using the kspec merge driver for
|
|
1087
|
+
* YAML conflict resolution.
|
|
1088
|
+
*
|
|
1089
|
+
* AC: @config-shadow ac-11 — pull-rebase before push prevents divergence
|
|
1090
|
+
*
|
|
1091
|
+
* @returns true if pull succeeded (or was unnecessary), false on conflict
|
|
1092
|
+
*/
|
|
1093
|
+
async function pullRebaseBeforePush(worktreeDir, branchName, debug, options) {
|
|
1094
|
+
try {
|
|
1095
|
+
// Fetch latest remote state for the shadow branch specifically.
|
|
1096
|
+
// Using the worktree dir for fetch ensures we use the branch's tracking config.
|
|
1097
|
+
try {
|
|
1098
|
+
await runGitAsync(worktreeDir, ["fetch"]);
|
|
1099
|
+
}
|
|
1100
|
+
catch {
|
|
1101
|
+
if (debug) {
|
|
1102
|
+
console.error("[DEBUG] Shadow pull-rebase: fetch failed, skipping pull");
|
|
1103
|
+
}
|
|
1104
|
+
// Fetch failure is non-fatal — push may still succeed if already up to date
|
|
1105
|
+
return true;
|
|
1106
|
+
}
|
|
1107
|
+
// AC: @config-shadow ac-3 — resolve the configured remote name from git config
|
|
1108
|
+
// instead of hardcoding "origin", so custom shadow.remote setups work correctly
|
|
1109
|
+
const projectRoot = path.dirname(worktreeDir);
|
|
1110
|
+
let remoteName = "origin";
|
|
1111
|
+
try {
|
|
1112
|
+
const { stdout } = await runGitAsync(worktreeDir, [
|
|
1113
|
+
"config",
|
|
1114
|
+
`branch.${branchName}.remote`,
|
|
1115
|
+
]);
|
|
1116
|
+
const configured = stdout.trim();
|
|
1117
|
+
if (configured) {
|
|
1118
|
+
remoteName = configured;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
catch {
|
|
1122
|
+
// Fall back to origin if config lookup fails
|
|
1123
|
+
}
|
|
1124
|
+
const remoteHasBranch = await remoteBranchExists(projectRoot, branchName, remoteName);
|
|
1125
|
+
if (!remoteHasBranch) {
|
|
1126
|
+
if (debug) {
|
|
1127
|
+
console.error("[DEBUG] Shadow pull-rebase: no remote branch yet, skipping pull");
|
|
1128
|
+
}
|
|
1129
|
+
return true;
|
|
1130
|
+
}
|
|
1131
|
+
// Check if there are any upstream changes to integrate.
|
|
1132
|
+
// If local is already at or ahead of remote, skip the pull.
|
|
1133
|
+
try {
|
|
1134
|
+
const { stdout } = await runGitAsync(worktreeDir, [
|
|
1135
|
+
"rev-list",
|
|
1136
|
+
"--count",
|
|
1137
|
+
`${branchName}..@{upstream}`,
|
|
1138
|
+
]);
|
|
1139
|
+
const behindCount = parseInt(stdout.trim(), 10);
|
|
1140
|
+
if (behindCount === 0) {
|
|
1141
|
+
if (debug) {
|
|
1142
|
+
console.error("[DEBUG] Shadow pull-rebase: already up to date with remote");
|
|
1143
|
+
}
|
|
1144
|
+
return true;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
catch {
|
|
1148
|
+
// rev-list may fail if upstream isn't set — proceed with pull attempt
|
|
1149
|
+
}
|
|
1150
|
+
// Try fast-forward first (cleanest, no rebase needed)
|
|
1151
|
+
try {
|
|
1152
|
+
await runGitAsync(worktreeDir, ["pull", "--ff-only"]);
|
|
1153
|
+
if (debug) {
|
|
1154
|
+
console.error("[DEBUG] Shadow pull-rebase: fast-forward succeeded");
|
|
1155
|
+
}
|
|
1156
|
+
return true;
|
|
1157
|
+
}
|
|
1158
|
+
catch {
|
|
1159
|
+
// FF failed, need rebase
|
|
1160
|
+
}
|
|
1161
|
+
// Fall back to rebase — the kspec merge driver handles YAML conflicts
|
|
1162
|
+
try {
|
|
1163
|
+
await runGitAsync(worktreeDir, ["pull", "--rebase"]);
|
|
1164
|
+
if (debug) {
|
|
1165
|
+
console.error("[DEBUG] Shadow pull-rebase: rebase succeeded");
|
|
1166
|
+
}
|
|
1167
|
+
return true;
|
|
1168
|
+
}
|
|
1169
|
+
catch {
|
|
1170
|
+
// Rebase failed — abort and report
|
|
1171
|
+
}
|
|
1172
|
+
// Abort the failed rebase so local state is clean
|
|
1173
|
+
try {
|
|
1174
|
+
await runGitAsync(worktreeDir, ["rebase", "--abort"]);
|
|
1175
|
+
}
|
|
1176
|
+
catch {
|
|
1177
|
+
// May not be in rebase state
|
|
1178
|
+
}
|
|
1179
|
+
if (debug) {
|
|
1180
|
+
console.error("[DEBUG] Shadow pull-rebase: conflict detected, push skipped");
|
|
1181
|
+
}
|
|
1182
|
+
return false;
|
|
1183
|
+
}
|
|
1184
|
+
catch (err) {
|
|
1185
|
+
if (debug) {
|
|
1186
|
+
console.error("[DEBUG] Shadow pull-rebase error:", err);
|
|
1187
|
+
}
|
|
1188
|
+
// Pull failure is non-fatal — still attempt push
|
|
1189
|
+
return true;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Fire-and-forget push to remote with pull-rebase-before-push.
|
|
821
1194
|
* AC-1: Called after each auto-commit when tracking is configured.
|
|
822
1195
|
* AC-8: Automatically sets up tracking if main branch has remote.
|
|
1196
|
+
* AC: @config-shadow ac-11 — pull-rebase before push for bidirectional sync.
|
|
823
1197
|
* Push failures are surfaced as warnings, but local commits still succeed.
|
|
824
1198
|
*
|
|
825
1199
|
* AC: @config-shadow ac-7 — backward compat when called without config
|
|
@@ -847,6 +1221,14 @@ export async function shadowPushAsync(worktreeDir, verbose, options) {
|
|
|
847
1221
|
}
|
|
848
1222
|
return; // AC: @shadow-sync ac-4 - silently skip if no tracking
|
|
849
1223
|
}
|
|
1224
|
+
// AC: @config-shadow ac-11 — pull-rebase before pushing to integrate remote changes
|
|
1225
|
+
// AC: @shadow-write-sync ac-write-always-syncs — writes always perform full sync
|
|
1226
|
+
const branchName = getBranchName(options);
|
|
1227
|
+
const pullOk = await pullRebaseBeforePush(worktreeDir, branchName, debug, options);
|
|
1228
|
+
if (!pullOk) {
|
|
1229
|
+
noteShadowPushFailure(worktreeDir, "Pull-rebase failed due to conflicts. Run `kspec shadow resolve` to fix.");
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
850
1232
|
try {
|
|
851
1233
|
if (debug) {
|
|
852
1234
|
console.error(`[DEBUG] Shadow push: git push (cwd: ${worktreeDir})`);
|
|
@@ -915,7 +1297,22 @@ export async function shadowPushAsync(worktreeDir, verbose, options) {
|
|
|
915
1297
|
* @param worktreeDir Path to shadow worktree
|
|
916
1298
|
* @param options Optional shadow configuration
|
|
917
1299
|
*/
|
|
918
|
-
|
|
1300
|
+
// In-flight dedup: if a pull is already running for this worktree, piggyback
|
|
1301
|
+
// on its result instead of starting a concurrent stash/pull/pop sequence.
|
|
1302
|
+
const pullInflight = new Map();
|
|
1303
|
+
export function shadowPull(worktreeDir, options) {
|
|
1304
|
+
const key = path.resolve(worktreeDir);
|
|
1305
|
+
const existing = pullInflight.get(key);
|
|
1306
|
+
if (existing) {
|
|
1307
|
+
return existing;
|
|
1308
|
+
}
|
|
1309
|
+
const promise = shadowPullImpl(worktreeDir, options).finally(() => {
|
|
1310
|
+
pullInflight.delete(key);
|
|
1311
|
+
});
|
|
1312
|
+
pullInflight.set(key, promise);
|
|
1313
|
+
return promise;
|
|
1314
|
+
}
|
|
1315
|
+
async function shadowPullImpl(worktreeDir, options) {
|
|
919
1316
|
const branchName = getBranchName(options);
|
|
920
1317
|
const result = {
|
|
921
1318
|
success: false,
|
|
@@ -937,17 +1334,41 @@ export async function shadowPull(worktreeDir, options) {
|
|
|
937
1334
|
return result;
|
|
938
1335
|
}
|
|
939
1336
|
// Check if remote branch exists before attempting pull
|
|
1337
|
+
// AC: @config-shadow ac-3 — use configured remote instead of hardcoded origin
|
|
1338
|
+
const remoteName = getRemoteName(options);
|
|
940
1339
|
// Fetch first to ensure refs are up to date
|
|
941
|
-
await fetchRemote(projectRoot);
|
|
942
|
-
const remoteHasBranch = await remoteBranchExists(projectRoot, branchName);
|
|
1340
|
+
await fetchRemote(projectRoot, remoteName);
|
|
1341
|
+
const remoteHasBranch = await remoteBranchExists(projectRoot, branchName, remoteName);
|
|
943
1342
|
if (!remoteHasBranch) {
|
|
944
1343
|
// Remote branch doesn't exist yet - nothing to pull, but success
|
|
945
1344
|
result.success = true;
|
|
946
1345
|
return result;
|
|
947
1346
|
}
|
|
1347
|
+
// Stash uncommitted changes before pulling to avoid false conflict reports
|
|
1348
|
+
let stashed = false;
|
|
1349
|
+
try {
|
|
1350
|
+
const { stdout } = await runGitAsync(worktreeDir, ["stash", "push", "-m", "shadow-sync-auto"]);
|
|
1351
|
+
stashed = !stdout.includes("No local changes");
|
|
1352
|
+
}
|
|
1353
|
+
catch {
|
|
1354
|
+
// If stash fails, skip the pull entirely — don't risk reporting a false conflict
|
|
1355
|
+
result.success = true;
|
|
1356
|
+
return result;
|
|
1357
|
+
}
|
|
1358
|
+
const unstash = async () => {
|
|
1359
|
+
if (stashed) {
|
|
1360
|
+
try {
|
|
1361
|
+
await runGitAsync(worktreeDir, ["stash", "pop"]);
|
|
1362
|
+
}
|
|
1363
|
+
catch {
|
|
1364
|
+
// Stash pop conflict is unlikely but leave stash intact if it happens
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
948
1368
|
try {
|
|
949
1369
|
// Try fast-forward only first (cleanest)
|
|
950
1370
|
await runGitAsync(worktreeDir, ["pull", "--ff-only"]);
|
|
1371
|
+
await unstash();
|
|
951
1372
|
result.success = true;
|
|
952
1373
|
result.pulled = true;
|
|
953
1374
|
return result;
|
|
@@ -958,6 +1379,7 @@ export async function shadowPull(worktreeDir, options) {
|
|
|
958
1379
|
try {
|
|
959
1380
|
// AC: @shadow-sync ac-6 - Fall back to rebase
|
|
960
1381
|
await runGitAsync(worktreeDir, ["pull", "--rebase"]);
|
|
1382
|
+
await unstash();
|
|
961
1383
|
result.success = true;
|
|
962
1384
|
result.pulled = true;
|
|
963
1385
|
return result;
|
|
@@ -972,6 +1394,7 @@ export async function shadowPull(worktreeDir, options) {
|
|
|
972
1394
|
catch {
|
|
973
1395
|
// May not be in rebase state, ignore
|
|
974
1396
|
}
|
|
1397
|
+
await unstash();
|
|
975
1398
|
result.hadConflict = true;
|
|
976
1399
|
result.error = "Sync conflict detected. Run `kspec shadow resolve` to fix.";
|
|
977
1400
|
return result;
|
|
@@ -1004,6 +1427,135 @@ export async function shadowSync(worktreeDir, options) {
|
|
|
1004
1427
|
}
|
|
1005
1428
|
return pullResult;
|
|
1006
1429
|
}
|
|
1430
|
+
// ─── Lazy Drift Check ────────────────────────────────────────────────────────
|
|
1431
|
+
const FETCH_TIMEOUT_MS = 5000;
|
|
1432
|
+
/**
|
|
1433
|
+
* Spawn a git command with a hard timeout. Returns stdout/stderr on success,
|
|
1434
|
+
* throws on non-zero exit or timeout. On timeout, sends SIGTERM then SIGKILL.
|
|
1435
|
+
*
|
|
1436
|
+
* Used for drift check fetch only — other git ops continue using runGitAsync.
|
|
1437
|
+
*
|
|
1438
|
+
* AC: @shadow-lazy-read-sync ac-fetch-timeout
|
|
1439
|
+
*/
|
|
1440
|
+
export function spawnGitWithTimeout(cwd, args, timeoutMs) {
|
|
1441
|
+
return new Promise((resolve, reject) => {
|
|
1442
|
+
const child = spawn("git", args, {
|
|
1443
|
+
cwd,
|
|
1444
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1445
|
+
});
|
|
1446
|
+
let stdout = "";
|
|
1447
|
+
let stderr = "";
|
|
1448
|
+
child.stdout.on("data", (d) => {
|
|
1449
|
+
stdout += d;
|
|
1450
|
+
});
|
|
1451
|
+
child.stderr.on("data", (d) => {
|
|
1452
|
+
stderr += d;
|
|
1453
|
+
});
|
|
1454
|
+
let promiseSettled = false;
|
|
1455
|
+
let processExited = false;
|
|
1456
|
+
const timer = setTimeout(() => {
|
|
1457
|
+
child.kill("SIGTERM");
|
|
1458
|
+
setTimeout(() => {
|
|
1459
|
+
if (!processExited)
|
|
1460
|
+
child.kill("SIGKILL");
|
|
1461
|
+
}, 1000);
|
|
1462
|
+
promiseSettled = true;
|
|
1463
|
+
reject(new Error(`git ${args[0]} timed out after ${timeoutMs}ms`));
|
|
1464
|
+
}, timeoutMs);
|
|
1465
|
+
child.on("close", (code) => {
|
|
1466
|
+
processExited = true;
|
|
1467
|
+
clearTimeout(timer);
|
|
1468
|
+
if (promiseSettled)
|
|
1469
|
+
return;
|
|
1470
|
+
promiseSettled = true;
|
|
1471
|
+
if (code === 0)
|
|
1472
|
+
resolve({ stdout, stderr });
|
|
1473
|
+
else
|
|
1474
|
+
reject(new Error(`git ${args[0]} exited ${code}: ${stderr}`));
|
|
1475
|
+
});
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Lightweight drift check: determine whether local shadow branch needs
|
|
1480
|
+
* to pull from remote. Uses FETCH_HEAD mtime to avoid redundant fetches,
|
|
1481
|
+
* and ahead/behind counts to decide if a pull is needed.
|
|
1482
|
+
*
|
|
1483
|
+
* AC: @shadow-lazy-read-sync ac-drift-check
|
|
1484
|
+
* AC: @shadow-lazy-read-sync ac-fetch-head-location
|
|
1485
|
+
* AC: @shadow-lazy-read-sync ac-fetch-head-freshness
|
|
1486
|
+
* AC: @shadow-lazy-read-sync ac-fetch-when-stale
|
|
1487
|
+
* AC: @shadow-lazy-read-sync ac-fetch-timeout-no-error
|
|
1488
|
+
* AC: @shadow-lazy-read-sync ac-fetch-timeout-debug-log
|
|
1489
|
+
* AC: @shadow-lazy-read-sync ac-pull-when-behind
|
|
1490
|
+
* AC: @shadow-lazy-read-sync ac-no-pull-when-ahead
|
|
1491
|
+
* AC: @shadow-lazy-read-sync ac-pull-when-diverged
|
|
1492
|
+
* AC: @shadow-lazy-read-sync ac-upstream-ref-missing
|
|
1493
|
+
* AC: @shadow-lazy-read-sync ac-no-drift-fast-path
|
|
1494
|
+
* AC: @shadow-lazy-read-sync ac-threshold-from-config
|
|
1495
|
+
*
|
|
1496
|
+
* @returns true if shadowPull() should be called, false if local state is current
|
|
1497
|
+
*/
|
|
1498
|
+
export async function shadowNeedsSync(worktreeDir, remoteName, thresholdMs) {
|
|
1499
|
+
// 1. Resolve FETCH_HEAD path for this worktree
|
|
1500
|
+
// AC: ac-fetch-head-location — use rev-parse --git-path from worktree dir
|
|
1501
|
+
const { stdout: fetchHeadRaw } = await runGitAsync(worktreeDir, [
|
|
1502
|
+
"rev-parse",
|
|
1503
|
+
"--git-path",
|
|
1504
|
+
"FETCH_HEAD",
|
|
1505
|
+
]);
|
|
1506
|
+
const fetchHeadPath = path.resolve(worktreeDir, fetchHeadRaw.trim());
|
|
1507
|
+
// 2. Check freshness — if stale or missing, fetch with timeout
|
|
1508
|
+
// AC: ac-fetch-head-freshness, ac-fetch-when-stale
|
|
1509
|
+
let fetchNeeded = true;
|
|
1510
|
+
try {
|
|
1511
|
+
const stat = await fs.stat(fetchHeadPath);
|
|
1512
|
+
fetchNeeded = Date.now() - stat.mtimeMs > thresholdMs;
|
|
1513
|
+
}
|
|
1514
|
+
catch {
|
|
1515
|
+
// No FETCH_HEAD — need to fetch
|
|
1516
|
+
}
|
|
1517
|
+
if (fetchNeeded) {
|
|
1518
|
+
try {
|
|
1519
|
+
// AC: ac-fetch-timeout — kill if exceeds FETCH_TIMEOUT_MS
|
|
1520
|
+
await spawnGitWithTimeout(worktreeDir, ["fetch", remoteName], FETCH_TIMEOUT_MS);
|
|
1521
|
+
}
|
|
1522
|
+
catch (err) {
|
|
1523
|
+
// AC: ac-fetch-timeout-no-error — no error surfaced to user
|
|
1524
|
+
// AC: ac-fetch-timeout-debug-log — debug log if enabled
|
|
1525
|
+
if (isDebugEnabled()) {
|
|
1526
|
+
console.error(`[DEBUG] shadow drift-check: fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1527
|
+
}
|
|
1528
|
+
return false;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
// 3. Check ahead/behind — only sync when behind or diverged
|
|
1532
|
+
// AC: ac-pull-when-behind, ac-no-pull-when-ahead, ac-pull-when-diverged
|
|
1533
|
+
try {
|
|
1534
|
+
const { stdout } = await runGitAsync(worktreeDir, [
|
|
1535
|
+
"rev-list",
|
|
1536
|
+
"--left-right",
|
|
1537
|
+
"--count",
|
|
1538
|
+
"HEAD...@{u}",
|
|
1539
|
+
]);
|
|
1540
|
+
const [, behind] = stdout.trim().split("\t").map(Number);
|
|
1541
|
+
// AC: ac-no-drift-fast-path — behind === 0 means no pull needed
|
|
1542
|
+
return behind > 0;
|
|
1543
|
+
}
|
|
1544
|
+
catch {
|
|
1545
|
+
// AC: ac-upstream-ref-missing — force sync as safer default
|
|
1546
|
+
return true;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
/**
|
|
1550
|
+
* Check if debug logging is enabled (KSPEC_DEBUG=1 or --debug-shadow).
|
|
1551
|
+
*/
|
|
1552
|
+
function isDebugEnabled() {
|
|
1553
|
+
if (process.env.KSPEC_DEBUG === "1")
|
|
1554
|
+
return true;
|
|
1555
|
+
if (getVerboseModeFunc?.())
|
|
1556
|
+
return true;
|
|
1557
|
+
return false;
|
|
1558
|
+
}
|
|
1007
1559
|
/**
|
|
1008
1560
|
* Check if .gitignore has uncommitted changes
|
|
1009
1561
|
*/
|
|
@@ -1092,6 +1644,123 @@ async function ensureGitignore(projectRoot, options) {
|
|
|
1092
1644
|
throw new ShadowError(`Failed to update .gitignore: ${error}`, "GIT_ERROR", "Check file permissions for .gitignore");
|
|
1093
1645
|
}
|
|
1094
1646
|
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Add .kspec-sessions/ to root .gitignore if not already present.
|
|
1649
|
+
* Does NOT commit — caller is responsible for committing if needed.
|
|
1650
|
+
*
|
|
1651
|
+
* AC: @session-storage-modes ac-gitignore
|
|
1652
|
+
*
|
|
1653
|
+
* @param projectRoot Git repository root
|
|
1654
|
+
* @returns true if entry was added, false if already present
|
|
1655
|
+
*/
|
|
1656
|
+
export async function needsSessionsGitignore(projectRoot) {
|
|
1657
|
+
const gitignorePath = path.join(projectRoot, ".gitignore");
|
|
1658
|
+
let content = "";
|
|
1659
|
+
try {
|
|
1660
|
+
content = await fs.readFile(gitignorePath, "utf-8");
|
|
1661
|
+
}
|
|
1662
|
+
catch {
|
|
1663
|
+
// File doesn't exist — entry is needed
|
|
1664
|
+
return true;
|
|
1665
|
+
}
|
|
1666
|
+
const lines = content.split("\n");
|
|
1667
|
+
const patterns = [
|
|
1668
|
+
SESSIONS_WORKTREE_DIR,
|
|
1669
|
+
`${SESSIONS_WORKTREE_DIR}/`,
|
|
1670
|
+
`/${SESSIONS_WORKTREE_DIR}`,
|
|
1671
|
+
`/${SESSIONS_WORKTREE_DIR}/`,
|
|
1672
|
+
];
|
|
1673
|
+
for (const line of lines) {
|
|
1674
|
+
const trimmed = line.trim();
|
|
1675
|
+
if (patterns.includes(trimmed)) {
|
|
1676
|
+
return false; // Already present
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
return true;
|
|
1680
|
+
}
|
|
1681
|
+
export async function ensureSessionsGitignore(projectRoot) {
|
|
1682
|
+
const gitignorePath = path.join(projectRoot, ".gitignore");
|
|
1683
|
+
const entry = `${SESSIONS_WORKTREE_DIR}/`;
|
|
1684
|
+
try {
|
|
1685
|
+
const needed = await needsSessionsGitignore(projectRoot);
|
|
1686
|
+
if (!needed) {
|
|
1687
|
+
return false;
|
|
1688
|
+
}
|
|
1689
|
+
let content = "";
|
|
1690
|
+
try {
|
|
1691
|
+
content = await fs.readFile(gitignorePath, "utf-8");
|
|
1692
|
+
}
|
|
1693
|
+
catch {
|
|
1694
|
+
// File doesn't exist, will create
|
|
1695
|
+
}
|
|
1696
|
+
// Add to gitignore
|
|
1697
|
+
const newContent = content.endsWith("\n") || content === ""
|
|
1698
|
+
? `${content}${entry}\n`
|
|
1699
|
+
: `${content}\n${entry}\n`;
|
|
1700
|
+
await fs.writeFile(gitignorePath, newContent, "utf-8");
|
|
1701
|
+
return true;
|
|
1702
|
+
}
|
|
1703
|
+
catch (error) {
|
|
1704
|
+
throw new ShadowError(`Failed to update .gitignore with sessions directory: ${error}`, "GIT_ERROR", "Check file permissions for .gitignore");
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* Add sessions/ to .kspec/.gitignore to prevent legacy session data
|
|
1709
|
+
* from being tracked on kspec-meta shadow branch.
|
|
1710
|
+
*
|
|
1711
|
+
* AC: @session-legacy-migration ac-shadow-gitignore
|
|
1712
|
+
*
|
|
1713
|
+
* @param projectRoot Git repository root
|
|
1714
|
+
* @param options Optional shadow configuration for directory name
|
|
1715
|
+
* @returns true if entry was added, false if already present
|
|
1716
|
+
*/
|
|
1717
|
+
export async function needsShadowSessionsGitignore(projectRoot, options) {
|
|
1718
|
+
const directoryName = getDirectoryName(options);
|
|
1719
|
+
const shadowGitignorePath = path.join(projectRoot, directoryName, ".gitignore");
|
|
1720
|
+
const entry = "sessions/";
|
|
1721
|
+
try {
|
|
1722
|
+
const content = await fs.readFile(shadowGitignorePath, "utf-8");
|
|
1723
|
+
const lines = content.split("\n");
|
|
1724
|
+
if (lines.some((line) => line.trim() === entry || line.trim() === "sessions")) {
|
|
1725
|
+
return false; // Already present
|
|
1726
|
+
}
|
|
1727
|
+
return true;
|
|
1728
|
+
}
|
|
1729
|
+
catch {
|
|
1730
|
+
// File doesn't exist — can't add to non-existent file
|
|
1731
|
+
return false;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
export async function ensureShadowSessionsGitignore(projectRoot, options) {
|
|
1735
|
+
const directoryName = getDirectoryName(options);
|
|
1736
|
+
const shadowGitignorePath = path.join(projectRoot, directoryName, ".gitignore");
|
|
1737
|
+
const entry = "sessions/";
|
|
1738
|
+
try {
|
|
1739
|
+
const needed = await needsShadowSessionsGitignore(projectRoot, options);
|
|
1740
|
+
if (!needed) {
|
|
1741
|
+
return false;
|
|
1742
|
+
}
|
|
1743
|
+
let content = "";
|
|
1744
|
+
try {
|
|
1745
|
+
content = await fs.readFile(shadowGitignorePath, "utf-8");
|
|
1746
|
+
}
|
|
1747
|
+
catch {
|
|
1748
|
+
// File doesn't exist — this shouldn't happen since init creates it,
|
|
1749
|
+
// but handle gracefully
|
|
1750
|
+
return false;
|
|
1751
|
+
}
|
|
1752
|
+
// Add to gitignore
|
|
1753
|
+
const newContent = content.endsWith("\n") || content === ""
|
|
1754
|
+
? `${content}${entry}\n`
|
|
1755
|
+
: `${content}\n${entry}\n`;
|
|
1756
|
+
await fs.writeFile(shadowGitignorePath, newContent, "utf-8");
|
|
1757
|
+
return true;
|
|
1758
|
+
}
|
|
1759
|
+
catch (error) {
|
|
1760
|
+
// Non-fatal — shadow gitignore update is best-effort
|
|
1761
|
+
return false;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1095
1764
|
/**
|
|
1096
1765
|
* Generate initial manifest content for shadow branch
|
|
1097
1766
|
*/
|
|
@@ -1350,14 +2019,32 @@ export async function initializeShadow(projectRoot, options = {}) {
|
|
|
1350
2019
|
if (remoteExists) {
|
|
1351
2020
|
remoteHasShadow = await remoteBranchExists(projectRoot, branchName, remoteName);
|
|
1352
2021
|
}
|
|
2022
|
+
let stashedWorktreeDir = null;
|
|
1353
2023
|
try {
|
|
1354
2024
|
// Step 1: Update .gitignore first (before creating worktree)
|
|
1355
2025
|
result.gitignoreUpdated = await ensureGitignore(projectRoot, options.shadow);
|
|
2026
|
+
// Step 1b: Also add .kspec-sessions/ to .gitignore
|
|
2027
|
+
// AC: @session-storage-modes ac-gitignore
|
|
2028
|
+
const sessionsAdded = await ensureSessionsGitignore(projectRoot);
|
|
2029
|
+
if (sessionsAdded) {
|
|
2030
|
+
// Commit .kspec-sessions/ gitignore entry (may be separate commit if .kspec/ was already present)
|
|
2031
|
+
await runGitAsync(projectRoot, ["add", ".gitignore"]);
|
|
2032
|
+
await runGitAsync(projectRoot, [
|
|
2033
|
+
"commit",
|
|
2034
|
+
"-m",
|
|
2035
|
+
`chore: add ${SESSIONS_WORKTREE_DIR}/ to .gitignore for session storage`,
|
|
2036
|
+
]);
|
|
2037
|
+
}
|
|
2038
|
+
// Step 1c: Create .kspec-sessions/ directory
|
|
2039
|
+
// AC: @session-storage-modes ac-sessions-dir-autocreate
|
|
2040
|
+
const sessionsDir = path.join(projectRoot, SESSIONS_WORKTREE_DIR);
|
|
2041
|
+
await fs.mkdir(sessionsDir, { recursive: true });
|
|
2042
|
+
result.sessionsDirectoryCreated = true;
|
|
1356
2043
|
// Step 2: Create worktree with orphan branch (or attach to existing branch)
|
|
1357
2044
|
if (!status.worktreeExists || !status.worktreeLinked) {
|
|
1358
2045
|
// Remove existing directory if present but not linked
|
|
1359
2046
|
if (status.worktreeExists && !status.worktreeLinked) {
|
|
1360
|
-
await
|
|
2047
|
+
stashedWorktreeDir = await stashBrokenWorktreeDir(worktreeDir);
|
|
1361
2048
|
}
|
|
1362
2049
|
// Remove stale worktree reference if any
|
|
1363
2050
|
try {
|
|
@@ -1405,14 +2092,20 @@ export async function initializeShadow(projectRoot, options = {}) {
|
|
|
1405
2092
|
else if (!status.branchExists) {
|
|
1406
2093
|
// AC: @shadow-init-remote ac-2 ac-3 - No remote branch or no remote - create orphan branch
|
|
1407
2094
|
// AC: @config-shadow ac-1 — use configured branch name
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
2095
|
+
// AC: @config-shadow ac-10 — fallback for git < 2.42
|
|
2096
|
+
if (gitSupportsOrphanWorktree(projectRoot)) {
|
|
2097
|
+
await runGitAsync(projectRoot, [
|
|
2098
|
+
"worktree",
|
|
2099
|
+
"add",
|
|
2100
|
+
"--orphan",
|
|
2101
|
+
"-b",
|
|
2102
|
+
branchName,
|
|
2103
|
+
directoryName,
|
|
2104
|
+
]);
|
|
2105
|
+
}
|
|
2106
|
+
else {
|
|
2107
|
+
await createOrphanBranchFallback(projectRoot, branchName, directoryName);
|
|
2108
|
+
}
|
|
1416
2109
|
result.branchCreated = true;
|
|
1417
2110
|
}
|
|
1418
2111
|
else {
|
|
@@ -1454,7 +2147,8 @@ export async function initializeShadow(projectRoot, options = {}) {
|
|
|
1454
2147
|
// AC: @artifacts-directory ac-init-creates, ac-gitignore-entry
|
|
1455
2148
|
const artifactsDir = path.join(worktreeDir, "artifacts");
|
|
1456
2149
|
await fs.mkdir(artifactsDir, { recursive: true });
|
|
1457
|
-
|
|
2150
|
+
// AC: @session-legacy-migration ac-shadow-gitignore — sessions/ not tracked on kspec-meta
|
|
2151
|
+
await fs.writeFile(path.join(worktreeDir, ".gitignore"), "# Ephemeral artifacts - reports, exports, generated files\n# Not tracked in shadow branch\nartifacts/\n\n# Sessions stored in .kspec-sessions/ at project root, not on shadow branch\nsessions/\n", "utf-8");
|
|
1458
2152
|
filesCreated = true;
|
|
1459
2153
|
}
|
|
1460
2154
|
// Step 4: Initial commit if files were created
|
|
@@ -1470,10 +2164,23 @@ export async function initializeShadow(projectRoot, options = {}) {
|
|
|
1470
2164
|
// Step 7: Configure merge driver for semantic YAML merging
|
|
1471
2165
|
// AC: @yaml-merge-driver ac-12
|
|
1472
2166
|
await configureMergeDriver(projectRoot, worktreeDir);
|
|
2167
|
+
// Step 8: Initialize session branch worktree if sessions.storage is "branch"
|
|
2168
|
+
// AC: @session-branch-worktree ac-init
|
|
2169
|
+
if (options.sessions?.storage === "branch") {
|
|
2170
|
+
const { initializeSessionBranch } = await import("./session-branch.js");
|
|
2171
|
+
const sessionBranchName = options.sessions.branch || "kspec-sessions";
|
|
2172
|
+
const sessionResult = await initializeSessionBranch(projectRoot, sessionBranchName);
|
|
2173
|
+
if (sessionResult.success) {
|
|
2174
|
+
result.sessionBranchCreated = true;
|
|
2175
|
+
}
|
|
2176
|
+
// Non-fatal: session branch failure doesn't block shadow init
|
|
2177
|
+
}
|
|
2178
|
+
await discardStashedWorktreeDir(stashedWorktreeDir);
|
|
1473
2179
|
result.success = true;
|
|
1474
2180
|
return result;
|
|
1475
2181
|
}
|
|
1476
2182
|
catch (error) {
|
|
2183
|
+
await restoreStashedWorktreeDir(stashedWorktreeDir, worktreeDir).catch(() => { });
|
|
1477
2184
|
result.error = error instanceof Error ? error.message : String(error);
|
|
1478
2185
|
return result;
|
|
1479
2186
|
}
|
|
@@ -1492,6 +2199,8 @@ export async function repairShadow(projectRoot, options) {
|
|
|
1492
2199
|
const branchName = getBranchName(options);
|
|
1493
2200
|
const directoryName = getDirectoryName(options);
|
|
1494
2201
|
const status = await getShadowStatus(projectRoot, options);
|
|
2202
|
+
const remoteQueryTarget = getRemoteQueryTarget(options);
|
|
2203
|
+
const remoteHasShadow = await remoteShadowBranchExists(projectRoot, options);
|
|
1495
2204
|
if (status.healthy) {
|
|
1496
2205
|
return {
|
|
1497
2206
|
success: true,
|
|
@@ -1504,7 +2213,7 @@ export async function repairShadow(projectRoot, options) {
|
|
|
1504
2213
|
pushedToRemote: false,
|
|
1505
2214
|
};
|
|
1506
2215
|
}
|
|
1507
|
-
if (!status.branchExists) {
|
|
2216
|
+
if (!status.branchExists && !remoteHasShadow) {
|
|
1508
2217
|
// Can't repair without a branch - need full init
|
|
1509
2218
|
return {
|
|
1510
2219
|
success: false,
|
|
@@ -1518,8 +2227,9 @@ export async function repairShadow(projectRoot, options) {
|
|
|
1518
2227
|
error: "Shadow branch does not exist. Run `kspec init` instead.",
|
|
1519
2228
|
};
|
|
1520
2229
|
}
|
|
1521
|
-
//
|
|
2230
|
+
// The branch exists locally or remotely, but the worktree is broken - repair it.
|
|
1522
2231
|
const worktreeDir = path.join(projectRoot, directoryName);
|
|
2232
|
+
let stashedWorktreeDir = null;
|
|
1523
2233
|
try {
|
|
1524
2234
|
// Remove stale worktree reference
|
|
1525
2235
|
try {
|
|
@@ -1534,7 +2244,7 @@ export async function repairShadow(projectRoot, options) {
|
|
|
1534
2244
|
// Ignore - worktree may not be in git's list
|
|
1535
2245
|
}
|
|
1536
2246
|
// Remove directory if exists (handles corrupted .git file case)
|
|
1537
|
-
await
|
|
2247
|
+
stashedWorktreeDir = await stashBrokenWorktreeDir(worktreeDir);
|
|
1538
2248
|
// Prune stale worktree references (cleans up orphaned entries)
|
|
1539
2249
|
try {
|
|
1540
2250
|
await runGitAsync(projectRoot, ["worktree", "prune"]);
|
|
@@ -1542,6 +2252,13 @@ export async function repairShadow(projectRoot, options) {
|
|
|
1542
2252
|
catch {
|
|
1543
2253
|
// Ignore - prune is best-effort
|
|
1544
2254
|
}
|
|
2255
|
+
if (!status.branchExists && remoteHasShadow) {
|
|
2256
|
+
await runGitAsync(projectRoot, [
|
|
2257
|
+
"fetch",
|
|
2258
|
+
remoteQueryTarget,
|
|
2259
|
+
`${branchName}:${branchName}`,
|
|
2260
|
+
]);
|
|
2261
|
+
}
|
|
1545
2262
|
// Recreate worktree
|
|
1546
2263
|
await runGitAsync(projectRoot, [
|
|
1547
2264
|
"worktree",
|
|
@@ -1549,6 +2266,13 @@ export async function repairShadow(projectRoot, options) {
|
|
|
1549
2266
|
directoryName,
|
|
1550
2267
|
branchName,
|
|
1551
2268
|
]);
|
|
2269
|
+
if (remoteHasShadow) {
|
|
2270
|
+
const tracking = await ensureRemoteTracking(worktreeDir, projectRoot, options);
|
|
2271
|
+
if (!tracking.success) {
|
|
2272
|
+
throw new Error(tracking.guidance || "Failed to configure shadow branch remote tracking");
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
await discardStashedWorktreeDir(stashedWorktreeDir);
|
|
1552
2276
|
// Install pre-commit hook
|
|
1553
2277
|
await installShadowHook(projectRoot);
|
|
1554
2278
|
// AC: @artifacts-directory ac-repair-recreates
|
|
@@ -1561,11 +2285,12 @@ export async function repairShadow(projectRoot, options) {
|
|
|
1561
2285
|
gitignoreUpdated: false,
|
|
1562
2286
|
initialCommit: false,
|
|
1563
2287
|
alreadyExists: false,
|
|
1564
|
-
createdFromRemote:
|
|
2288
|
+
createdFromRemote: !status.branchExists && remoteHasShadow,
|
|
1565
2289
|
pushedToRemote: false,
|
|
1566
2290
|
};
|
|
1567
2291
|
}
|
|
1568
2292
|
catch (error) {
|
|
2293
|
+
await restoreStashedWorktreeDir(stashedWorktreeDir, worktreeDir).catch(() => { });
|
|
1569
2294
|
return {
|
|
1570
2295
|
success: false,
|
|
1571
2296
|
branchCreated: false,
|