sdd-agent-pack 1.3.4 → 1.3.6

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.
@@ -20,8 +20,13 @@ LLM Configuration:
20
20
  LLM_API_KEY — API key from your LLM provider
21
21
 
22
22
  Optional:
23
- LLM_MODEL — Model name (default: openrouter/anthropic/claude-sonnet-4-5-20250929)
24
- LLM_BASE_URL — Provider base URL
23
+ LLM_MODEL — Model name (default: openrouter/anthropic/claude-sonnet-4-5-20250929)
24
+ LLM_BASE_URL — Provider base URL
25
+ SANDBOX_VOLUMES — Mount host dirs into Docker sandbox (format: host:container[:mode])
26
+ Required when using --cloud (agent-server/sandbox mode) so
27
+ file changes persist on the host filesystem.
28
+ Example: SANDBOX_VOLUMES=$PWD:/workspace:rw
29
+ SANDBOX_USER_ID — Host user ID for sandbox file ownership (default: $(id -u))
25
30
 
26
31
  Examples:
27
32
  OpenRouter:
@@ -61,7 +66,16 @@ TASKS_FILE = "specs/tasks.md"
61
66
 
62
67
 
63
68
  def parse_epics(file_path: str) -> list[dict]:
64
- """Parse incomplete epics from tasks.md frontmatter."""
69
+ """Parse incomplete epics from tasks.md.
70
+
71
+ Detects epic headings like:
72
+ ## Epic EPIC-001 — Title
73
+ ## Epic TDS.1: Title
74
+ ### Epic Something
75
+
76
+ An epic is "incomplete" if any checkbox (- [ ]) is unchecked under it.
77
+ An epic is "complete" if all its checkboxes are checked or there are none.
78
+ """
65
79
  if not os.path.exists(file_path):
66
80
  print(f"✗ tasks.md not found at {file_path}")
67
81
  print(" Run this from your repository root.")
@@ -71,43 +85,35 @@ def parse_epics(file_path: str) -> list[dict]:
71
85
  content = f.read()
72
86
 
73
87
  epics = []
74
-
75
- # Try YAML frontmatter format first
76
- # Look for: status: pending or status: in-progress under an epic entry
77
- frontmatter_match = re.search(
78
- r"autopilot:\s*\n\s+epics:\s*\n(.+?)(?=\n\S|\Z)",
79
- content,
80
- re.DOTALL,
81
- )
82
-
83
- if frontmatter_match:
84
- # Parse structured frontmatter
85
- epic_pattern = re.compile(
86
- r"- id:\s*(\S+)\s*\n\s+title:\s*(.+?)\s*\n\s+status:\s*(.+?)(?:\n|$)"
87
- )
88
- for match in epic_pattern.finditer(frontmatter_match.group(1)):
89
- epic_id = match.group(1)
90
- title = match.group(2).strip()
91
- status = match.group(3).strip()
92
- if status in ("pending", "in-progress"):
93
- epics.append({"id": epic_id, "title": title, "status": status})
94
- else:
95
- # Fallback: parse markdown headings and status lines
96
- lines = content.split("\n")
97
- current_epic = None
98
- for i, line in enumerate(lines):
99
- heading_match = re.match(r"^#{2,4}\s+(.+)$", line)
100
- if heading_match:
101
- current_epic = heading_match.group(1).strip()
102
- elif current_epic and re.search(
103
- r"status:\s*(pending|in-progress)", line
104
- ):
105
- epics.append(
106
- {"id": current_epic, "title": current_epic, "status": "pending"}
107
- )
108
- current_epic = None
109
- elif current_epic and re.search(r"status:\s*complete", line):
110
- current_epic = None
88
+ lines = content.split("\n")
89
+
90
+ epic_heading_re = re.compile(r"^#{2,4}\s+Epic\s+(.+)$", re.IGNORECASE)
91
+
92
+ current_epic = None
93
+ has_unchecked = False
94
+
95
+ for line in lines:
96
+ heading_match = epic_heading_re.match(line)
97
+ if heading_match:
98
+ # Save previous epic if it has unchecked tasks
99
+ if current_epic and has_unchecked:
100
+ epics.append({
101
+ "id": current_epic,
102
+ "title": current_epic,
103
+ "status": "incomplete",
104
+ })
105
+ current_epic = heading_match.group(1).strip()
106
+ has_unchecked = False
107
+ elif current_epic and re.match(r"^\s*-\s+\[\s*\]", line):
108
+ has_unchecked = True
109
+
110
+ # Don't forget the last epic
111
+ if current_epic and has_unchecked:
112
+ epics.append({
113
+ "id": current_epic,
114
+ "title": current_epic,
115
+ "status": "incomplete",
116
+ })
111
117
 
112
118
  return epics
113
119
 
@@ -149,6 +155,22 @@ def run_epic(epic: dict, use_cloud: bool) -> bool:
149
155
  print(" See .sdd-agent-pack/templates/env.example for all options.")
150
156
  return False
151
157
 
