@mindfoldhq/trellis 0.1.9 → 0.2.1

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.
Files changed (102) hide show
  1. package/dist/cli/index.js +2 -0
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/commands/init.d.ts.map +1 -1
  4. package/dist/commands/init.js +12 -6
  5. package/dist/commands/init.js.map +1 -1
  6. package/dist/commands/update.d.ts +1 -0
  7. package/dist/commands/update.d.ts.map +1 -1
  8. package/dist/commands/update.js +669 -38
  9. package/dist/commands/update.js.map +1 -1
  10. package/dist/configurators/opencode.js +1 -1
  11. package/dist/configurators/opencode.js.map +1 -1
  12. package/dist/configurators/workflow.d.ts +4 -3
  13. package/dist/configurators/workflow.d.ts.map +1 -1
  14. package/dist/configurators/workflow.js +23 -20
  15. package/dist/configurators/workflow.js.map +1 -1
  16. package/dist/constants/paths.d.ts +29 -30
  17. package/dist/constants/paths.d.ts.map +1 -1
  18. package/dist/constants/paths.js +32 -35
  19. package/dist/constants/paths.js.map +1 -1
  20. package/dist/migrations/index.d.ts +35 -0
  21. package/dist/migrations/index.d.ts.map +1 -0
  22. package/dist/migrations/index.js +124 -0
  23. package/dist/migrations/index.js.map +1 -0
  24. package/dist/migrations/manifests/0.1.9.json +30 -0
  25. package/dist/migrations/manifests/0.2.0.json +43 -0
  26. package/dist/templates/claude/agents/check.md +3 -3
  27. package/dist/templates/claude/agents/debug.md +1 -1
  28. package/dist/templates/claude/agents/dispatch.md +12 -12
  29. package/dist/templates/claude/agents/implement.md +6 -6
  30. package/dist/templates/claude/agents/plan.md +37 -37
  31. package/dist/templates/claude/agents/research.md +1 -1
  32. package/dist/templates/claude/commands/before-backend-dev.md +5 -5
  33. package/dist/templates/claude/commands/before-frontend-dev.md +5 -5
  34. package/dist/templates/claude/commands/break-loop.md +2 -2
  35. package/dist/templates/claude/commands/check-backend.md +6 -6
  36. package/dist/templates/claude/commands/check-cross-layer.md +5 -5
  37. package/dist/templates/claude/commands/check-frontend.md +6 -6
  38. package/dist/templates/claude/commands/create-command.md +3 -3
  39. package/dist/templates/claude/commands/finish-work.md +6 -6
  40. package/dist/templates/claude/commands/integrate-skill.md +11 -11
  41. package/dist/templates/claude/commands/{onboard-developer.md → onboard.md} +31 -28
  42. package/dist/templates/claude/commands/parallel.md +17 -17
  43. package/dist/templates/claude/commands/{record-agent-flow.md → record-session.md} +7 -7
  44. package/dist/templates/claude/commands/start.md +36 -36
  45. package/dist/templates/claude/hooks/inject-subagent-context.py +77 -76
  46. package/dist/templates/claude/hooks/ralph-loop.py +18 -18
  47. package/dist/templates/claude/hooks/session-start.py +4 -4
  48. package/dist/templates/cursor/commands/before-backend-dev.md +5 -5
  49. package/dist/templates/cursor/commands/before-frontend-dev.md +5 -5
  50. package/dist/templates/cursor/commands/break-loop.md +2 -2
  51. package/dist/templates/cursor/commands/check-backend.md +6 -6
  52. package/dist/templates/cursor/commands/check-cross-layer.md +5 -5
  53. package/dist/templates/cursor/commands/check-frontend.md +6 -6
  54. package/dist/templates/cursor/commands/create-command.md +3 -3
  55. package/dist/templates/cursor/commands/finish-work.md +6 -6
  56. package/dist/templates/cursor/commands/integrate-skill.md +11 -11
  57. package/dist/templates/cursor/commands/{onboard-developer.md → onboard.md} +31 -28
  58. package/dist/templates/cursor/commands/{record-agent-flow.md → record-session.md} +7 -7
  59. package/dist/templates/cursor/commands/start.md +25 -25
  60. package/dist/templates/extract.d.ts +2 -2
  61. package/dist/templates/extract.js +2 -2
  62. package/dist/templates/markdown/agents.md +2 -2
  63. package/dist/templates/markdown/gitignore.txt +2 -2
  64. package/dist/templates/markdown/index.d.ts +1 -0
  65. package/dist/templates/markdown/index.d.ts.map +1 -1
  66. package/dist/templates/markdown/index.js +4 -2
  67. package/dist/templates/markdown/index.js.map +1 -1
  68. package/dist/templates/markdown/{agent-traces-index.md → workspace-index.md} +14 -14
  69. package/dist/templates/trellis/index.d.ts +7 -1
  70. package/dist/templates/trellis/index.d.ts.map +1 -1
  71. package/dist/templates/trellis/index.js +14 -2
  72. package/dist/templates/trellis/index.js.map +1 -1
  73. package/dist/templates/trellis/scripts/add-session.sh +26 -26
  74. package/dist/templates/trellis/scripts/common/developer.sh +20 -21
  75. package/dist/templates/trellis/scripts/common/git-context.sh +90 -115
  76. package/dist/templates/trellis/scripts/common/paths.sh +53 -63
  77. package/dist/templates/trellis/scripts/common/phase.sh +40 -40
  78. package/dist/templates/trellis/scripts/common/registry.sh +13 -13
  79. package/dist/templates/trellis/scripts/common/task-queue.sh +142 -0
  80. package/dist/templates/trellis/scripts/common/task-utils.sh +151 -0
  81. package/dist/templates/trellis/scripts/common/worktree.sh +3 -3
  82. package/dist/templates/trellis/scripts/create-bootstrap.sh +43 -42
  83. package/dist/templates/trellis/scripts/init-developer.sh +1 -1
  84. package/dist/templates/trellis/scripts/multi-agent/cleanup.sh +33 -33
  85. package/dist/templates/trellis/scripts/multi-agent/create-pr.sh +30 -30
  86. package/dist/templates/trellis/scripts/multi-agent/plan.sh +28 -28
  87. package/dist/templates/trellis/scripts/multi-agent/start.sh +56 -56
  88. package/dist/templates/trellis/scripts/multi-agent/status.sh +59 -59
  89. package/dist/templates/trellis/scripts/{feature.sh → task.sh} +235 -185
  90. package/dist/templates/trellis/workflow.md +71 -74
  91. package/dist/types/migration.d.ts +74 -0
  92. package/dist/types/migration.d.ts.map +1 -0
  93. package/dist/types/migration.js +8 -0
  94. package/dist/types/migration.js.map +1 -0
  95. package/dist/utils/template-hash.d.ts +78 -0
  96. package/dist/utils/template-hash.d.ts.map +1 -0
  97. package/dist/utils/template-hash.js +234 -0
  98. package/dist/utils/template-hash.js.map +1 -0
  99. package/package.json +1 -1
  100. package/dist/templates/trellis/scripts/common/backlog.sh +0 -220
  101. package/dist/templates/trellis/scripts/common/feature-utils.sh +0 -194
  102. /package/dist/templates/trellis/{backlog → tasks}/.gitkeep +0 -0
