refactor: rename Ephemeral → Wisp (Steam Engine metaphor)
Wisp = ephemeral vapor produced by the Steam Engine (Gas Town). This aligns with the metaphor: - Claude = Fire - Claude Code = Steam - Gas Town = Steam Engine - Wisps = ephemeral vapor it produces Changes: - types.Issue.Ephemeral → types.Issue.Wisp - types.IssueFilter.Ephemeral → types.IssueFilter.Wisp - JSON field: "ephemeral" → "wisp" - CLI flag: --ephemeral → --wisp (bd cleanup) - All tests updated Note: SQLite column remains "ephemeral" (no migration needed). This is a breaking change for JSON consumers using 0.33.0. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -704,20 +704,20 @@ func flushToJSONLWithState(state flushState) {
|
||||
}
|
||||
|
||||
// Convert map to slice (will be sorted by writeJSONLAtomic)
|
||||
// Filter out ephemeral issues - they should never be exported to JSONL (bd-687g)
|
||||
// Ephemeral issues exist only in SQLite and are shared via .beads/redirect, not JSONL.
|
||||
// Filter out wisps - they should never be exported to JSONL (bd-687g)
|
||||
// Wisps exist only in SQLite and are shared via .beads/redirect, not JSONL.
|
||||
// This prevents "zombie" issues that resurrect after mol squash deletes them.
|
||||
issues := make([]*types.Issue, 0, len(issueMap))
|
||||
ephemeralSkipped := 0
|
||||
wispsSkipped := 0
|
||||
for _, issue := range issueMap {
|
||||
if issue.Ephemeral {
|
||||
ephemeralSkipped++
|
||||
if issue.Wisp {
|
||||
wispsSkipped++
|
||||
continue
|
||||
}
|
||||
issues = append(issues, issue)
|
||||
}
|
||||
if ephemeralSkipped > 0 {
|
||||
debug.Logf("auto-flush: filtered %d ephemeral issues from export", ephemeralSkipped)
|
||||
if wispsSkipped > 0 {
|
||||
debug.Logf("auto-flush: filtered %d wisps from export", wispsSkipped)
|
||||
}
|
||||
|
||||
// Filter issues by prefix in multi-repo mode for non-primary repos (fixes GH #437)
|
||||
|
||||
@@ -44,8 +44,8 @@ Delete all closed issues and prune tombstones:
|
||||
Delete issues closed more than 30 days ago:
|
||||
bd cleanup --older-than 30 --force
|
||||
|
||||
Delete only closed ephemeral issues (transient messages):
|
||||
bd cleanup --ephemeral --force
|
||||
Delete only closed wisps (transient molecules):
|
||||
bd cleanup --wisp --force
|
||||
|
||||
Preview what would be deleted/pruned:
|
||||
bd cleanup --dry-run
|
||||
@@ -69,7 +69,7 @@ SEE ALSO:
|
||||
cascade, _ := cmd.Flags().GetBool("cascade")
|
||||
olderThanDays, _ := cmd.Flags().GetInt("older-than")
|
||||
hardDelete, _ := cmd.Flags().GetBool("hard")
|
||||
ephemeralOnly, _ := cmd.Flags().GetBool("ephemeral")
|
||||
wispOnly, _ := cmd.Flags().GetBool("wisp")
|
||||
|
||||
// Calculate custom TTL for --hard mode
|
||||
// When --hard is set, use --older-than days as the tombstone TTL cutoff
|
||||
@@ -115,10 +115,10 @@ SEE ALSO:
|
||||
filter.ClosedBefore = &cutoffTime
|
||||
}
|
||||
|
||||
// Add ephemeral filter if specified (bd-kwro.9)
|
||||
if ephemeralOnly {
|
||||
ephemeralTrue := true
|
||||
filter.Ephemeral = &ephemeralTrue
|
||||
// Add wisp filter if specified (bd-kwro.9)
|
||||
if wispOnly {
|
||||
wispTrue := true
|
||||
filter.Wisp = &wispTrue
|
||||
}
|
||||
|
||||
// Get all closed issues matching filter
|
||||
@@ -153,17 +153,17 @@ SEE ALSO:
|
||||
if olderThanDays > 0 {
|
||||
result["filter"] = fmt.Sprintf("older than %d days", olderThanDays)
|
||||
}
|
||||
if ephemeralOnly {
|
||||
result["ephemeral"] = true
|
||||
if wispOnly {
|
||||
result["wisp"] = true
|
||||
}
|
||||
output, _ := json.MarshalIndent(result, "", " ")
|
||||
fmt.Println(string(output))
|
||||
} else {
|
||||
msg := "No closed issues to delete"
|
||||
if ephemeralOnly && olderThanDays > 0 {
|
||||
msg = fmt.Sprintf("No closed ephemeral issues older than %d days to delete", olderThanDays)
|
||||
} else if ephemeralOnly {
|
||||
msg = "No closed ephemeral issues to delete"
|
||||
if wispOnly && olderThanDays > 0 {
|
||||
msg = fmt.Sprintf("No closed wisps older than %d days to delete", olderThanDays)
|
||||
} else if wispOnly {
|
||||
msg = "No closed wisps to delete"
|
||||
} else if olderThanDays > 0 {
|
||||
msg = fmt.Sprintf("No closed issues older than %d days to delete", olderThanDays)
|
||||
}
|
||||
@@ -181,8 +181,8 @@ SEE ALSO:
|
||||
// Show preview
|
||||
if !force && !dryRun {
|
||||
issueType := "closed"
|
||||
if ephemeralOnly {
|
||||
issueType = "closed ephemeral"
|
||||
if wispOnly {
|
||||
issueType = "closed wisp"
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Would delete %d %s issue(s). Use --force to confirm or --dry-run to preview.\n", len(issueIDs), issueType)
|
||||
os.Exit(1)
|
||||
@@ -190,8 +190,8 @@ SEE ALSO:
|
||||
|
||||
if !jsonOutput {
|
||||
issueType := "closed"
|
||||
if ephemeralOnly {
|
||||
issueType = "closed ephemeral"
|
||||
if wispOnly {
|
||||
issueType = "closed wisp"
|
||||
}
|
||||
if olderThanDays > 0 {
|
||||
fmt.Printf("Found %d %s issue(s) older than %d days\n", len(closedIssues), issueType, olderThanDays)
|
||||
@@ -255,6 +255,6 @@ func init() {
|
||||
cleanupCmd.Flags().Bool("cascade", false, "Recursively delete all dependent issues")
|
||||
cleanupCmd.Flags().Int("older-than", 0, "Only delete issues closed more than N days ago (0 = all closed issues)")
|
||||
cleanupCmd.Flags().Bool("hard", false, "Bypass tombstone TTL safety; use --older-than days as cutoff")
|
||||
cleanupCmd.Flags().Bool("ephemeral", false, "Only delete closed ephemeral issues (transient messages)")
|
||||
cleanupCmd.Flags().Bool("wisp", false, "Only delete closed wisps (transient molecules)")
|
||||
rootCmd.AddCommand(cleanupCmd)
|
||||
}
|
||||
|
||||
@@ -346,11 +346,11 @@ Examples:
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out ephemeral issues - they should never be exported to JSONL (bd-687g)
|
||||
// Ephemeral issues exist only in SQLite and are shared via .beads/redirect, not JSONL.
|
||||
// Filter out wisps - they should never be exported to JSONL (bd-687g)
|
||||
// Wisps exist only in SQLite and are shared via .beads/redirect, not JSONL.
|
||||
filtered := make([]*types.Issue, 0, len(issues))
|
||||
for _, issue := range issues {
|
||||
if !issue.Ephemeral {
|
||||
if !issue.Wisp {
|
||||
filtered = append(filtered, issue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,10 +292,10 @@ var versionChanges = []VersionChange{
|
||||
Version: "0.33.0",
|
||||
Date: "2025-12-21",
|
||||
Changes: []string{
|
||||
"NEW: Ephemeral molecules (bd-2vh3) - bd mol spawn creates ephemeral issues by default",
|
||||
"NEW: Ephemeral issues live only in SQLite, never export to JSONL (prevents zombie resurrection)",
|
||||
"NEW: --persistent flag on bd mol spawn to opt out of ephemeral spawning",
|
||||
"NEW: bd mol squash compresses ephemeral children into digest issue",
|
||||
"NEW: Wisp molecules (bd-2vh3) - bd mol spawn creates wisp issues by default",
|
||||
"NEW: Wisp issues live only in SQLite, never export to JSONL (prevents zombie resurrection)",
|
||||
"NEW: --persistent flag on bd mol spawn to opt out of wisp spawning",
|
||||
"NEW: bd mol squash compresses wisp children into digest issue",
|
||||
"NEW: --summary flag on bd mol squash for agent-provided AI summaries",
|
||||
"FIX: DeleteIssue now cascades to comments table (bd-687g)",
|
||||
},
|
||||
@@ -391,7 +391,7 @@ var versionChanges = []VersionChange{
|
||||
Date: "2025-12-16",
|
||||
Changes: []string{
|
||||
"bd setup droid (GH#598) - Factory.ai (Droid) IDE support",
|
||||
"Messaging schema fields (bd-kwro.1) - New 'message' issue type, sender/ephemeral/replies_to/relates_to/duplicate_of/superseded_by fields",
|
||||
"Messaging schema fields (bd-kwro.1) - New 'message' issue type, sender/wisp/replies_to/relates_to/duplicate_of/superseded_by fields",
|
||||
"New dependency types: replies-to, relates-to, duplicates, supersedes",
|
||||
"Windows build fixes (GH#585) - gosec lint errors resolved",
|
||||
"Issue ID prefix extraction fix - Word-like suffixes now parse correctly",
|
||||
|
||||
@@ -283,7 +283,7 @@ func bondProtoMol(ctx context.Context, s storage.Storage, proto, mol *types.Issu
|
||||
return nil, fmt.Errorf("missing required variables: %s (use --var)", strings.Join(missingVars, ", "))
|
||||
}
|
||||
|
||||
// Spawn the proto (ephemeral by default for molecule execution - bd-2vh3)
|
||||
// Spawn the proto (wisp by default for molecule execution - bd-2vh3)
|
||||
spawnResult, err := spawnMolecule(ctx, s, subgraph, vars, "", actorName, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("spawning proto: %w", err)
|
||||
|
||||
@@ -89,7 +89,7 @@ func runMolRun(cmd *cobra.Command, args []string) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Spawn the molecule with actor as assignee (ephemeral for cleanup - bd-2vh3)
|
||||
// Spawn the molecule with actor as assignee (wisp for cleanup - bd-2vh3)
|
||||
result, err := spawnMolecule(ctx, store, subgraph, vars, actor, actor, true)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error spawning molecule: %v\n", err)
|
||||
|
||||
@@ -182,9 +182,9 @@ func runMolSpawn(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
|
||||
// Clone the subgraph (spawn the molecule)
|
||||
// Spawned molecules are ephemeral by default (bd-2vh3) - use --persistent to opt out
|
||||
ephemeral := !persistent
|
||||
result, err := spawnMolecule(ctx, store, subgraph, vars, assignee, actor, ephemeral)
|
||||
// Spawned molecules are wisps by default (bd-2vh3) - use --persistent to opt out
|
||||
wisp := !persistent
|
||||
result, err := spawnMolecule(ctx, store, subgraph, vars, assignee, actor, wisp)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error spawning molecule: %v\n", err)
|
||||
os.Exit(1)
|
||||
@@ -236,7 +236,7 @@ func init() {
|
||||
molSpawnCmd.Flags().String("assignee", "", "Assign the root issue to this agent/user")
|
||||
molSpawnCmd.Flags().StringSlice("attach", []string{}, "Proto to attach after spawning (repeatable)")
|
||||
molSpawnCmd.Flags().String("attach-type", types.BondTypeSequential, "Bond type for attachments: sequential, parallel, or conditional")
|
||||
molSpawnCmd.Flags().Bool("persistent", false, "Create non-ephemeral issues (default: ephemeral for cleanup)")
|
||||
molSpawnCmd.Flags().Bool("persistent", false, "Create non-wisp issues (default: wisp for cleanup)")
|
||||
|
||||
molCmd.AddCommand(molSpawnCmd)
|
||||
}
|
||||
|
||||
@@ -18,17 +18,17 @@ import (
|
||||
var molSquashCmd = &cobra.Command{
|
||||
Use: "squash <molecule-id>",
|
||||
Short: "Compress molecule execution into a digest",
|
||||
Long: `Squash a molecule's ephemeral children into a single digest issue.
|
||||
Long: `Squash a molecule's wisp children into a single digest issue.
|
||||
|
||||
This command collects all ephemeral child issues of a molecule, generates
|
||||
a summary digest, and optionally deletes the ephemeral children.
|
||||
This command collects all wisp child issues of a molecule, generates
|
||||
a summary digest, and optionally deletes the wisps.
|
||||
|
||||
The squash operation:
|
||||
1. Loads the molecule and all its children
|
||||
2. Filters to only ephemeral issues
|
||||
2. Filters to only wisps (ephemeral vapor from the Steam Engine)
|
||||
3. Generates a digest (summary of work done)
|
||||
4. Creates a non-ephemeral digest issue
|
||||
5. Deletes the ephemeral children (unless --keep-children)
|
||||
4. Creates a permanent digest issue
|
||||
5. Deletes the wisps (unless --keep-children)
|
||||
|
||||
AGENT INTEGRATION:
|
||||
Use --summary to provide an AI-generated summary. This keeps bd as a pure
|
||||
@@ -36,7 +36,7 @@ tool - the calling agent (Gas Town polecat, Claude Code, etc.) is responsible
|
||||
for generating intelligent summaries. Without --summary, a basic concatenation
|
||||
of child issue content is used.
|
||||
|
||||
This is part of the ephemeral workflow: spawn creates ephemeral issues,
|
||||
This is part of the wisp workflow: spawn creates wisps,
|
||||
execution happens, squash compresses the trace into an outcome.
|
||||
|
||||
Example:
|
||||
@@ -92,39 +92,39 @@ func runMolSquash(cmd *cobra.Command, args []string) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Filter to only ephemeral children (exclude root)
|
||||
var ephemeralChildren []*types.Issue
|
||||
// Filter to only wisp children (exclude root)
|
||||
var wispChildren []*types.Issue
|
||||
for _, issue := range subgraph.Issues {
|
||||
if issue.ID == subgraph.Root.ID {
|
||||
continue // Skip root
|
||||
}
|
||||
if issue.Ephemeral {
|
||||
ephemeralChildren = append(ephemeralChildren, issue)
|
||||
if issue.Wisp {
|
||||
wispChildren = append(wispChildren, issue)
|
||||
}
|
||||
}
|
||||
|
||||
if len(ephemeralChildren) == 0 {
|
||||
if len(wispChildren) == 0 {
|
||||
if jsonOutput {
|
||||
outputJSON(SquashResult{
|
||||
MoleculeID: moleculeID,
|
||||
SquashedCount: 0,
|
||||
})
|
||||
} else {
|
||||
fmt.Printf("No ephemeral children found for molecule %s\n", moleculeID)
|
||||
fmt.Printf("No wisp children found for molecule %s\n", moleculeID)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
fmt.Printf("\nDry run: would squash %d ephemeral children of %s\n\n", len(ephemeralChildren), moleculeID)
|
||||
fmt.Printf("\nDry run: would squash %d wisp children of %s\n\n", len(wispChildren), moleculeID)
|
||||
fmt.Printf("Root: %s\n", subgraph.Root.Title)
|
||||
fmt.Printf("\nEphemeral children to squash:\n")
|
||||
for _, issue := range ephemeralChildren {
|
||||
fmt.Printf("\nWisp children to squash:\n")
|
||||
for _, issue := range wispChildren {
|
||||
status := string(issue.Status)
|
||||
fmt.Printf(" - [%s] %s (%s)\n", status, issue.Title, issue.ID)
|
||||
}
|
||||
fmt.Printf("\nDigest preview:\n")
|
||||
digest := generateDigest(subgraph.Root, ephemeralChildren)
|
||||
digest := generateDigest(subgraph.Root, wispChildren)
|
||||
// Show first 500 chars of digest
|
||||
if len(digest) > 500 {
|
||||
fmt.Printf("%s...\n", digest[:500])
|
||||
@@ -140,7 +140,7 @@ func runMolSquash(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
|
||||
// Perform the squash
|
||||
result, err := squashMolecule(ctx, store, subgraph.Root, ephemeralChildren, keepChildren, summary, actor)
|
||||
result, err := squashMolecule(ctx, store, subgraph.Root, wispChildren, keepChildren, summary, actor)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error squashing molecule: %v\n", err)
|
||||
os.Exit(1)
|
||||
@@ -157,7 +157,7 @@ func runMolSquash(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("%s Squashed molecule: %d children → 1 digest\n", ui.RenderPass("✓"), result.SquashedCount)
|
||||
fmt.Printf(" Digest ID: %s\n", result.DigestID)
|
||||
if result.DeletedCount > 0 {
|
||||
fmt.Printf(" Deleted: %d ephemeral issues\n", result.DeletedCount)
|
||||
fmt.Printf(" Deleted: %d wisps\n", result.DeletedCount)
|
||||
} else if result.KeptChildren {
|
||||
fmt.Printf(" Children preserved (--keep-children)\n")
|
||||
}
|
||||
@@ -235,16 +235,16 @@ func squashMolecule(ctx context.Context, s storage.Storage, root *types.Issue, c
|
||||
digestContent = generateDigest(root, children)
|
||||
}
|
||||
|
||||
// Create digest issue (non-ephemeral)
|
||||
// Create digest issue (permanent, not a wisp)
|
||||
now := time.Now()
|
||||
digestIssue := &types.Issue{
|
||||
Title: fmt.Sprintf("Digest: %s", root.Title),
|
||||
Description: digestContent,
|
||||
Status: types.StatusClosed,
|
||||
CloseReason: fmt.Sprintf("Squashed from %d ephemeral steps", len(children)),
|
||||
CloseReason: fmt.Sprintf("Squashed from %d wisps", len(children)),
|
||||
Priority: root.Priority,
|
||||
IssueType: types.TypeTask,
|
||||
Ephemeral: false, // Digest is permanent
|
||||
Wisp: false, // Digest is permanent, not a wisp
|
||||
ClosedAt: &now,
|
||||
}
|
||||
|
||||
@@ -280,9 +280,9 @@ func squashMolecule(ctx context.Context, s storage.Storage, root *types.Issue, c
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Delete ephemeral children (outside transaction for better error handling)
|
||||
// Delete wisp children (outside transaction for better error handling)
|
||||
if !keepChildren {
|
||||
deleted, err := deleteEphemeralChildren(ctx, s, childIDs)
|
||||
deleted, err := deleteWispChildren(ctx, s, childIDs)
|
||||
if err != nil {
|
||||
// Log but don't fail - digest was created successfully
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to delete some children: %v\n", err)
|
||||
@@ -293,8 +293,8 @@ func squashMolecule(ctx context.Context, s storage.Storage, root *types.Issue, c
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// deleteEphemeralChildren removes the ephemeral issues from the database
|
||||
func deleteEphemeralChildren(ctx context.Context, s storage.Storage, ids []string) (int, error) {
|
||||
// deleteWispChildren removes the wisp issues from the database
|
||||
func deleteWispChildren(ctx context.Context, s storage.Storage, ids []string) (int, error) {
|
||||
// Type assert to SQLite storage for delete access
|
||||
d, ok := s.(*sqlite.SQLiteStorage)
|
||||
if !ok {
|
||||
@@ -316,7 +316,7 @@ func deleteEphemeralChildren(ctx context.Context, s storage.Storage, ids []strin
|
||||
|
||||
func init() {
|
||||
molSquashCmd.Flags().Bool("dry-run", false, "Preview what would be squashed")
|
||||
molSquashCmd.Flags().Bool("keep-children", false, "Don't delete ephemeral children after squash")
|
||||
molSquashCmd.Flags().Bool("keep-children", false, "Don't delete wisp children after squash")
|
||||
molSquashCmd.Flags().String("summary", "", "Agent-provided summary (bypasses auto-generation)")
|
||||
|
||||
molCmd.AddCommand(molSquashCmd)
|
||||
|
||||
@@ -489,7 +489,7 @@ func TestSquashMolecule(t *testing.T) {
|
||||
Status: types.StatusClosed,
|
||||
Priority: 2,
|
||||
IssueType: types.TypeTask,
|
||||
Ephemeral: true,
|
||||
Wisp: true,
|
||||
CloseReason: "Completed design",
|
||||
}
|
||||
child2 := &types.Issue{
|
||||
@@ -498,7 +498,7 @@ func TestSquashMolecule(t *testing.T) {
|
||||
Status: types.StatusClosed,
|
||||
Priority: 2,
|
||||
IssueType: types.TypeTask,
|
||||
Ephemeral: true,
|
||||
Wisp: true,
|
||||
CloseReason: "Code merged",
|
||||
}
|
||||
|
||||
@@ -547,7 +547,7 @@ func TestSquashMolecule(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get digest: %v", err)
|
||||
}
|
||||
if digest.Ephemeral {
|
||||
if digest.Wisp {
|
||||
t.Error("Digest should NOT be ephemeral")
|
||||
}
|
||||
if digest.Status != types.StatusClosed {
|
||||
@@ -591,11 +591,11 @@ func TestSquashMoleculeWithDelete(t *testing.T) {
|
||||
}
|
||||
|
||||
child := &types.Issue{
|
||||
Title: "Ephemeral Step",
|
||||
Title: "Wisp Step",
|
||||
Status: types.StatusClosed,
|
||||
Priority: 2,
|
||||
IssueType: types.TypeTask,
|
||||
Ephemeral: true,
|
||||
Wisp: true,
|
||||
}
|
||||
if err := s.CreateIssue(ctx, child, "test"); err != nil {
|
||||
t.Fatalf("Failed to create child: %v", err)
|
||||
@@ -700,12 +700,12 @@ func TestSquashMoleculeWithAgentSummary(t *testing.T) {
|
||||
}
|
||||
|
||||
child := &types.Issue{
|
||||
Title: "Ephemeral Step",
|
||||
Title: "Wisp Step",
|
||||
Description: "This should NOT appear in digest",
|
||||
Status: types.StatusClosed,
|
||||
Priority: 2,
|
||||
IssueType: types.TypeTask,
|
||||
Ephemeral: true,
|
||||
Wisp: true,
|
||||
CloseReason: "Done",
|
||||
}
|
||||
if err := s.CreateIssue(ctx, child, "test"); err != nil {
|
||||
@@ -737,15 +737,15 @@ func TestSquashMoleculeWithAgentSummary(t *testing.T) {
|
||||
}
|
||||
|
||||
// Verify auto-generated content is NOT present
|
||||
if strings.Contains(digest.Description, "Ephemeral Step") {
|
||||
if strings.Contains(digest.Description, "Wisp Step") {
|
||||
t.Error("Digest should NOT contain auto-generated content when agent summary provided")
|
||||
}
|
||||
}
|
||||
|
||||
// TestEphemeralFilteringFromExport verifies that ephemeral issues are filtered
|
||||
// from JSONL export (bd-687g). Ephemeral issues should only exist in SQLite,
|
||||
// TestWispFilteringFromExport verifies that wisp issues are filtered
|
||||
// from JSONL export (bd-687g). Wisp issues should only exist in SQLite,
|
||||
// not in issues.jsonl, to prevent "zombie" resurrection after mol squash.
|
||||
func TestEphemeralFilteringFromExport(t *testing.T) {
|
||||
func TestWispFilteringFromExport(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
dbPath := t.TempDir() + "/test.db"
|
||||
s, err := sqlite.New(ctx, dbPath)
|
||||
@@ -757,27 +757,27 @@ func TestEphemeralFilteringFromExport(t *testing.T) {
|
||||
t.Fatalf("Failed to set config: %v", err)
|
||||
}
|
||||
|
||||
// Create a mix of ephemeral and non-ephemeral issues
|
||||
// Create a mix of wisp and non-wisp issues
|
||||
normalIssue := &types.Issue{
|
||||
Title: "Normal Issue",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeTask,
|
||||
Ephemeral: false,
|
||||
Wisp: false,
|
||||
}
|
||||
ephemeralIssue := &types.Issue{
|
||||
Title: "Ephemeral Issue",
|
||||
wispIssue := &types.Issue{
|
||||
Title: "Wisp Issue",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: types.TypeTask,
|
||||
Ephemeral: true,
|
||||
Wisp: true,
|
||||
}
|
||||
|
||||
if err := s.CreateIssue(ctx, normalIssue, "test"); err != nil {
|
||||
t.Fatalf("Failed to create normal issue: %v", err)
|
||||
}
|
||||
if err := s.CreateIssue(ctx, ephemeralIssue, "test"); err != nil {
|
||||
t.Fatalf("Failed to create ephemeral issue: %v", err)
|
||||
if err := s.CreateIssue(ctx, wispIssue, "test"); err != nil {
|
||||
t.Fatalf("Failed to create wisp issue: %v", err)
|
||||
}
|
||||
|
||||
// Get all issues from DB - should include both
|
||||
@@ -789,15 +789,15 @@ func TestEphemeralFilteringFromExport(t *testing.T) {
|
||||
t.Fatalf("Expected 2 issues in DB, got %d", len(allIssues))
|
||||
}
|
||||
|
||||
// Filter ephemeral issues (simulating export behavior)
|
||||
// Filter wisp issues (simulating export behavior)
|
||||
exportableIssues := make([]*types.Issue, 0)
|
||||
for _, issue := range allIssues {
|
||||
if !issue.Ephemeral {
|
||||
if !issue.Wisp {
|
||||
exportableIssues = append(exportableIssues, issue)
|
||||
}
|
||||
}
|
||||
|
||||
// Should only have the non-ephemeral issue
|
||||
// Should only have the non-wisp issue
|
||||
if len(exportableIssues) != 1 {
|
||||
t.Errorf("Expected 1 exportable issue, got %d", len(exportableIssues))
|
||||
}
|
||||
|
||||
@@ -292,7 +292,7 @@ Example:
|
||||
return
|
||||
}
|
||||
|
||||
// Clone the subgraph (deprecated command, non-ephemeral for backwards compatibility)
|
||||
// Clone the subgraph (deprecated command, non-wisp for backwards compatibility)
|
||||
result, err := cloneSubgraph(ctx, store, subgraph, vars, assignee, actor, false)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error instantiating template: %v\n", err)
|
||||
@@ -453,8 +453,8 @@ func substituteVariables(text string, vars map[string]string) string {
|
||||
|
||||
// cloneSubgraph creates new issues from the template with variable substitution
|
||||
// If assignee is non-empty, it will be set on the root epic
|
||||
// If ephemeral is true, spawned issues are marked for bulk deletion when closed (bd-2vh3)
|
||||
func cloneSubgraph(ctx context.Context, s storage.Storage, subgraph *TemplateSubgraph, vars map[string]string, assignee string, actorName string, ephemeral bool) (*InstantiateResult, error) {
|
||||
// If wisp is true, spawned issues are marked for bulk deletion when closed (bd-2vh3)
|
||||
func cloneSubgraph(ctx context.Context, s storage.Storage, subgraph *TemplateSubgraph, vars map[string]string, assignee string, actorName string, wisp bool) (*InstantiateResult, error) {
|
||||
if s == nil {
|
||||
return nil, fmt.Errorf("no database connection")
|
||||
}
|
||||
@@ -484,7 +484,7 @@ func cloneSubgraph(ctx context.Context, s storage.Storage, subgraph *TemplateSub
|
||||
IssueType: oldIssue.IssueType,
|
||||
Assignee: issueAssignee,
|
||||
EstimatedMinutes: oldIssue.EstimatedMinutes,
|
||||
Ephemeral: ephemeral, // bd-2vh3: mark for cleanup when closed
|
||||
Wisp: wisp, // bd-2vh3: mark for cleanup when closed
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func TestThreadTraversal(t *testing.T) {
|
||||
IssueType: types.TypeMessage,
|
||||
Assignee: "worker",
|
||||
Sender: "manager",
|
||||
Ephemeral: true,
|
||||
Wisp: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
@@ -43,7 +43,7 @@ func TestThreadTraversal(t *testing.T) {
|
||||
IssueType: types.TypeMessage,
|
||||
Assignee: "manager",
|
||||
Sender: "worker",
|
||||
Ephemeral: true,
|
||||
Wisp: true,
|
||||
CreatedAt: now.Add(time.Minute),
|
||||
UpdatedAt: now.Add(time.Minute),
|
||||
}
|
||||
@@ -59,7 +59,7 @@ func TestThreadTraversal(t *testing.T) {
|
||||
IssueType: types.TypeMessage,
|
||||
Assignee: "worker",
|
||||
Sender: "manager",
|
||||
Ephemeral: true,
|
||||
Wisp: true,
|
||||
CreatedAt: now.Add(2 * time.Minute),
|
||||
UpdatedAt: now.Add(2 * time.Minute),
|
||||
}
|
||||
@@ -190,7 +190,7 @@ func TestThreadTraversalEmptyThread(t *testing.T) {
|
||||
IssueType: types.TypeMessage,
|
||||
Assignee: "user",
|
||||
Sender: "sender",
|
||||
Ephemeral: true,
|
||||
Wisp: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
@@ -228,7 +228,7 @@ func TestThreadTraversalBranching(t *testing.T) {
|
||||
IssueType: types.TypeMessage,
|
||||
Assignee: "user",
|
||||
Sender: "sender",
|
||||
Ephemeral: true,
|
||||
Wisp: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
@@ -245,7 +245,7 @@ func TestThreadTraversalBranching(t *testing.T) {
|
||||
IssueType: types.TypeMessage,
|
||||
Assignee: "sender",
|
||||
Sender: "user",
|
||||
Ephemeral: true,
|
||||
Wisp: true,
|
||||
CreatedAt: now.Add(time.Minute),
|
||||
UpdatedAt: now.Add(time.Minute),
|
||||
}
|
||||
@@ -261,7 +261,7 @@ func TestThreadTraversalBranching(t *testing.T) {
|
||||
IssueType: types.TypeMessage,
|
||||
Assignee: "sender",
|
||||
Sender: "another-user",
|
||||
Ephemeral: true,
|
||||
Wisp: true,
|
||||
CreatedAt: now.Add(2 * time.Minute),
|
||||
UpdatedAt: now.Add(2 * time.Minute),
|
||||
}
|
||||
@@ -364,7 +364,7 @@ func TestThreadTraversalOnlyRepliesTo(t *testing.T) {
|
||||
IssueType: types.TypeMessage,
|
||||
Assignee: "user",
|
||||
Sender: "sender",
|
||||
Ephemeral: true,
|
||||
Wisp: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
@@ -380,7 +380,7 @@ func TestThreadTraversalOnlyRepliesTo(t *testing.T) {
|
||||
IssueType: types.TypeMessage,
|
||||
Assignee: "user",
|
||||
Sender: "sender",
|
||||
Ephemeral: true,
|
||||
Wisp: true,
|
||||
CreatedAt: now.Add(time.Minute),
|
||||
UpdatedAt: now.Add(time.Minute),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user