devrites 4.5.0 → 4.7.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/CHANGELOG.md +12 -0
- package/NOTICE.md +0 -13
- package/README.md +4 -3
- package/docs/adr/0029-v5-workspace-schema-and-native-migration.md +62 -0
- package/docs/adr/README.md +3 -2
- package/docs/architecture.md +1 -1
- package/docs/cli.md +2 -2
- package/docs/command-map.md +2 -2
- package/docs/engine/commands.md +9 -0
- package/docs/engine/state-schema.md +2 -2
- package/docs/engine/workspace-schema.md +9 -3
- package/docs/usage.md +1 -1
- package/engine/commands.go +23 -0
- package/engine/internal/fsutil/copy_test.go +193 -0
- package/engine/internal/gate/gate_test.go +7 -7
- package/engine/internal/gate/readiness_binding_test.go +8 -2
- package/engine/internal/install/apply.go +314 -0
- package/engine/internal/install/binary.go +255 -0
- package/engine/internal/install/install.go +0 -1596
- package/engine/internal/install/manifest.go +109 -0
- package/engine/internal/install/merge.go +468 -0
- package/engine/internal/install/preflight.go +234 -0
- package/engine/internal/install/uninstall.go +119 -0
- package/engine/internal/install/update.go +184 -0
- package/engine/internal/lib/candidate_test.go +3 -0
- package/engine/internal/lib/closeout.go +4 -0
- package/engine/internal/lib/closeout_test.go +3 -3
- package/engine/internal/lib/migrate.go +219 -0
- package/engine/internal/lib/migrate_test.go +154 -0
- package/engine/internal/lib/resolve.go +3 -0
- package/engine/internal/lib/resolve_remediation_test.go +1 -1
- package/engine/internal/lib/secretscan_test.go +1 -0
- package/engine/internal/lib/taskgraph_test.go +2 -2
- package/engine/internal/parallel/ops.go +67 -16
- package/engine/internal/parallel/ops_test.go +211 -0
- package/engine/internal/state/cmd/workflowmanifest/main.go +24 -13
- package/engine/internal/state/cmd/workflowmanifest/main_test.go +96 -0
- package/engine/internal/state/cursor.go +74 -0
- package/engine/internal/state/schema.go +3 -2
- package/engine/internal/state/state_test.go +2 -2
- package/engine/internal/state/workflow_manifest.json +1 -1
- package/engine/internal/state/workspaceschema.go +58 -0
- package/engine/internal/state/workspaceschema_test.go +67 -0
- package/engine/main.go +7 -31
- package/engine/root_routing_test.go +10 -2
- package/engine/testdata/fixtures/basic/devrites-root/work/auth-tokens/state.md +1 -0
- package/engine/testdata/fixtures/basic/devrites-root/work/search-ranking/state.md +1 -0
- package/engine/testdata/golden/TestParityResolve/answer/state.md.golden +1 -0
- package/engine/testdata/golden/TestParityResolve/drop/state.md.golden +1 -0
- package/engine/tests/parity_closeout_test.go +3 -0
- package/engine/tests/parity_resolve_test.go +1 -0
- package/pack/.claude/agents/devrites-code-reviewer.md +8 -0
- package/pack/.claude/agents/devrites-devex-reviewer.md +7 -0
- package/pack/.claude/agents/devrites-doubt-reviewer.md +7 -0
- package/pack/.claude/agents/devrites-evidence-scout.md +4 -0
- package/pack/.claude/agents/devrites-frontend-reviewer.md +8 -0
- package/pack/.claude/agents/devrites-performance-reviewer.md +7 -0
- package/pack/.claude/agents/devrites-plan-reviewer.md +8 -0
- package/pack/.claude/agents/devrites-retrospector.md +2 -0
- package/pack/.claude/agents/devrites-security-auditor.md +7 -0
- package/pack/.claude/agents/devrites-simplifier-reviewer.md +7 -0
- package/pack/.claude/agents/devrites-spec-reviewer.md +7 -0
- package/pack/.claude/agents/devrites-strategy-reviewer.md +7 -0
- package/pack/.claude/agents/devrites-test-analyst.md +7 -0
- package/pack/.claude/skills/devrites-frontend-craft/SKILL.md +4 -7
- package/pack/.claude/skills/devrites-frontend-craft/reference/quality-standards.md +9 -4
- package/pack/.claude/skills/devrites-lib/reference/intent-map.md +7 -0
- package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +1 -1
- package/pack/.claude/skills/devrites-lib/reference/reply-contract.md +9 -7
- package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +1 -1
- package/pack/.claude/skills/devrites-lib/reference/standards/code-review.md +6 -1
- package/pack/.claude/skills/devrites-lib/reference/standards/coding-style.md +4 -3
- package/pack/.claude/skills/devrites-lib/reference/standards/context-hygiene.md +4 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/core.md +3 -6
- package/pack/.claude/skills/devrites-lib/reference/standards/data-integrity.md +4 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/development-workflow.md +8 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/documentation.md +6 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/edge-case-trace.md +3 -1
- package/pack/.claude/skills/devrites-lib/reference/standards/elicitation.md +5 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/error-handling.md +14 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/integration-reliability.md +18 -6
- package/pack/.claude/skills/devrites-lib/reference/standards/patterns.md +16 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/performance.md +4 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/repository-topology.md +6 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/security-checklist.md +3 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/security.md +10 -2
- package/pack/.claude/skills/devrites-lib/reference/standards/skill-authoring.md +8 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/spec-grammar.md +5 -6
- package/pack/.claude/skills/devrites-lib/reference/standards/testing.md +2 -2
- package/pack/.claude/skills/devrites-lib/reference/standards/tooling.md +1 -1
- package/pack/.claude/skills/rite-define/reference/plan-template.md +5 -2
- package/pack/.claude/skills/rite-learn/SKILL.md +6 -0
- package/pack/.claude/skills/rite-polish/SKILL.md +4 -0
- package/pack/.claude/skills/rite-polish/reference/anti-ai-slop.md +12 -53
- package/pack/.claude/skills/rite-pr-feedback/SKILL.md +7 -2
- package/pack/.claude/skills/rite-spec/reference/spec-template.md +5 -4
- package/pack/.claude/skills/rite-status/SKILL.md +2 -0
- package/pack/generated/claude/agents/devrites-code-reviewer.md +8 -0
- package/pack/generated/claude/agents/devrites-devex-reviewer.md +7 -0
- package/pack/generated/claude/agents/devrites-doubt-reviewer.md +7 -0
- package/pack/generated/claude/agents/devrites-evidence-scout.md +4 -0
- package/pack/generated/claude/agents/devrites-frontend-reviewer.md +8 -0
- package/pack/generated/claude/agents/devrites-performance-reviewer.md +7 -0
- package/pack/generated/claude/agents/devrites-plan-reviewer.md +8 -0
- package/pack/generated/claude/agents/devrites-retrospector.md +2 -0
- package/pack/generated/claude/agents/devrites-security-auditor.md +7 -0
- package/pack/generated/claude/agents/devrites-simplifier-reviewer.md +7 -0
- package/pack/generated/claude/agents/devrites-spec-reviewer.md +7 -0
- package/pack/generated/claude/agents/devrites-strategy-reviewer.md +7 -0
- package/pack/generated/claude/agents/devrites-test-analyst.md +7 -0
- package/pack/generated/claude/skills/devrites-frontend-craft/SKILL.md +4 -7
- package/pack/generated/claude/skills/devrites-frontend-craft/reference/quality-standards.md +9 -4
- package/pack/generated/claude/skills/devrites-lib/reference/intent-map.md +7 -0
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +1 -1
- package/pack/generated/claude/skills/devrites-lib/reference/reply-contract.md +9 -7
- package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +1 -1
- package/pack/generated/claude/skills/devrites-lib/reference/standards/code-review.md +6 -1
- package/pack/generated/claude/skills/devrites-lib/reference/standards/coding-style.md +4 -3
- package/pack/generated/claude/skills/devrites-lib/reference/standards/context-hygiene.md +4 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/core.md +3 -6
- package/pack/generated/claude/skills/devrites-lib/reference/standards/data-integrity.md +4 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/development-workflow.md +8 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/documentation.md +6 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/edge-case-trace.md +3 -1
- package/pack/generated/claude/skills/devrites-lib/reference/standards/elicitation.md +5 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/error-handling.md +14 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/integration-reliability.md +18 -6
- package/pack/generated/claude/skills/devrites-lib/reference/standards/patterns.md +16 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/performance.md +4 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/repository-topology.md +6 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/security-checklist.md +3 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/security.md +10 -2
- package/pack/generated/claude/skills/devrites-lib/reference/standards/skill-authoring.md +8 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/spec-grammar.md +5 -6
- package/pack/generated/claude/skills/devrites-lib/reference/standards/testing.md +2 -2
- package/pack/generated/claude/skills/devrites-lib/reference/standards/tooling.md +1 -1
- package/pack/generated/claude/skills/rite-define/reference/plan-template.md +5 -2
- package/pack/generated/claude/skills/rite-learn/SKILL.md +6 -0
- package/pack/generated/claude/skills/rite-polish/SKILL.md +4 -0
- package/pack/generated/claude/skills/rite-polish/reference/anti-ai-slop.md +12 -53
- package/pack/generated/claude/skills/rite-pr-feedback/SKILL.md +7 -2
- package/pack/generated/claude/skills/rite-spec/reference/spec-template.md +5 -4
- package/pack/generated/claude/skills/rite-status/SKILL.md +2 -0
- package/pack/generated/codex/agents/devrites-code-reviewer.toml +8 -0
- package/pack/generated/codex/agents/devrites-devex-reviewer.toml +7 -0
- package/pack/generated/codex/agents/devrites-doubt-reviewer.toml +7 -0
- package/pack/generated/codex/agents/devrites-evidence-scout.toml +4 -0
- package/pack/generated/codex/agents/devrites-frontend-reviewer.toml +8 -0
- package/pack/generated/codex/agents/devrites-performance-reviewer.toml +7 -0
- package/pack/generated/codex/agents/devrites-plan-reviewer.toml +8 -0
- package/pack/generated/codex/agents/devrites-retrospector.toml +2 -0
- package/pack/generated/codex/agents/devrites-security-auditor.toml +7 -0
- package/pack/generated/codex/agents/devrites-simplifier-reviewer.toml +7 -0
- package/pack/generated/codex/agents/devrites-spec-reviewer.toml +7 -0
- package/pack/generated/codex/agents/devrites-strategy-reviewer.toml +7 -0
- package/pack/generated/codex/agents/devrites-test-analyst.toml +7 -0
- package/pack/generated/codex/skills/devrites-frontend-craft/SKILL.md +4 -7
- package/pack/generated/codex/skills/devrites-frontend-craft/reference/quality-standards.md +9 -4
- package/pack/generated/codex/skills/devrites-lib/reference/intent-map.md +7 -0
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +1 -1
- package/pack/generated/codex/skills/devrites-lib/reference/reply-contract.md +9 -7
- package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +1 -1
- package/pack/generated/codex/skills/devrites-lib/reference/standards/code-review.md +6 -1
- package/pack/generated/codex/skills/devrites-lib/reference/standards/coding-style.md +4 -3
- package/pack/generated/codex/skills/devrites-lib/reference/standards/context-hygiene.md +4 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/core.md +3 -6
- package/pack/generated/codex/skills/devrites-lib/reference/standards/data-integrity.md +4 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/development-workflow.md +8 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/documentation.md +6 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/edge-case-trace.md +3 -1
- package/pack/generated/codex/skills/devrites-lib/reference/standards/elicitation.md +5 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/error-handling.md +14 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/integration-reliability.md +18 -6
- package/pack/generated/codex/skills/devrites-lib/reference/standards/patterns.md +16 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/performance.md +4 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/repository-topology.md +6 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/security-checklist.md +3 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/security.md +10 -2
- package/pack/generated/codex/skills/devrites-lib/reference/standards/skill-authoring.md +8 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/spec-grammar.md +5 -6
- package/pack/generated/codex/skills/devrites-lib/reference/standards/testing.md +2 -2
- package/pack/generated/codex/skills/devrites-lib/reference/standards/tooling.md +1 -1
- package/pack/generated/codex/skills/rite-define/reference/plan-template.md +5 -2
- package/pack/generated/codex/skills/rite-learn/SKILL.md +6 -0
- package/pack/generated/codex/skills/rite-polish/SKILL.md +4 -0
- package/pack/generated/codex/skills/rite-polish/reference/anti-ai-slop.md +12 -53
- package/pack/generated/codex/skills/rite-pr-feedback/SKILL.md +7 -2
- package/pack/generated/codex/skills/rite-spec/reference/spec-template.md +5 -4
- package/pack/generated/codex/skills/rite-status/SKILL.md +2 -0
- package/package.json +1 -1
- package/docs/markdown-instruction-upgrade-2026-08-27.md +0 -127
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
package lib
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"flag"
|
|
5
|
+
"fmt"
|
|
6
|
+
"io"
|
|
7
|
+
"os"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"strings"
|
|
10
|
+
|
|
11
|
+
"github.com/devrites/devrites/internal/devritespaths"
|
|
12
|
+
"github.com/devrites/devrites/internal/fsutil"
|
|
13
|
+
"github.com/devrites/devrites/internal/state"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
// migrateStep is one deterministic action of a migration plan.
|
|
17
|
+
type migrateStep struct {
|
|
18
|
+
kind string // "phase", "cursor", "schema", or "stub"
|
|
19
|
+
path string // workspace-relative file a stub creates
|
|
20
|
+
detail string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type migrateQuestion struct {
|
|
24
|
+
id string
|
|
25
|
+
prompt string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type migratePlan struct {
|
|
29
|
+
phase string
|
|
30
|
+
steps []migrateStep
|
|
31
|
+
questions []migrateQuestion
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Migrate normalizes a pre-v5 feature workspace to the current schema: it
|
|
35
|
+
// converts legacy bullet cursor fields into canonical table rows, records the
|
|
36
|
+
// schema row, and creates missing required artifacts as empty stubs. Bound
|
|
37
|
+
// proof files are preserved byte-exact; content is never synthesized. The
|
|
38
|
+
// command is one-shot and fail-closed: on ambiguity it writes nothing, prints
|
|
39
|
+
// its questions, and exits non-zero; answers arrive on rerun via
|
|
40
|
+
// --answer id=choice. --dry-run prints the plan and always writes nothing.
|
|
41
|
+
func Migrate(root string, args []string, stdout, stderr io.Writer) int {
|
|
42
|
+
flags := flag.NewFlagSet("migrate", flag.ContinueOnError)
|
|
43
|
+
flags.SetOutput(io.Discard)
|
|
44
|
+
dryRun := flags.Bool("dry-run", false, "print the plan without writing")
|
|
45
|
+
var answerPairs stringSlice
|
|
46
|
+
flags.Var(&answerPairs, "answer", "answer a migration question: id=choice")
|
|
47
|
+
// Parse with interleaving support: stdlib stops at the first positional,
|
|
48
|
+
// so re-parse the remainder and collect positionals along the way.
|
|
49
|
+
var positionals []string
|
|
50
|
+
remaining := args
|
|
51
|
+
for {
|
|
52
|
+
if err := flags.Parse(remaining); err != nil {
|
|
53
|
+
fmt.Fprintf(stderr, "devrites: %v\n", err)
|
|
54
|
+
return 2
|
|
55
|
+
}
|
|
56
|
+
rest := flags.Args()
|
|
57
|
+
if len(rest) == 0 {
|
|
58
|
+
break
|
|
59
|
+
}
|
|
60
|
+
positionals = append(positionals, rest[0])
|
|
61
|
+
remaining = rest[1:]
|
|
62
|
+
}
|
|
63
|
+
if len(positionals) != 1 {
|
|
64
|
+
fmt.Fprintln(stderr, "usage: devrites-engine migrate <slug> [--dry-run] [--answer id=choice]")
|
|
65
|
+
return 2
|
|
66
|
+
}
|
|
67
|
+
slug := positionals[0]
|
|
68
|
+
|
|
69
|
+
work, err := devritespaths.ExistingFeatureDirChecked(root, slug)
|
|
70
|
+
if err != nil {
|
|
71
|
+
fmt.Fprintf(stderr, "migrate: %v\n", err)
|
|
72
|
+
return 2
|
|
73
|
+
}
|
|
74
|
+
answered := map[string]string{}
|
|
75
|
+
for _, pair := range answerPairs {
|
|
76
|
+
id, value, ok := strings.Cut(pair, "=")
|
|
77
|
+
if !ok || strings.TrimSpace(id) == "" {
|
|
78
|
+
fmt.Fprintf(stderr, "devrites: invalid --answer %q (want id=choice)\n", pair)
|
|
79
|
+
return 2
|
|
80
|
+
}
|
|
81
|
+
answered[strings.TrimSpace(id)] = strings.TrimSpace(value)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Read, plan, and apply under one feature lock so a concurrent
|
|
85
|
+
// `state resolve`/`close` can never be clobbered by a stale read.
|
|
86
|
+
ledger := filepath.Join(work, "state.md")
|
|
87
|
+
var applied int
|
|
88
|
+
var exit int
|
|
89
|
+
var result string
|
|
90
|
+
err = state.WithFeatureLock(root, slug, func() error {
|
|
91
|
+
raw, err := os.ReadFile(ledger)
|
|
92
|
+
if err != nil {
|
|
93
|
+
return err
|
|
94
|
+
}
|
|
95
|
+
lines := strings.Split(strings.ReplaceAll(string(raw), "\r\n", "\n"), "\n")
|
|
96
|
+
|
|
97
|
+
version, err := state.WorkspaceSchema(root, slug)
|
|
98
|
+
if err != nil {
|
|
99
|
+
return err
|
|
100
|
+
}
|
|
101
|
+
if version > state.SchemaVersion {
|
|
102
|
+
fmt.Fprintf(stderr, "migrate: workspace %s declares schema %d, newer than this engine's %d; upgrade devrites\n", slug, version, state.SchemaVersion)
|
|
103
|
+
exit = 3
|
|
104
|
+
return nil
|
|
105
|
+
}
|
|
106
|
+
if version == state.SchemaVersion {
|
|
107
|
+
result = fmt.Sprintf("migrate: %s already at schema %d; nothing to do\n", slug, version)
|
|
108
|
+
exit = 0
|
|
109
|
+
return nil
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
plan := buildMigratePlan(root, slug, lines, answered)
|
|
113
|
+
if len(plan.questions) > 0 {
|
|
114
|
+
for _, q := range plan.questions {
|
|
115
|
+
fmt.Fprintf(stderr, "migrate: question %s: %s\n", q.id, q.prompt)
|
|
116
|
+
}
|
|
117
|
+
fmt.Fprintln(stderr, "migrate: nothing written; re-run with --answer id=choice")
|
|
118
|
+
exit = 3
|
|
119
|
+
return nil
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if *dryRun {
|
|
123
|
+
fmt.Fprintf(stdout, "migrate: plan for %s\n", slug)
|
|
124
|
+
for _, step := range plan.steps {
|
|
125
|
+
fmt.Fprintf(stdout, " [%s] %s\n", step.kind, step.detail)
|
|
126
|
+
}
|
|
127
|
+
fmt.Fprintln(stdout, "migrate: dry run; nothing written")
|
|
128
|
+
exit = 0
|
|
129
|
+
return nil
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// New stub files go first: they are idempotent, never bound, and
|
|
133
|
+
// state.md is the commit point written last.
|
|
134
|
+
for _, step := range plan.steps {
|
|
135
|
+
if step.kind != "stub" {
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
path := filepath.Join(work, filepath.FromSlash(step.path))
|
|
139
|
+
if _, statErr := os.Stat(path); statErr == nil {
|
|
140
|
+
return fmt.Errorf("stub target %s appeared during migration; re-run migrate", step.path)
|
|
141
|
+
}
|
|
142
|
+
if err := fsutil.WriteFileAtomic(path, nil, 0o644); err != nil {
|
|
143
|
+
return err
|
|
144
|
+
}
|
|
145
|
+
applied++
|
|
146
|
+
}
|
|
147
|
+
next := lines
|
|
148
|
+
for _, step := range plan.steps {
|
|
149
|
+
if step.kind == "phase" {
|
|
150
|
+
next = state.UpsertCursorField(next, state.CursorPhase, plan.phase)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if converted, changed := state.ConvertCursorToTable(next); changed {
|
|
154
|
+
next = converted
|
|
155
|
+
}
|
|
156
|
+
next = state.UpsertCursorField(next, state.CursorSchema, fmt.Sprint(state.SchemaVersion))
|
|
157
|
+
if err := fsutil.WriteFileAtomic(ledger, []byte(strings.Join(next, "\n")), 0o644); err != nil {
|
|
158
|
+
return err
|
|
159
|
+
}
|
|
160
|
+
result = fmt.Sprintf("migrate: %s normalized to schema %d (%d stub(s) created)\n", slug, state.SchemaVersion, applied)
|
|
161
|
+
exit = 0
|
|
162
|
+
return nil
|
|
163
|
+
})
|
|
164
|
+
if err != nil {
|
|
165
|
+
fmt.Fprintf(stderr, "migrate: %v\n", err)
|
|
166
|
+
return 1
|
|
167
|
+
}
|
|
168
|
+
fmt.Fprint(stdout, result)
|
|
169
|
+
return exit
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
func buildMigratePlan(root, slug string, lines []string, answered map[string]string) migratePlan {
|
|
173
|
+
plan := migratePlan{}
|
|
174
|
+
phase, hasPhase := state.CursorField(lines, state.CursorPhase)
|
|
175
|
+
|
|
176
|
+
if value, ok := answered["phase"]; ok {
|
|
177
|
+
plan.steps = append(plan.steps, migrateStep{kind: "phase", detail: "record phase " + value + " from answer"})
|
|
178
|
+
phase, hasPhase = value, true
|
|
179
|
+
}
|
|
180
|
+
if !hasPhase {
|
|
181
|
+
plan.questions = append(plan.questions, migrateQuestion{id: "phase",
|
|
182
|
+
prompt: "state.md records no phase; re-run with --answer phase=<phase>"})
|
|
183
|
+
return plan
|
|
184
|
+
}
|
|
185
|
+
if _, ok := state.PolicyFor(state.Phase(phase)); !ok {
|
|
186
|
+
plan.questions = append(plan.questions, migrateQuestion{id: "phase",
|
|
187
|
+
prompt: "state.md records unknown phase " + phase + "; re-run with --answer phase=<phase>"})
|
|
188
|
+
return plan
|
|
189
|
+
}
|
|
190
|
+
plan.phase = phase
|
|
191
|
+
|
|
192
|
+
if state.CursorForm(lines) == "legacy" {
|
|
193
|
+
plan.steps = append(plan.steps, migrateStep{kind: "cursor", detail: "convert legacy cursor bullets to canonical table rows"})
|
|
194
|
+
}
|
|
195
|
+
plan.steps = append(plan.steps, migrateStep{kind: "schema", detail: fmt.Sprintf("record schema %d in state.md", state.SchemaVersion)})
|
|
196
|
+
|
|
197
|
+
policy, _ := state.PolicyFor(state.Phase(phase))
|
|
198
|
+
for _, artifact := range policy.RequiredArtifacts {
|
|
199
|
+
name := string(artifact)
|
|
200
|
+
if name == state.LedgerFile {
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
if _, err := os.Stat(filepath.Join(devritespaths.FeatureDir(root, slug), filepath.FromSlash(name))); err == nil {
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
plan.steps = append(plan.steps, migrateStep{kind: "stub", path: name, detail: "create missing " + name + " as an empty stub"})
|
|
207
|
+
}
|
|
208
|
+
return plan
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// stringSlice collects repeated string flag values.
|
|
212
|
+
type stringSlice []string
|
|
213
|
+
|
|
214
|
+
func (s *stringSlice) String() string { return strings.Join(*s, ",") }
|
|
215
|
+
|
|
216
|
+
func (s *stringSlice) Set(value string) error {
|
|
217
|
+
*s = append(*s, value)
|
|
218
|
+
return nil
|
|
219
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
package lib
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// seedPreV5Workspace builds a pre-v5 workspace with a bullet cursor, prose,
|
|
11
|
+
// and no required artifacts beyond state.md.
|
|
12
|
+
func seedPreV5Workspace(t *testing.T, phase string) (root, workspace string) {
|
|
13
|
+
t.Helper()
|
|
14
|
+
root = t.TempDir()
|
|
15
|
+
workspace = filepath.Join(root, "work", "feature")
|
|
16
|
+
if err := os.MkdirAll(workspace, 0o755); err != nil {
|
|
17
|
+
t.Fatal(err)
|
|
18
|
+
}
|
|
19
|
+
body := "- Phase: " + phase + "\n- Status: running\n- Next step: continue slice S-2\n\n# Status\n\nMinting middleware landed.\n"
|
|
20
|
+
if err := os.WriteFile(filepath.Join(workspace, "state.md"), []byte(body), 0o644); err != nil {
|
|
21
|
+
t.Fatal(err)
|
|
22
|
+
}
|
|
23
|
+
return root, workspace
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func TestMigrateDryRunPlansWithoutWriting(t *testing.T) {
|
|
27
|
+
root, workspace := seedPreV5Workspace(t, "build")
|
|
28
|
+
var stdout, stderr strings.Builder
|
|
29
|
+
if code := Migrate(root, []string{"feature", "--dry-run"}, &stdout, &stderr); code != 0 {
|
|
30
|
+
t.Fatalf("code=%d stderr=%q", code, stderr.String())
|
|
31
|
+
}
|
|
32
|
+
out := stdout.String()
|
|
33
|
+
if !strings.Contains(out, "migrate: plan for feature") ||
|
|
34
|
+
!strings.Contains(out, "[cursor] convert legacy cursor bullets") ||
|
|
35
|
+
!strings.Contains(out, "[schema] record schema") ||
|
|
36
|
+
!strings.Contains(out, "[stub] create missing spec.md") {
|
|
37
|
+
t.Fatalf("plan incomplete:\n%s", out)
|
|
38
|
+
}
|
|
39
|
+
// Nothing written: state.md bytes unchanged and no stub exists.
|
|
40
|
+
body, err := os.ReadFile(filepath.Join(workspace, "state.md"))
|
|
41
|
+
if err != nil || !strings.HasPrefix(string(body), "- Phase: build\n") {
|
|
42
|
+
t.Fatalf("state.md changed during dry run: %v %q", err, body)
|
|
43
|
+
}
|
|
44
|
+
if _, err := os.Stat(filepath.Join(workspace, "spec.md")); !os.IsNotExist(err) {
|
|
45
|
+
t.Fatalf("stub created during dry run: %v", err)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func TestMigrateExecutesCursorConversionAndStubs(t *testing.T) {
|
|
50
|
+
root, workspace := seedPreV5Workspace(t, "build")
|
|
51
|
+
var stdout, stderr strings.Builder
|
|
52
|
+
if code := Migrate(root, []string{"feature"}, &stdout, &stderr); code != 0 {
|
|
53
|
+
t.Fatalf("code=%d stderr=%q", code, stderr.String())
|
|
54
|
+
}
|
|
55
|
+
body, err := os.ReadFile(filepath.Join(workspace, "state.md"))
|
|
56
|
+
if err != nil {
|
|
57
|
+
t.Fatal(err)
|
|
58
|
+
}
|
|
59
|
+
text := string(body)
|
|
60
|
+
if !strings.Contains(text, "| phase | build |") || !strings.Contains(text, "| status | running |") {
|
|
61
|
+
t.Fatalf("cursor bullets not converted:\n%s", text)
|
|
62
|
+
}
|
|
63
|
+
if !strings.Contains(text, "| next_action | continue slice S-2 |") {
|
|
64
|
+
t.Fatalf("next step not converted to canonical spelling:\n%s", text)
|
|
65
|
+
}
|
|
66
|
+
if strings.Contains(text, "- Next step:") {
|
|
67
|
+
t.Fatalf("legacy next step bullet survived:\n%s", text)
|
|
68
|
+
}
|
|
69
|
+
if !strings.Contains(text, "| schema | 3 |") {
|
|
70
|
+
t.Fatalf("schema row missing:\n%s", text)
|
|
71
|
+
}
|
|
72
|
+
if !strings.Contains(text, "# Status\n\nMinting middleware landed.") {
|
|
73
|
+
t.Fatalf("prose not preserved:\n%s", text)
|
|
74
|
+
}
|
|
75
|
+
// Required artifacts for build exist as empty stubs.
|
|
76
|
+
for _, name := range []string{"spec.md", "brief.md", "eng-review.md", "test-plan.md"} {
|
|
77
|
+
info, err := os.Stat(filepath.Join(workspace, name))
|
|
78
|
+
if err != nil {
|
|
79
|
+
t.Fatalf("stub %s: %v", name, err)
|
|
80
|
+
}
|
|
81
|
+
if info.Size() != 0 {
|
|
82
|
+
t.Fatalf("stub %s synthesized content", name)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Idempotent: a second run is a no-op.
|
|
86
|
+
stdout.Reset()
|
|
87
|
+
if code := Migrate(root, []string{"feature"}, &stdout, &stderr); code != 0 {
|
|
88
|
+
t.Fatalf("second run code=%d stderr=%q", code, stderr.String())
|
|
89
|
+
}
|
|
90
|
+
if !strings.Contains(stdout.String(), "nothing to do") {
|
|
91
|
+
t.Fatalf("second run should be a no-op, got %q", stdout.String())
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func TestMigrateUnknownPhaseAsksAndWritesNothing(t *testing.T) {
|
|
96
|
+
root, workspace := seedPreV5Workspace(t, "research")
|
|
97
|
+
var stdout, stderr strings.Builder
|
|
98
|
+
if code := Migrate(root, []string{"feature"}, &stdout, &stderr); code != 3 {
|
|
99
|
+
t.Fatalf("code=%d want 3", code)
|
|
100
|
+
}
|
|
101
|
+
if !strings.Contains(stderr.String(), "question phase:") || !strings.Contains(stderr.String(), "nothing written") {
|
|
102
|
+
t.Fatalf("stderr=%q", stderr.String())
|
|
103
|
+
}
|
|
104
|
+
body, _ := os.ReadFile(filepath.Join(workspace, "state.md"))
|
|
105
|
+
if !strings.HasPrefix(string(body), "- Phase: research\n") {
|
|
106
|
+
t.Fatalf("state.md changed on refusal: %q", body)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Answering the question on rerun proceeds.
|
|
110
|
+
if code := Migrate(root, []string{"feature", "--answer", "phase=build"}, &stdout, &stderr); code != 0 {
|
|
111
|
+
t.Fatalf("answer rerun code=%d stderr=%q", code, stderr.String())
|
|
112
|
+
}
|
|
113
|
+
body, _ = os.ReadFile(filepath.Join(workspace, "state.md"))
|
|
114
|
+
if !strings.Contains(string(body), "| phase | build |") || !strings.Contains(string(body), "| schema | 3 |") {
|
|
115
|
+
t.Fatalf("answer rerun did not migrate:\n%s", body)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// A done workspace migrates like any other: it is fully artifacted (zero
|
|
120
|
+
// stubs), and normalizing it unblocks `state close`, which refuses pre-v5
|
|
121
|
+
// workspaces. A special no-op would strand done workspaces between migrate
|
|
122
|
+
// and close.
|
|
123
|
+
func TestMigrateDoneWorkspaceNormalizesWithoutStubs(t *testing.T) {
|
|
124
|
+
root, workspace := seedPreV5Workspace(t, "done")
|
|
125
|
+
var stdout, stderr strings.Builder
|
|
126
|
+
if code := Migrate(root, []string{"feature"}, &stdout, &stderr); code != 0 {
|
|
127
|
+
t.Fatalf("code=%d stderr=%q", code, stderr.String())
|
|
128
|
+
}
|
|
129
|
+
body, _ := os.ReadFile(filepath.Join(workspace, "state.md"))
|
|
130
|
+
if !strings.Contains(string(body), "| schema | 3 |") {
|
|
131
|
+
t.Fatalf("done workspace not normalized:\n%s", body)
|
|
132
|
+
}
|
|
133
|
+
if !strings.Contains(stdout.String(), "normalized to schema 3") {
|
|
134
|
+
t.Fatalf("stdout=%q", stdout.String())
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
func TestMigrateCurrentAndNewerSchemaAreRefused(t *testing.T) {
|
|
139
|
+
root, workspace := seedPreV5Workspace(t, "build")
|
|
140
|
+
if err := os.WriteFile(filepath.Join(workspace, "state.md"), []byte("| phase | build |\n| schema | 3 |\n"), 0o644); err != nil {
|
|
141
|
+
t.Fatal(err)
|
|
142
|
+
}
|
|
143
|
+
var stdout, stderr strings.Builder
|
|
144
|
+
if code := Migrate(root, []string{"feature"}, &stdout, &stderr); code != 0 || !strings.Contains(stdout.String(), "nothing to do") {
|
|
145
|
+
t.Fatalf("current schema: code=%d stdout=%q", code, stdout.String())
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if err := os.WriteFile(filepath.Join(workspace, "state.md"), []byte("| phase | build |\n| schema | 9 |\n"), 0o644); err != nil {
|
|
149
|
+
t.Fatal(err)
|
|
150
|
+
}
|
|
151
|
+
if code := Migrate(root, []string{"feature"}, &stdout, &stderr); code != 3 || !strings.Contains(stderr.String(), "upgrade devrites") {
|
|
152
|
+
t.Fatalf("newer schema: code=%d stderr=%q", code, stderr.String())
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -35,6 +35,9 @@ func Resolve(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
35
35
|
if !isFile(sfile) {
|
|
36
36
|
return fail(stderr, "state.md missing at "+sfile, 2)
|
|
37
37
|
}
|
|
38
|
+
if err := state.RequireWorkspaceSchema(root, slug); err != nil {
|
|
39
|
+
return fail(stderr, err.Error(), 3)
|
|
40
|
+
}
|
|
38
41
|
|
|
39
42
|
var mode, qid, payload string
|
|
40
43
|
switch first := argAt(args, 0); first {
|
|
@@ -55,6 +55,6 @@ func resolveWorkspace(t *testing.T, questions string) string {
|
|
|
55
55
|
root := t.TempDir()
|
|
56
56
|
testutil.WriteFile(t, filepath.Join(root, "ACTIVE"), "feat\n")
|
|
57
57
|
testutil.WriteFile(t, filepath.Join(root, "work", "feat", "questions.md"), questions)
|
|
58
|
-
testutil.WriteFile(t, filepath.Join(root, "work", "feat", "state.md"), "- Status: running\n- Next step: continue\n")
|
|
58
|
+
testutil.WriteFile(t, filepath.Join(root, "work", "feat", "state.md"), "- Status: running\n- Next step: continue\n- Schema: 3\n")
|
|
59
59
|
return root
|
|
60
60
|
}
|
|
@@ -348,6 +348,7 @@ func TestSecretScanBlocksHighSeverityTouchedFile(t *testing.T) {
|
|
|
348
348
|
root := filepath.Join(project, ".devrites")
|
|
349
349
|
testutil.WriteFile(t, filepath.Join(root, "ACTIVE"), "leak\n")
|
|
350
350
|
testutil.WriteFile(t, filepath.Join(root, "work", "leak", "touched-files.md"), candidateTestManifest(candidateTestRow{state: "present", path: "secrets.txt", slice: "S-1", reason: "Credential check."}))
|
|
351
|
+
testutil.WriteFile(t, filepath.Join(root, "work", "leak", "state.md"), "| schema | 3 |\n")
|
|
351
352
|
secret := "ghp_" + strings.Repeat("f", 32)
|
|
352
353
|
testutil.WriteFile(t, filepath.Join(project, "secrets.txt"), secret+"\n")
|
|
353
354
|
var out, err bytes.Buffer
|
|
@@ -259,7 +259,7 @@ func TestObserveSummaryExposesTaskGraphProblems(t *testing.T) {
|
|
|
259
259
|
if err := os.MkdirAll(workspace, 0o755); err != nil {
|
|
260
260
|
t.Fatal(err)
|
|
261
261
|
}
|
|
262
|
-
if err := os.WriteFile(filepath.Join(workspace, "state.md"), []byte("| phase | define |\n"), 0o644); err != nil {
|
|
262
|
+
if err := os.WriteFile(filepath.Join(workspace, "state.md"), []byte("| phase | define |\n| schema | 3 |\n"), 0o644); err != nil {
|
|
263
263
|
t.Fatal(err)
|
|
264
264
|
}
|
|
265
265
|
body := `# Tasks
|
|
@@ -308,7 +308,7 @@ func TestObserveSummaryExposesProblemsWhenNoSliceHeaders(t *testing.T) {
|
|
|
308
308
|
if err := os.MkdirAll(workspace, 0o755); err != nil {
|
|
309
309
|
t.Fatal(err)
|
|
310
310
|
}
|
|
311
|
-
if err := os.WriteFile(filepath.Join(workspace, "state.md"), []byte("| phase | define |\n"), 0o644); err != nil {
|
|
311
|
+
if err := os.WriteFile(filepath.Join(workspace, "state.md"), []byte("| phase | define |\n| schema | 3 |\n"), 0o644); err != nil {
|
|
312
312
|
t.Fatal(err)
|
|
313
313
|
}
|
|
314
314
|
if err := os.WriteFile(filepath.Join(workspace, "tasks.md"), []byte("# Tasks\n\n- do the work\n"), 0o644); err != nil {
|
|
@@ -9,6 +9,16 @@ import (
|
|
|
9
9
|
"strings"
|
|
10
10
|
)
|
|
11
11
|
|
|
12
|
+
// Test seams: indirection over the real primitives so discriminating tests
|
|
13
|
+
// can inject transient failures without touching the filesystem.
|
|
14
|
+
var (
|
|
15
|
+
writeLease = WriteLease
|
|
16
|
+
removeWorktree = worktreeRemove
|
|
17
|
+
warnf = func(format string, args ...any) {
|
|
18
|
+
fmt.Fprintf(os.Stderr, "warning: parallel: "+format+"\n", args...)
|
|
19
|
+
}
|
|
20
|
+
)
|
|
21
|
+
|
|
12
22
|
// CreateOpts configures parallel worktree creation.
|
|
13
23
|
type CreateOpts struct {
|
|
14
24
|
Root string
|
|
@@ -54,13 +64,16 @@ func Create(opts CreateOpts) (*Lease, error) {
|
|
|
54
64
|
createdBranches := make([]string, 0, len(opts.Slices))
|
|
55
65
|
cleanupPartial := func() {
|
|
56
66
|
for _, p := range createdPaths {
|
|
57
|
-
|
|
67
|
+
if err := removeWorktree(opts.Root, p); err != nil {
|
|
68
|
+
warnf("worktree cleanup %s: %v", p, err)
|
|
69
|
+
}
|
|
58
70
|
}
|
|
59
71
|
for _, b := range createdBranches {
|
|
60
|
-
|
|
72
|
+
if err := deleteBranch(opts.Root, b); err != nil {
|
|
73
|
+
warnf("branch cleanup %s: %v", b, err)
|
|
74
|
+
}
|
|
61
75
|
}
|
|
62
76
|
}
|
|
63
|
-
|
|
64
77
|
slices := make([]LeaseSlice, 0, len(opts.Slices))
|
|
65
78
|
for _, sp := range opts.Slices {
|
|
66
79
|
if err := validateSliceID(sp.ID); err != nil {
|
|
@@ -252,12 +265,28 @@ func Integrate(opts IntegrateOpts) (tip string, lease *Lease, err error) {
|
|
|
252
265
|
}
|
|
253
266
|
|
|
254
267
|
fail := func(reason error) (string, *Lease, error) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
268
|
+
if err := removeWorktree(opts.Root, stageWT); err != nil {
|
|
269
|
+
warnf("staging worktree cleanup: %v", err)
|
|
270
|
+
}
|
|
271
|
+
if err := deleteBranch(opts.Root, ibranch); err != nil {
|
|
272
|
+
warnf("integrate branch cleanup: %v", err)
|
|
273
|
+
}
|
|
274
|
+
if _, err := git(opts.Root, "worktree", "prune"); err != nil {
|
|
275
|
+
warnf("worktree prune: %v", err)
|
|
276
|
+
}
|
|
258
277
|
lease.Status = StatusIntegrateFailed
|
|
259
|
-
|
|
260
|
-
|
|
278
|
+
if err := writeLease(leasePath, lease); err != nil {
|
|
279
|
+
// The integrate-failed transition must survive a transient write
|
|
280
|
+
// failure: a stale running lease still blocks create and stays
|
|
281
|
+
// retryable, but the honest state must be recorded when possible.
|
|
282
|
+
if retryErr := writeLease(leasePath, lease); retryErr != nil {
|
|
283
|
+
warnf("lease %s still status=%s (write failed twice: %v)",
|
|
284
|
+
leasePath, StatusRunning, retryErr)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if err := ensureControlAtBase(opts.Root, base, true); err != nil {
|
|
288
|
+
warnf("control reset to base: %v", err)
|
|
289
|
+
}
|
|
261
290
|
return "", lease, reason
|
|
262
291
|
}
|
|
263
292
|
|
|
@@ -313,8 +342,18 @@ func Integrate(opts IntegrateOpts) (tip string, lease *Lease, err error) {
|
|
|
313
342
|
if err != nil {
|
|
314
343
|
return fail(err)
|
|
315
344
|
}
|
|
316
|
-
|
|
317
|
-
|
|
345
|
+
if err := removeWorktree(opts.Root, stageWT); err != nil {
|
|
346
|
+
if _, statErr := os.Stat(stageWT); statErr == nil {
|
|
347
|
+
// The directory is genuinely stuck; failing now keeps the lease in
|
|
348
|
+
// integrate-failed and avoids compounding into un-cleanable branch
|
|
349
|
+
// state. Retrying integrate re-attempts the same removals.
|
|
350
|
+
return fail(fmt.Errorf("staging worktree could not be removed; retry integrate after cleanup: %w", err))
|
|
351
|
+
}
|
|
352
|
+
warnf("staging worktree cleanup: %v", err)
|
|
353
|
+
}
|
|
354
|
+
if _, err := git(opts.Root, "worktree", "prune"); err != nil {
|
|
355
|
+
warnf("worktree prune: %v", err)
|
|
356
|
+
}
|
|
318
357
|
|
|
319
358
|
if opts.ApplyToControl {
|
|
320
359
|
if err := mergeFFOnly(opts.Root, tip); err != nil {
|
|
@@ -361,17 +400,29 @@ func Cleanup(repoRoot, slug string, force bool) error {
|
|
|
361
400
|
}
|
|
362
401
|
for _, sl := range lease.Slices {
|
|
363
402
|
if sl.WorktreePath != "" {
|
|
364
|
-
|
|
365
|
-
|
|
403
|
+
if err := removeWorktree(repoRoot, sl.WorktreePath); err != nil {
|
|
404
|
+
warnf("worktree cleanup %s: %v", sl.WorktreePath, err)
|
|
405
|
+
}
|
|
406
|
+
if err := os.RemoveAll(sl.WorktreePath); err != nil {
|
|
407
|
+
warnf("worktree dir cleanup %s: %v", sl.WorktreePath, err)
|
|
408
|
+
}
|
|
366
409
|
}
|
|
367
410
|
if sl.Branch != "" {
|
|
368
|
-
|
|
411
|
+
if err := deleteBranch(repoRoot, sl.Branch); err != nil {
|
|
412
|
+
warnf("branch cleanup %s: %v", sl.Branch, err)
|
|
413
|
+
}
|
|
369
414
|
}
|
|
370
415
|
}
|
|
371
416
|
ibranch := IntegrateBranchName(slug, lease.BatchID)
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
417
|
+
if err := deleteBranch(repoRoot, ibranch); err != nil {
|
|
418
|
+
warnf("integrate branch cleanup %s: %v", ibranch, err)
|
|
419
|
+
}
|
|
420
|
+
if err := os.RemoveAll(filepath.Join(ScratchRoot(repoRoot), lease.BatchID)); err != nil {
|
|
421
|
+
warnf("scratch cleanup: %v", err)
|
|
422
|
+
}
|
|
423
|
+
if _, err := git(repoRoot, "worktree", "prune"); err != nil {
|
|
424
|
+
warnf("worktree prune: %v", err)
|
|
425
|
+
}
|
|
375
426
|
return ClearLease(leasePath)
|
|
376
427
|
}
|
|
377
428
|
|