@@ -7,57 +7,57 @@
7
7
  # Usage:
8
8
  # source common/phase.sh
9
9
  #
10
- # get_current_phase "$feature_json" # Returns current phase number
11
- # get_total_phases "$feature_json" # Returns total phase count
12
- # get_phase_action "$feature_json" "$phase" # Returns action name for phase
13
- # get_phase_info "$feature_json" # Returns "N/M (action)" format
14
- # set_phase "$feature_json" "$phase" # Sets current_phase
15
- # advance_phase "$feature_json" # Advances to next phase
16
- # get_phase_for_action "$feature_json" "$action" # Returns phase number for action
10
+ # get_current_phase "$task_json" # Returns current phase number
11
+ # get_total_phases "$task_json" # Returns total phase count
12
+ # get_phase_action "$task_json" "$phase" # Returns action name for phase
13
+ # get_phase_info "$task_json" # Returns "N/M (action)" format
14
+ # set_phase "$task_json" "$phase" # Sets current_phase
15
+ # advance_phase "$task_json" # Advances to next phase
16
+ # get_phase_for_action "$task_json" "$action" # Returns phase number for action
17
17
  # =============================================================================
18
18
 
19
19
  # Get current phase number
20
20
  get_current_phase() {
21
- local feature_json="$1"
22
- if [ ! -f "$feature_json" ]; then
21
+ local task_json="$1"
22
+ if [ ! -f "$task_json" ]; then
23
23
  echo "0"
24
24
  return
25
25
  fi
26
- jq -r '.current_phase // 0' "$feature_json"
26
+ jq -r '.current_phase // 0' "$task_json"
27
27
  }
28
28
 
29
29
  # Get total number of phases
30
30
  get_total_phases() {
31
- local feature_json="$1"
32
- if [ ! -f "$feature_json" ]; then
31
+ local task_json="$1"
32
+ if [ ! -f "$task_json" ]; then
33
33
  echo "0"
34
34
  return
35
35
  fi
36
- jq -r '.next_action | length // 0' "$feature_json"
36
+ jq -r '.next_action | length // 0' "$task_json"
37
37
  }
38
38
 
39
39
  # Get action name for a specific phase
40
40
  get_phase_action() {
41
- local feature_json="$1"
41
+ local task_json="$1"
42
42
  local phase="$2"
43
- if [ ! -f "$feature_json" ]; then
43
+ if [ ! -f "$task_json" ]; then
44
44
  echo "unknown"
45
45
  return
46
46
  fi
47
- jq -r --argjson phase "$phase" '.next_action[] | select(.phase == $phase) | .action // "unknown"' "$feature_json"
47
+ jq -r --argjson phase "$phase" '.next_action[] | select(.phase == $phase) | .action // "unknown"' "$task_json"
48
48
  }
49
49
 
50
50
  # Get formatted phase info: "N/M (action)"
51
51
  get_phase_info() {
52
- local feature_json="$1"
53
- if [ ! -f "$feature_json" ]; then
52
+ local task_json="$1"
53
+ if [ ! -f "$task_json" ]; then
54
54
  echo "N/A"
55
55
  return
56
56
  fi
57
57
 
58
- local current_phase=$(get_current_phase "$feature_json")
59
- local total_phases=$(get_total_phases "$feature_json")
60
- local action_name=$(get_phase_action "$feature_json" "$current_phase")
58
+ local current_phase=$(get_current_phase "$task_json")
59
+ local total_phases=$(get_total_phases "$task_json")
60
+ local action_name=$(get_phase_action "$task_json" "$current_phase")
61
61
 
62
62
  if [ "$current_phase" = "0" ] || [ "$current_phase" = "null" ]; then
63
63
  echo "0/${total_phases} (pending)"
@@ -68,29 +68,29 @@ get_phase_info() {
68
68
 
69
69
  # Set current phase to a specific value
70
70
  set_phase() {
71
- local feature_json="$1"
71
+ local task_json="$1"
72
72
  local phase="$2"
73
73
 
74
- if [ ! -f "$feature_json" ]; then
75
- echo "Error: feature.json not found: $feature_json" >&2
74
+ if [ ! -f "$task_json" ]; then
75
+ echo "Error: task.json not found: $task_json" >&2
76
76
  return 1
77
77
  fi
78
78
 
79
- jq --argjson phase "$phase" '.current_phase = $phase' "$feature_json" > "${feature_json}.tmp"
80
- mv "${feature_json}.tmp" "$feature_json"
79
+ jq --argjson phase "$phase" '.current_phase = $phase' "$task_json" > "${task_json}.tmp"
80
+ mv "${task_json}.tmp" "$task_json"
81
81
  }
82
82
 
83
83
  # Advance to next phase
84
84
  advance_phase() {
85
- local feature_json="$1"
85
+ local task_json="$1"
86
86
 
87
- if [ ! -f "$feature_json" ]; then
88
- echo "Error: feature.json not found: $feature_json" >&2
87
+ if [ ! -f "$task_json" ]; then
88
+ echo "Error: task.json not found: $task_json" >&2
89
89
  return 1
90
90
  fi
91
91
 
92
- local current=$(get_current_phase "$feature_json")
93
- local total=$(get_total_phases "$feature_json")
92
+ local current=$(get_current_phase "$task_json")
93
+ local total=$(get_total_phases "$task_json")
94
94
  local next=$((current + 1))
95
95
 
96
96
  if [ "$next" -gt "$total" ]; then
@@ -98,20 +98,20 @@ advance_phase() {
98
98
  return 0
99
99
  fi
100
100
 
101
- set_phase "$feature_json" "$next"
101
+ set_phase "$task_json" "$next"
102
102
  }
103
103
 
104
104
  # Get phase number for a specific action name
105
105
  get_phase_for_action() {
106
- local feature_json="$1"
106
+ local task_json="$1"
107
107
  local action="$2"
108
108
 
109
- if [ ! -f "$feature_json" ]; then
109
+ if [ ! -f "$task_json" ]; then
110
110
  echo "0"
111
111
  return
112
112
  fi
113
113
 
114
- jq -r --arg action "$action" '.next_action[] | select(.action == $action) | .phase // 0' "$feature_json"
114
+ jq -r --arg action "$action" '.next_action[] | select(.action == $action) | .phase // 0' "$task_json"
115
115
  }
116
116
 
117
117
  # Map subagent type to action name
@@ -131,20 +131,20 @@ map_subagent_to_action() {
131
131
 
132
132
  # Check if a phase is completed (current_phase > phase)
133
133
  is_phase_completed() {
134
- local feature_json="$1"
134
+ local task_json="$1"
135
135
  local phase="$2"
136
136
 
137
- local current=$(get_current_phase "$feature_json")
137
+ local current=$(get_current_phase "$task_json")
138
138
  [ "$current" -gt "$phase" ]
139
139
  }
140
140
 
141
141
  # Check if we're at a specific action
142
142
  is_current_action() {
143
- local feature_json="$1"
143
+ local task_json="$1"
144
144
  local action="$2"
145
145
 
146
- local current=$(get_current_phase "$feature_json")
147
- local action_phase=$(get_phase_for_action "$feature_json" "$action")
146
+ local current=$(get_current_phase "$task_json")
147
+ local action_phase=$(get_phase_for_action "$task_json" "$action")
148
148
 
149
149
  [ "$current" = "$action_phase" ]
150
150
  }
@@ -8,7 +8,7 @@
8
8
  # registry_get_file - Get registry file path
9
9
  # registry_get_agent_by_id - Find agent by ID
10
10
  # registry_get_agent_by_worktree - Find agent by worktree path
11
- # registry_get_feature_dir - Get feature dir for a worktree
11
+ # registry_get_task_dir - Get feature dir for a worktree
12
12
  # registry_remove_by_id - Remove agent by ID
13
13
  # registry_remove_by_worktree - Remove agent by worktree path
14
14
  # registry_add_agent - Add agent to registry
@@ -99,7 +99,7 @@ registry_get_agent_by_worktree() {
99
99
  return 1
100
100
  }
101
101
 
102
- # Search agent by ID or feature_dir containing search term
102
+ # Search agent by ID or task_dir containing search term
103
103
  # Args: search_term, [repo_root]
104
104
  # Returns: first matching agent JSON object (compact), or empty if not found
105
105
  registry_search_agent() {
@@ -112,7 +112,7 @@ registry_search_agent() {
112
112
  fi
113
113
 
114
114
  local agent=$(jq -c --arg search "$search" \
115
- '[.agents[] | select(.id == $search or (.feature_dir | contains($search)))] | first' \
115
+ '[.agents[] | select(.id == $search or (.task_dir | contains($search)))] | first' \
116
116
  "$registry_file" 2>/dev/null)
117
117
 
118
118
  if [[ -n "$agent" ]] && [[ "$agent" != "null" ]]; then
@@ -125,8 +125,8 @@ registry_search_agent() {
125
125
 
126
126
  # Get feature directory for a worktree
127
127
  # Args: worktree_path, [repo_root]
128
- # Returns: feature_dir value, or empty if not found
129
- registry_get_feature_dir() {
128
+ # Returns: task_dir value, or empty if not found
129
+ registry_get_task_dir() {
130
130
  local worktree_path="$1"
131
131
  local repo_root="${2:-$(get_repo_root)}"
132
132
  local registry_file=$(registry_get_file "$repo_root")
@@ -135,12 +135,12 @@ registry_get_feature_dir() {
135
135
  return 1
136
136
  fi
137
137
 
138
- local feature_dir=$(jq -r --arg path "$worktree_path" \
139
- '.agents[] | select(.worktree_path == $path) | .feature_dir' \
138
+ local task_dir=$(jq -r --arg path "$worktree_path" \
139
+ '.agents[] | select(.worktree_path == $path) | .task_dir' \
140
140
  "$registry_file" 2>/dev/null)
141
141
 
142
- if [[ -n "$feature_dir" ]] && [[ "$feature_dir" != "null" ]]; then
143
- echo "$feature_dir"
142
+ if [[ -n "$task_dir" ]] && [[ "$task_dir" != "null" ]]; then
143
+ echo "$task_dir"
144
144
  return 0
145
145
  fi
146
146
 
@@ -192,13 +192,13 @@ registry_remove_by_worktree() {
192
192
  }
193
193
 
194
194
  # Add agent to registry (replaces if same ID exists)
195
- # Args: agent_id, worktree_path, pid, feature_dir, [repo_root]
195
+ # Args: agent_id, worktree_path, pid, task_dir, [repo_root]
196
196
  # Returns: 0 on success
197
197
  registry_add_agent() {
198
198
  local agent_id="$1"
199
199
  local worktree_path="$2"
200
200
  local pid="$3"
201
- local feature_dir="$4"
201
+ local task_dir="$4"
202
202
  local repo_root="${5:-$(get_repo_root)}"
203
203
 
204
204
  _ensure_registry "$repo_root"
@@ -217,13 +217,13 @@ registry_add_agent() {
217
217
  --arg worktree "$worktree_path" \
218
218
  --arg pid "$pid" \
219
219
  --arg started_at "$started_at" \
220
- --arg feature_dir "$feature_dir" \
220
+ --arg task_dir "$task_dir" \
221
221
  '{
222
222
  id: $id,
223
223
  worktree_path: $worktree,
224
224
  pid: ($pid | tonumber),
225
225
  started_at: $started_at,
226
- feature_dir: $feature_dir
226
+ task_dir: $task_dir
227
227
  }')
228
228
 
229
229
  # Add to registry
@@ -0,0 +1,142 @@
1
+ #!/bin/bash
2
+ # Task queue utility functions
3
+ #
4
+ # Usage: source this file in other scripts
5
+ # source "$(dirname "$0")/common/task-queue.sh"
6
+ #
7
+ # Provides:
8
+ # list_pending_tasks - List tasks with pending status
9
+ # get_task_stats - Get P0/P1/P2/P3 counts
10
+
11
+ # Ensure paths.sh is loaded
12
+ if ! type get_repo_root &>/dev/null; then
13
+ echo "Error: paths.sh must be sourced before task-queue.sh" >&2
14
+ exit 1
15
+ fi
16
+
17
+ # =============================================================================
18
+ # Public Functions
19
+ # =============================================================================
20
+
21
+ # List tasks by status
22
+ # Args: [filter_status]
23
+ # Output: formatted list to stdout
24
+ list_tasks_by_status() {
25
+ local filter_status="${1:-}"
26
+ local repo_root="${2:-$(get_repo_root)}"
27
+
28
+ local tasks_dir=$(get_tasks_dir "$repo_root")
29
+
30
+ if [[ ! -d "$tasks_dir" ]]; then
31
+ return 0
32
+ fi
33
+
34
+ for d in "$tasks_dir"/*/; do
35
+ if [[ -d "$d" ]] && [[ "$(basename "$d")" != "archive" ]]; then
36
+ local task_json="$d/$FILE_TASK_JSON"
37
+ if [[ -f "$task_json" ]]; then
38
+ local id=$(jq -r '.id' "$task_json")
39
+ local title=$(jq -r '.title // .name' "$task_json")
40
+ local priority=$(jq -r '.priority // "P2"' "$task_json")
41
+ local status=$(jq -r '.status // "planning"' "$task_json")
42
+ local assignee=$(jq -r '.assignee // "-"' "$task_json")
43
+
44
+ # Apply filter
45
+ if [[ -n "$filter_status" ]] && [[ "$status" != "$filter_status" ]]; then
46
+ continue
47
+ fi
48
+
49
+ echo "$priority|$id|$title|$status|$assignee"
50
+ fi
51
+ fi
52
+ done
53
+ }
54
+
55
+ # List pending tasks
56
+ list_pending_tasks() {
57
+ list_tasks_by_status "planning" "$@"
58
+ }
59
+
60
+ # List tasks assigned to a specific developer
61
+ # Args: developer_name, [filter_status], [repo_root]
62
+ # Output: formatted list to stdout
63
+ list_tasks_by_assignee() {
64
+ local assignee="$1"
65
+ local filter_status="${2:-}"
66
+ local repo_root="${3:-$(get_repo_root)}"
67
+
68
+ local tasks_dir=$(get_tasks_dir "$repo_root")
69
+
70
+ if [[ ! -d "$tasks_dir" ]]; then
71
+ return 0
72
+ fi
73
+
74
+ for d in "$tasks_dir"/*/; do
75
+ if [[ -d "$d" ]] && [[ "$(basename "$d")" != "archive" ]]; then
76
+ local task_json="$d/$FILE_TASK_JSON"
77
+ if [[ -f "$task_json" ]]; then
78
+ local id=$(jq -r '.id' "$task_json")
79
+ local title=$(jq -r '.title // .name' "$task_json")
80
+ local priority=$(jq -r '.priority // "P2"' "$task_json")
81
+ local status=$(jq -r '.status // "planning"' "$task_json")
82
+ local task_assignee=$(jq -r '.assignee // "-"' "$task_json")
83
+
84
+ # Apply assignee filter
85
+ if [[ "$task_assignee" != "$assignee" ]]; then
86
+ continue
87
+ fi
88
+
89
+ # Apply status filter
90
+ if [[ -n "$filter_status" ]] && [[ "$status" != "$filter_status" ]]; then
91
+ continue
92
+ fi
93
+
94
+ echo "$priority|$id|$title|$status|$task_assignee"
95
+ fi
96
+ fi
97
+ done
98
+ }
99
+
100
+ # List my tasks (current developer)
101
+ # Args: [filter_status], [repo_root]
102
+ list_my_tasks() {
103
+ local filter_status="${1:-}"
104
+ local repo_root="${2:-$(get_repo_root)}"
105
+ local developer=$(get_developer "$repo_root")
106
+
107
+ if [[ -z "$developer" ]]; then
108
+ echo "Error: Developer not set" >&2
109
+ return 1
110
+ fi
111
+
112
+ list_tasks_by_assignee "$developer" "$filter_status" "$repo_root"
113
+ }
114
+
115
+ # Get task statistics
116
+ # Output: "P0:N P1:N P2:N P3:N Total:N"
117
+ get_task_stats() {
118
+ local repo_root="${1:-$(get_repo_root)}"
119
+ local tasks_dir=$(get_tasks_dir "$repo_root")
120
+
121
+ local p0=0 p1=0 p2=0 p3=0 total=0
122
+
123
+ if [[ -d "$tasks_dir" ]]; then
124
+ for d in "$tasks_dir"/*/; do
125
+ if [[ -d "$d" ]] && [[ "$(basename "$d")" != "archive" ]]; then
126
+ local task_json="$d/$FILE_TASK_JSON"
127
+ if [[ -f "$task_json" ]]; then
128
+ local priority=$(jq -r '.priority // "P2"' "$task_json" 2>/dev/null)
129
+ case "$priority" in
130
+ P0) ((p0++)) ;;
131
+ P1) ((p1++)) ;;
132
+ P2) ((p2++)) ;;
133
+ P3) ((p3++)) ;;
134
+ esac
135
+ ((total++))
136
+ fi
137
+ fi
138
+ done
139
+ fi
140
+
141
+ echo "P0:$p0 P1:$p1 P2:$p2 P3:$p3 Total:$total"
142
+ }
@@ -0,0 +1,151 @@
1
+ #!/bin/bash
2
+ # Task utility functions
3
+ #
4
+ # Usage: source this file in other scripts
5
+ # source "$(dirname "$0")/common/task-utils.sh"
6
+ #
7
+ # Provides:
8
+ # is_safe_task_path - Validate task path is safe to operate on
9
+ # find_task_by_name - Find task directory by name
10
+ # archive_task_dir - Archive task to monthly directory
11
+
12
+ # Ensure dependencies are loaded
13
+ if ! type get_repo_root &>/dev/null; then
14
+ echo "Error: paths.sh must be sourced before task-utils.sh" >&2
15
+ exit 1
16
+ fi
17
+
18
+ # =============================================================================
19
+ # Path Safety
20
+ # =============================================================================
21
+
22
+ # Check if a relative task path is safe to operate on
23
+ # Args: task_path (relative), repo_root
24
+ # Returns: 0 if safe, 1 if dangerous
25
+ # Outputs: error message to stderr if unsafe
26
+ is_safe_task_path() {
27
+ local task_path="$1"
28
+ local repo_root="${2:-$(get_repo_root)}"
29
+
30
+ # Check empty or null
31
+ if [[ -z "$task_path" ]] || [[ "$task_path" = "null" ]]; then
32
+ echo "Error: empty or null task path" >&2
33
+ return 1
34
+ fi
35
+
36
+ # Reject absolute paths
37
+ if [[ "$task_path" = /* ]]; then
38
+ echo "Error: absolute path not allowed: $task_path" >&2
39
+ return 1
40
+ fi
41
+
42
+ # Reject ".", "..", paths starting with "./" or "../", or containing ".."
43
+ if [[ "$task_path" = "." ]] || [[ "$task_path" = ".." ]] || \
44
+ [[ "$task_path" = "./" ]] || [[ "$task_path" == ./* ]] || \
45
+ [[ "$task_path" == *".."* ]]; then
46
+ echo "Error: path traversal not allowed: $task_path" >&2
47
+ return 1
48
+ fi
49
+
50
+ # Final check: ensure resolved path is not the repo root
51
+ local abs_path="${repo_root}/${task_path}"
52
+ if [[ -e "$abs_path" ]]; then
53
+ local resolved=$(realpath "$abs_path" 2>/dev/null)
54
+ local root_resolved=$(realpath "$repo_root" 2>/dev/null)
55
+ if [[ "$resolved" = "$root_resolved" ]]; then
56
+ echo "Error: path resolves to repo root: $task_path" >&2
57
+ return 1
58
+ fi
59
+ fi
60
+
61
+ return 0
62
+ }
63
+
64
+ # =============================================================================
65
+ # Task Lookup
66
+ # =============================================================================
67
+
68
+ # Find task directory by name (exact or suffix match)
69
+ # Args: task_name, tasks_dir
70
+ # Returns: absolute path to task directory, or empty if not found
71
+ find_task_by_name() {
72
+ local task_name="$1"
73
+ local tasks_dir="$2"
74
+
75
+ if [[ -z "$task_name" ]] || [[ -z "$tasks_dir" ]]; then
76
+ return 1
77
+ fi
78
+
79
+ # Try exact match first
80
+ local task_dir=$(find "$tasks_dir" -maxdepth 1 -type d -name "${task_name}" 2>/dev/null | head -1)
81
+
82
+ # Try suffix match (e.g., "my-task" matches "01-21-my-task")
83
+ if [[ -z "$task_dir" ]]; then
84
+ task_dir=$(find "$tasks_dir" -maxdepth 1 -type d -name "*-${task_name}" 2>/dev/null | head -1)
85
+ fi
86
+
87
+ if [[ -n "$task_dir" ]] && [[ -d "$task_dir" ]]; then
88
+ echo "$task_dir"
89
+ return 0
90
+ fi
91
+
92
+ return 1
93
+ }
94
+
95
+ # =============================================================================
96
+ # Archive Operations
97
+ # =============================================================================
98
+
99
+ # Archive a task directory to archive/{YYYY-MM}/
100
+ # Args: task_dir_abs, [repo_root]
101
+ # Returns: 0 on success, 1 on error
102
+ # Outputs: archive destination path
103
+ archive_task_dir() {
104
+ local task_dir_abs="$1"
105
+ local repo_root="${2:-$(get_repo_root)}"
106
+
107
+ if [[ ! -d "$task_dir_abs" ]]; then
108
+ echo "Error: task directory not found: $task_dir_abs" >&2
109
+ return 1
110
+ fi
111
+
112
+ # Get tasks directory (parent of the task)
113
+ local tasks_dir=$(dirname "$task_dir_abs")
114
+ local archive_dir="$tasks_dir/archive"
115
+ local year_month=$(date +%Y-%m)
116
+ local month_dir="$archive_dir/$year_month"
117
+
118
+ # Create archive directory
119
+ mkdir -p "$month_dir"
120
+
121
+ # Move task to archive
122
+ local task_name=$(basename "$task_dir_abs")
123
+ mv "$task_dir_abs" "$month_dir/"
124
+
125
+ # Output the destination
126
+ echo "$month_dir/$task_name"
127
+ return 0
128
+ }
129
+
130
+ # Complete archive workflow: archive directory
131
+ # Args: task_dir_abs, [repo_root]
132
+ # Returns: 0 on success
133
+ # Outputs: lines with status info
134
+ archive_task_complete() {
135
+ local task_dir_abs="$1"
136
+ local repo_root="${2:-$(get_repo_root)}"
137
+
138
+ if [[ ! -d "$task_dir_abs" ]]; then
139
+ echo "Error: task directory not found: $task_dir_abs" >&2
140
+ return 1
141
+ fi
142
+
143
+ # Archive the directory
144
+ local archive_dest
145
+ if archive_dest=$(archive_task_dir "$task_dir_abs" "$repo_root"); then
146
+ echo "archived_to:$archive_dest"
147
+ return 0
148
+ fi
149
+
150
+ return 1
151
+ }
@@ -120,9 +120,9 @@ get_worktree_post_create_hooks() {
120
120
  # Returns: absolute path to agents directory
121
121
  get_agents_dir() {
122
122
  local repo_root="${1:-$(get_repo_root)}"
123
- local progress_dir=$(get_progress_dir "$repo_root")
123
+ local workspace_dir=$(get_workspace_dir "$repo_root")
124
124
 
125
- if [[ -n "$progress_dir" ]]; then
126
- echo "$progress_dir/.agents"
125
+ if [[ -n "$workspace_dir" ]]; then
126
+ echo "$workspace_dir/.agents"
127
127
  fi
128
128
  }