158
+ # Ensure the sandbox can access the local project directory.
159
+ # In local (SDK) mode the Conversation(workspace=cwd) creates a
160
+ # LocalWorkspace that runs subprocesses directly on the host, so
161
+ # file changes persist automatically.
162
+ #
163
+ # When --cloud is used the agent runs in a remote Docker sandbox.
164
+ # In that case SANDBOX_VOLUMES mounts the host project into the
165
+ # container's /workspace so agent file changes survive.
166
+ if use_cloud:
167
+ sandbox_volumes = os.getenv(
168
+ "SANDBOX_VOLUMES",
169
+ f"{os.getcwd()}:/workspace:rw",
170
+ )
171
+ os.environ.setdefault("SANDBOX_VOLUMES", sandbox_volumes)
172
+ os.environ.setdefault("SANDBOX_USER_ID", str(os.getuid()))
173
+
152
174
  llm = LLM(
153
175
  model=model,
154
176
  api_key=api_key,
@@ -68,24 +68,36 @@ echo "━━━ SDD Agent Pack — Epic Orchestrator ━━━"
68
68
  echo ""
69
69
 
70
70
  # Parse incomplete epics from tasks.md
71
- # Looks for lines like: "### EPIC-001: Title" followed by "- [ ] ..."
71
+ # Looks for epic headings (## Epic XXX Title) and checks if any
72
+ # of their tasks or requirements have unchecked boxes (- [ ]).
73
+ # An epic is "incomplete" if any checkbox under it is unchecked.
72
74
  echo "Scanning $TASKS_FILE for incomplete epics..."
73
75
 
74
76
  epics=()
75
77
  current_epic=""
78
+ has_unchecked=false
76
79
 
77
80
  while IFS= read -r line; do
78
- if [[ "$line" =~ ^###[[:space:]]+(EPIC-[^:]+):(.+)$ ]] || [[ "$line" =~ ^###[[:space:]]+(T[^:]+):(.+)$ ]] || [[ "$line" =~ ^###[[:space:]]+([^:]+):(.+)$ ]]; then
79
- current_epic="${BASH_REMATCH[1]}:${BASH_REMATCH[2]}"
80
- current_epic="$(echo "$current_epic" | xargs)"
81
- elif [[ "$line" =~ status:[[:space:]]*(pending|in-progress) ]] && [ -n "$current_epic" ]; then
82
- epics+=("$current_epic")
83
- current_epic=""
84
- elif [[ "$line" =~ status:[[:space:]]*complete ]] || [[ "$line" =~ status:[[:space:]]*completed ]]; then
85
- current_epic=""
81
+ # Detect epic heading: ## Epic XXX Title or ## Epic XXX: Title
82
+ if [[ "$line" =~ ^[#]+\ +Epic\ +(.+)$ ]]; then
83
+ # Save previous epic if it has unchecked tasks
84
+ if [ -n "$current_epic" ] && [ "$has_unchecked" = true ]; then
85
+ epics+=("$current_epic")
86
+ fi
87
+ current_epic="${BASH_REMATCH[1]}"
88
+ current_epic="$(echo "$current_epic" | sed 's/^[#[:space:]]*//' | xargs)"
89
+ has_unchecked=false
90
+ # Detect unchecked checkbox
91
+ elif [[ "$line" =~ ^[[:space:]]*-\ +\[\ \] ]] && [ -n "$current_epic" ]; then
92
+ has_unchecked=true
86
93
  fi
87
94
  done < "$TASKS_FILE"
88
95
 
96
+ # Don't forget the last epic
97
+ if [ -n "$current_epic" ] && [ "$has_unchecked" = true ]; then
98
+ epics+=("$current_epic")
99
+ fi
100
+
89
101
  if [ ${#epics[@]} -eq 0 ]; then
90
102
  echo "✓ All epics are complete! Nothing to do."
91
103
  exit 0
@@ -143,6 +155,14 @@ Instructions:
143
155
  8. Do NOT work on any other epic"
144
156
 
145
157
  echo "Starting OpenHands (headless)..."
158
+
159
+ # Mount the current directory into the Docker sandbox container so
160
+ # agent file changes persist on the host filesystem.
161
+ # SANDBOX_VOLUMES — mounts $PWD into the container's /workspace (rw)
162
+ # SANDBOX_USER_ID — ensures created files have host-user ownership
163
+ export SANDBOX_VOLUMES="$PWD:/workspace:rw"
164
+ export SANDBOX_USER_ID="${SANDBOX_USER_ID:-$(id -u)}"
165
+
146
166
  if $OPENHANDS_CMD --headless --override-with-envs -t "$prompt" 2>&1; then
147
167
  echo ""
148
168
  echo "✓ [$current/$total] Epic completed: $epic"
@@ -5,6 +5,8 @@
5
5
  #
6
6
  # Get your OpenRouter API key: https://openrouter.ai/keys
7
7
 
8
+ # ── LLM Configuration ──────────────────────────────────────────────
9
+
8
10
  # Your API key from the LLM provider
9
11
  LLM_API_KEY=sk-or-paste-your-api-key-here
10
12
 
@@ -14,3 +16,17 @@ LLM_MODEL=openrouter/free
14
16
 
15
17
  # OpenRouter API base URL
16
18
  LLM_BASE_URL=https://openrouter.ai/api/v1
19
+
20
+ # ── Sandbox / Workspace Persistence ────────────────────────────────
21
+ #
22
+ # The orchestrate.sh script uses openhands --headless, which spawns a
23
+ # Docker sandbox container. Without a volume mount the agent's file
24
+ # changes are lost when the container exits.
25
+ #
26
+ # Setting SANDBOX_VOLUMES mounts the host project directory into the
27
+ # sandbox container's /workspace so all file changes persist on the
28
+ # host filesystem. Both scripts set a sensible default automatically,
29
+ # but you can override it here:
30
+
31
+ # SANDBOX_VOLUMES="$PWD:/workspace:rw"
32
+ # SANDBOX_USER_ID="$(id -u)"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdd-agent-pack",
3
- "version": "1.3.4",
3
+ "version": "1.3.6",
4
4
  "description": "Lightweight installer for SDD workflow assets into application repositories",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",