feat(agentic): archive stale completed plans
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
886461ca28
commit
09aa19afde
3 changed files with 99 additions and 10 deletions
|
|
@ -57,8 +57,8 @@ func (s *PrepSubsystem) registerCommands(ctx context.Context) {
|
|||
c.Command("lang/list", core.Command{Description: "List supported language identifiers", Action: s.cmdLangList})
|
||||
c.Command("epic", core.Command{Description: "Create sub-issues from an epic plan", Action: s.cmdEpic})
|
||||
c.Command("agentic:epic", core.Command{Description: "Create sub-issues from an epic plan", Action: s.cmdEpic})
|
||||
c.Command("plan-cleanup", core.Command{Description: "Permanently delete archived plans past the retention period", Action: s.cmdPlanCleanup})
|
||||
c.Command("agentic:plan-cleanup", core.Command{Description: "Permanently delete archived plans past the retention period", Action: s.cmdPlanCleanup})
|
||||
c.Command("plan-cleanup", core.Command{Description: "Archive old completed plans and delete stale archives past the retention period", Action: s.cmdPlanCleanup})
|
||||
c.Command("agentic:plan-cleanup", core.Command{Description: "Archive old completed plans and delete stale archives past the retention period", Action: s.cmdPlanCleanup})
|
||||
c.Command("pr-manage", core.Command{Description: "Manage open PRs (merge, close, review)", Action: s.cmdPRManage})
|
||||
c.Command("agentic:pr-manage", core.Command{Description: "Manage open PRs (merge, close, review)", Action: s.cmdPRManage})
|
||||
c.Command("review-queue", core.Command{Description: "Process the CodeRabbit review queue", Action: s.cmdReviewQueue})
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ type PlanCleanupOutput struct {
|
|||
Success bool `json:"success"`
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
DryRun bool `json:"dry_run,omitempty"`
|
||||
Archived int `json:"archived,omitempty"`
|
||||
Deleted int `json:"deleted,omitempty"`
|
||||
Matched int `json:"matched,omitempty"`
|
||||
Cutoff string `json:"cutoff,omitempty"`
|
||||
|
|
@ -44,16 +45,25 @@ func (s *PrepSubsystem) cmdPlanCleanup(options core.Options) core.Result {
|
|||
}
|
||||
|
||||
if output.Matched == 0 {
|
||||
core.Print(nil, "No archived plans found past the retention period.")
|
||||
core.Print(nil, "No plans found past the retention period.")
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
if output.DryRun {
|
||||
core.Print(nil, "DRY RUN: %d archived plan(s) would be permanently deleted (archived before %s).", output.Matched, output.Cutoff)
|
||||
core.Print(nil, "DRY RUN: %d plan(s) would be archived or deleted (cutoff %s).", output.Matched, output.Cutoff)
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
core.Print(nil, "Permanently deleted %d archived plan(s) archived before %s.", output.Deleted, output.Cutoff)
|
||||
if output.Archived > 0 && output.Deleted > 0 {
|
||||
core.Print(nil, "Archived %d plan(s) and permanently deleted %d stale archive(s) before %s.", output.Archived, output.Deleted, output.Cutoff)
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
if output.Archived > 0 {
|
||||
core.Print(nil, "Archived %d plan(s) before %s.", output.Archived, output.Cutoff)
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
core.Print(nil, "Permanently deleted %d stale archive(s) before %s.", output.Deleted, output.Cutoff)
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
|
|
@ -97,12 +107,31 @@ func (s *PrepSubsystem) planCleanup(options core.Options) core.Result {
|
|||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
archived := 0
|
||||
if output.DryRun {
|
||||
for _, candidate := range candidates {
|
||||
if planRetentionShouldArchive(candidate.plan.Status) {
|
||||
archived++
|
||||
}
|
||||
}
|
||||
output.Archived = archived
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
deleted := 0
|
||||
for _, candidate := range candidates {
|
||||
if planRetentionShouldArchive(candidate.plan.Status) {
|
||||
_, archiveErr := archivePlanResult(PlanDeleteInput{
|
||||
ID: candidate.plan.ID,
|
||||
Reason: "plan retention cleanup",
|
||||
}, "id is required", "planCleanup")
|
||||
if archiveErr != nil {
|
||||
return core.Result{Value: archiveErr, OK: false}
|
||||
}
|
||||
archived++
|
||||
continue
|
||||
}
|
||||
|
||||
if r := fs.Delete(candidate.path); !r.OK {
|
||||
err, _ := r.Value.(error)
|
||||
if err == nil {
|
||||
|
|
@ -113,13 +142,15 @@ func (s *PrepSubsystem) planCleanup(options core.Options) core.Result {
|
|||
deleted++
|
||||
}
|
||||
|
||||
output.Archived = archived
|
||||
output.Deleted = deleted
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
type planRetentionCandidate struct {
|
||||
path string
|
||||
archivedAt time.Time
|
||||
plan *Plan
|
||||
retainedAt time.Time
|
||||
}
|
||||
|
||||
func planRetentionCandidates(dir string, cutoff time.Time) []planRetentionCandidate {
|
||||
|
|
@ -138,24 +169,48 @@ func planRetentionCandidates(dir string, cutoff time.Time) []planRetentionCandid
|
|||
if !ok || plan == nil {
|
||||
continue
|
||||
}
|
||||
if plan.Status != "archived" {
|
||||
if !planRetentionShouldArchive(plan.Status) && plan.Status != "archived" {
|
||||
continue
|
||||
}
|
||||
|
||||
archivedAt := planArchivedAt(path, plan)
|
||||
if archivedAt.IsZero() || !archivedAt.Before(cutoff) {
|
||||
retainedAt := planRetentionAt(path, plan)
|
||||
if retainedAt.IsZero() || !retainedAt.Before(cutoff) {
|
||||
continue
|
||||
}
|
||||
|
||||
candidates = append(candidates, planRetentionCandidate{
|
||||
path: path,
|
||||
archivedAt: archivedAt,
|
||||
plan: plan,
|
||||
retainedAt: retainedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return candidates
|
||||
}
|
||||
|
||||
func planRetentionShouldArchive(status string) bool {
|
||||
switch status {
|
||||
case "approved", "completed":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func planRetentionAt(path string, plan *Plan) time.Time {
|
||||
if plan == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
if !plan.ArchivedAt.IsZero() {
|
||||
return plan.ArchivedAt
|
||||
}
|
||||
if !plan.UpdatedAt.IsZero() {
|
||||
return plan.UpdatedAt
|
||||
}
|
||||
return planArchivedAt(path, plan)
|
||||
}
|
||||
|
||||
func planArchivedAt(path string, plan *Plan) time.Time {
|
||||
if plan != nil && !plan.ArchivedAt.IsZero() {
|
||||
return plan.ArchivedAt
|
||||
|
|
|
|||
|
|
@ -59,6 +59,40 @@ func TestPlanRetention_PlanCleanup_Good_DeletesExpiredArchivedPlans(t *testing.T
|
|||
assert.True(t, fs.Exists(core.JoinPath(PlansRoot(), "active-plan-abc123.json")))
|
||||
}
|
||||
|
||||
func TestPlanRetention_PlanCleanup_Good_ArchivesExpiredCompletedPlans(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("CORE_WORKSPACE", dir)
|
||||
|
||||
s := newTestPrep(t)
|
||||
|
||||
plan := &Plan{
|
||||
ID: "completed-plan-abc123",
|
||||
Title: "Completed Plan",
|
||||
Status: "approved",
|
||||
Objective: "Archive me",
|
||||
UpdatedAt: time.Now().AddDate(0, 0, -100),
|
||||
}
|
||||
|
||||
_, err := writePlan(PlansRoot(), plan)
|
||||
require.NoError(t, err)
|
||||
|
||||
result := s.planCleanup(core.NewOptions(core.Option{Key: "days", Value: 90}))
|
||||
require.True(t, result.OK)
|
||||
|
||||
output, ok := result.Value.(PlanCleanupOutput)
|
||||
require.True(t, ok)
|
||||
assert.True(t, output.Success)
|
||||
assert.Equal(t, 1, output.Archived)
|
||||
assert.Equal(t, 0, output.Deleted)
|
||||
assert.Equal(t, 1, output.Matched)
|
||||
|
||||
updated, err := readPlan(PlansRoot(), plan.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "archived", updated.Status)
|
||||
assert.False(t, updated.ArchivedAt.IsZero())
|
||||
assert.True(t, fs.Exists(core.JoinPath(PlansRoot(), "completed-plan-abc123.json")))
|
||||
}
|
||||
|
||||
func TestPlanRetention_PlanCleanup_Bad_DryRunKeepsFiles(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("CORE_WORKSPACE", dir)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue