| name | code-migration |
| description | Migrate applications between technologies using kantra static analysis and automated fixes. Use when migrating Java, Node.js, Python, Go, or .NET applications. Keywords: kantra, migration, upgrade, modernize. |
Code Migration
Migrate applications by identifying issues from multiple sources, fixing them systematically, and validating the result.
Issue Sources
Collect issues from ALL of these sources during analysis.
| Source | Examples |
|---|
| Kantra analysis | Deprecated APIs, breaking changes, migration patterns |
| Build errors | Compilation failures, type errors, missing deps |
| Lint errors | Style violations, unused imports |
| Test failures | Broken tests from API changes |
| Target docs | Breaking changes Kantra doesn't detect (check targets/<target>.md) |
Kantra is a static source code analysis tool that uses rules to identify migration issues in the source code.
Phase 1: Discovery
Step 1: Explore Project Structure
Discover build system, test commands, and lint configuration.
Find build system:
| File Found | Build Command |
|---|
package.json | npm run build or yarn build |
pom.xml | mvn compile or mvn package |
build.gradle / build.gradle.kts | ./gradlew build |
Makefile | make |
go.mod | go build ./... |
Cargo.toml | cargo build |
*.csproj / *.sln | dotnet build |
Find test commands:
| Test Type | How to Find |
|---|
| Unit tests | Check package.json scripts for test, test:unit; or mvn test, go test ./... |
| Integration | Look for test:integration, test:e2e scripts |
| E2E | Look for Cypress (cypress run), Playwright (npx playwright test), or similar |
Find lint command:
| Tool | Detection |
|---|
| ESLint | .eslintrc* file → npm run lint or npx eslint . |
| Prettier | .prettierrc* file → npx prettier --check . |
| Go | golangci-lint run |
| Python | flake8, pylint, ruff |
Find dev server command:
Look for how to run the application locally (e.g., npm start, npm run dev).
Record findings:
Build: <command>
Dev server: <command>
Lint: <command>
Unit tests: <command>
Integration tests: <command>
E2E tests: <command>
Primary language: <language>
Step 2: Detect OpenShift Console Plugin
Check whether the project is an OpenShift Console dynamic plugin. Look for all of these indicators:
@openshift-console/dynamic-plugin-sdk in package.json dependencies or devDependencies
- A
console-extensions.json file in the project root
- A
ConsolePlugin resource in any YAML/JSON file under the project
If any indicator is present, mark the project as a console plugin (IS_CONSOLE_PLUGIN=true). Record this in $WORK_DIR/status.md under a ## Project Type heading once the workspace is created.
Step 3: Build Kantra Command
Construct the Kantra analyze command flags.
Ask user:
- Use custom rules? (If yes, get path)
- Enable default rulesets?
Detect provider:
| Files Found | Provider |
|---|
*.java, pom.xml, build.gradle | java |
*.ts, *.tsx, package.json with TS deps | typescript |
*.js, *.jsx, package.json | javascript |
go.mod, *.go | go |
*.py, requirements.txt, pyproject.toml | python |
*.cs, *.csproj | dotnet |
Build flags:
--provider=<detected_provider>
--rules=<path_to_rules>
--target=<migration_target>
You add --input, --output, and --overwrite:
kantra analyze --input <project> --output $WORK_DIR/round-N/kantra --overwrite <FLAGS>
Step 4: Create Workspace
WORK_DIR=$(mktemp -d -t migration-$(date +%m_%d_%y_%H))
Step 5: Console Plugin Cluster Setup
Only perform this step if IS_CONSOLE_PLUGIN=true (detected in Step 2).
5a. Prerequisites: Verify kubectl and curl are available. If kind is not installed, install it:
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
[ "$ARCH" = "x86_64" ] && ARCH="amd64"
[ "$ARCH" = "aarch64" ] && ARCH="arm64"
curl -Lo ./kind "https://kind.sigs.k8s.io/dl/v0.27.0/kind-${OS}-${ARCH}"
chmod +x ./kind && mkdir -p "$HOME/.local/bin" && mv ./kind "$HOME/.local/bin/kind"
5b. Create kind cluster: Delete any existing cluster with the same name, then create one with a NodePort mapping (port 30443). Wait for the node to be Ready.
5c. Install OLM:
kubectl create -f https://github.com/operator-framework/operator-lifecycle-manager/releases/latest/download/crds.yaml
kubectl wait --for=condition=Established crd --all --timeout=60s
kubectl create -f https://github.com/operator-framework/operator-lifecycle-manager/releases/latest/download/olm.yaml
Wait for olm-operator, catalog-operator, and packageserver deployments to roll out. Verify the operatorhubio-catalog CatalogSource is READY.
5d. Deploy OpenShift Console via OLM: Create namespace openshift-console, an OperatorGroup, a ServiceAccount with cluster-admin, a kubernetes.io/service-account-token Secret, a ClusterServiceVersion deploying quay.io/openshift/origin-console:latest with BRIDGE_K8S_MODE=off-cluster and BRIDGE_USER_AUTH=disabled, and a NodePort Service on port 30443. Wait for the CSV to reach Succeeded and the pod to be Ready.
5e. Verify console health: curl -sf -o /dev/null http://localhost:30443/health
5f. Collect credentials: Extract api_server, token, kubeconfig_path, ca_cert_path, context_name, and console_url. Save to $WORK_DIR/cluster-credentials.json.
5g. Determine console plugin dev command:
- Read cluster credentials for
api_server and token
- Detect plugin name from
package.json name field
- Check for console start script (
ci/start-console.sh or console script in package.json)
CRITICAL — npm start and webpack dev servers are long-running processes that NEVER exit on their own.
They start an HTTP server and keep running indefinitely to serve requests. If you run them without &, your session WILL hang indefinitely and never recover.
You MUST construct the dev command as a self-contained shell script ($WORK_DIR/start-dev.sh) that handles cleanup, backgrounding, PID tracking, logging, and readiness polling internally. Never run npm start or dev server commands directly in the shell tool — always go through the script.
Also create a companion $WORK_DIR/stop-dev.sh script for clean shutdown.
Do not run the dev command here to test it. Just construct and save it. It will be executed later with proper background management and readiness polling.
- If script exists: Before using it, read the script contents to determine how it starts the console bridge. Check whether the script runs
podman run or docker run with or without the -d (detach) flag:
- If the script runs the container without
-d (foreground/blocking), the script itself will never exit. You must run the script in the background with & and capture its PID.
- If the script runs the container with
-d (detached mode), the container starts in the background and the script exits on its own. Do not background the script with & — let it run to completion so any startup errors are reported.
- If you cannot determine the behavior from reading the script, default to running it in the background with
& to avoid hanging the session.
start-dev.sh Template
Every start-dev.sh script MUST follow this structure. The script handles its own backgrounding — callers run it with bash $WORK_DIR/start-dev.sh (no & needed).
Required elements:
- Port cleanup — kill any leftover processes on ports 9001 and 9000 from previous failed runs
nohup + log redirection — prevent shell timeout from killing the process; write output to log files
- PID files — write PIDs to
$WORK_DIR/*.pid for clean shutdown
- Readiness polling — poll each port with
curl, accepting exit code 22 (HTTP 404) for port 9001
- Exit immediately — the script must exit after starting and verifying servers, not block
Example (blocking console script — run with &):
#!/bin/bash
WORK_DIR=<work_dir>
fuser -k 9001/tcp 2>/dev/null || true
fuser -k 9000/tcp 2>/dev/null || true
podman stop migration-console okd-console 2>/dev/null || docker stop migration-console okd-console 2>/dev/null || true
sleep 1
cd <project_path>
nohup npm start > "$WORK_DIR/webpack.log" 2>&1 &
echo $! > "$WORK_DIR/webpack.pid"
for i in $(seq 1 60); do
curl -sf -o /dev/null http://localhost:9005 2>/dev/null; rc=$?
[ $rc -eq 0 ] || [ $rc -eq 22 ] && break
sleep 2
done
BRIDGE_K8S_MODE_OFF_CLUSTER_ENDPOINT=<api_server> \
BRIDGE_K8S_AUTH_BEARER_TOKEN=<token> \
nohup bash ./ci/start-console.sh > "$WORK_DIR/bridge.log" 2>&1 &
echo $! > "$WORK_DIR/bridge.pid"
for i in $(seq 1 60); do
curl -sf -o /dev/null http://localhost:9000 2>/dev/null && break
sleep 2
done
echo "Dev servers ready"
Example (detached console script — runs podman run -d and exits):
#!/bin/bash
WORK_DIR=<work_dir>
fuser -k 9001/tcp 2>/dev/null || true
fuser -k 9000/tcp 2>/dev/null || true
podman stop migration-console okd-console 2>/dev/null || docker stop migration-console okd-console 2>/dev/null || true
sleep 1
cd <project_path>
nohup npm start > "$WORK_DIR/webpack.log" 2>&1 &
echo $! > "$WORK_DIR/webpack.pid"
for i in $(seq 1 60); do
curl -sf -o /dev/null http://localhost:9005 2>/dev/null; rc=$?
[ $rc -eq 0 ] || [ $rc -eq 22 ] && break
sleep 2
done
BRIDGE_K8S_MODE_OFF_CLUSTER_ENDPOINT=<api_server> \
BRIDGE_K8S_AUTH_BEARER_TOKEN=<token> \
bash ./ci/start-console.sh > "$WORK_DIR/bridge.log" 2>&1
for i in $(seq 1 60); do
curl -sf -o /dev/null http://localhost:9000 2>/dev/null && break
sleep 2
done
echo "Dev servers ready"
stop-dev.sh
Also create $WORK_DIR/stop-dev.sh:
#!/bin/bash
WORK_DIR=<work_dir>
kill $(cat "$WORK_DIR/webpack.pid" 2>/dev/null) 2>/dev/null || true
kill $(cat "$WORK_DIR/bridge.pid" 2>/dev/null) 2>/dev/null || true
podman stop migration-console okd-console 2>/dev/null || docker stop migration-console okd-console 2>/dev/null || true
fuser -k 9001/tcp 2>/dev/null || true
fuser -k 9000/tcp 2>/dev/null || true
rm -f "$WORK_DIR/webpack.pid" "$WORK_DIR/bridge.pid"
- Both servers must be running: webpack dev server on port 9001 (serves plugin JS) and console bridge on port 9000 (provides the HTML shell that loads the plugin)
- Console dev URL is
http://localhost:9000
- Save the console dev command as a shell script file at
$WORK_DIR/start-dev.sh (not as a JSON string). Also save the URL to $WORK_DIR/console-dev-setup.json with a reference to the script path.
Step 6: Check Target Technology Specific Guidance
Look for a target-specific file in targets/. Match by lowercased target name without version numbers (e.g., "PatternFly 6" → patternfly.md, "Spring Boot 3.x" → spring-boot.md). Follow ALL pre-migration steps in the target file sequentially — each step must complete before the next one starts. Do not proceed to Phase 2 until all pre-migration steps are complete.
Phase 2: Fix Loop
Establish Test Baseline
Before making any code changes, run the full test suite to record pre-existing failures. Record results in $WORK_DIR/status.md:
## Test Baseline
- Total: [count]
- Passing: [count]
- Pre-existing failures:
- [test name]: [brief reason] (NOT migration-related)
Pre-existing failures do not count against the exit criteria. The exit check is: "no NEW test failures introduced by the migration." Compare test results against this baseline, not against zero failures.
First Round Only
Run initial analysis to create the fix plan:
-
Start the frontend analyzer provider — this must be running before Kantra is invoked:
cat > "$WORK_DIR/provider_settings.json" <<'PROVIDER_EOF'
[
{
"name": "frontend",
"address": "localhost:9005",
"initConfig": [
{
"analysisMode": "source-only",
"location": "<project>"
}
]
},
{
"name": "builtin",
"initConfig": [
{
"location": "<project>"
}
]
}
]
PROVIDER_EOF
nohup frontend-analyzer-provider serve -p 9005 > "$WORK_DIR/frontend-provider.log" 2>&1 &
echo $! > "$WORK_DIR/frontend-provider.pid"
for i in $(seq 1 30); do
curl -sf -o /dev/null http://localhost:9005 2>/dev/null; rc=$?
[ $rc -eq 0 ] || [ $rc -eq 22 ] && break
sleep 1
done
Replace <project> with the actual project path in the provider settings file.
-
Run Kantra — Kantra analysis can take 5-15 minutes and will exceed the shell timeout. Always run it in the background with nohup and redirect output to a log file.
Important: Kantra fails if the current directory is the input path. Always cd $WORK_DIR before running. Also set JAVA_HOME if not already set:
cd "$WORK_DIR"
export JAVA_HOME="${JAVA_HOME:-$(dirname $(dirname $(readlink -f $(which java))))}"
nohup kantra analyze --input <project> --output $WORK_DIR/round-1/kantra --overwrite \
--override-provider-settings "$WORK_DIR/provider_settings.json" \
--enable-default-rulesets=false --skip-static-report --no-dependency-rules \
--mode source-only --run-local=true --provider=java \
--label-selector '!impact=frontend-testing' \
<FLAGS> > $WORK_DIR/round-1/kantra-run.log 2>&1 &
KANTRA_PID=$!
Poll for completion: while kill -0 $KANTRA_PID 2>/dev/null; do sleep 10; done
Check $WORK_DIR/round-1/kantra/output.yaml exists before proceeding. If Kantra failed, check $WORK_DIR/round-1/kantra-run.log for errors.
Note: The <FLAGS> placeholder is replaced with flags from the kantra command builder. The flags above (--override-provider-settings, --enable-default-rulesets=false, etc.) are always included. If the command builder returns any of these same flags, do not duplicate them.
-
Parse Kantra output using the helper script:
- Overview:
python3 scripts/kantra_output_helper.py analyze $WORK_DIR/round-1/kantra/output.yaml
- File details:
python3 scripts/kantra_output_helper.py file $WORK_DIR/round-1/kantra/output.yaml <file>
-
Run build and lint commands
-
Run unit tests
-
Collect ALL issues from ALL sources (see Issue Sources table)
-
Create $WORK_DIR/status.md using the template below
Fix Loop Template
Create $WORK_DIR/status.md:
# Migration Status
## Groups
- [ ] Group 1: [Name] - [Brief description]
- [ ] Group 2: [Name] - [Brief description]
- [ ] Group 3: [Name] - [Brief description]
## Group Details
### Group 1: [Name]
**Why grouped**: [Related issues, same subsystem, etc.]
**Issues**:
- [Issue from Kantra/build/lint/tests]
- [Issue from Kantra/build/lint/tests]
**Files**: [file1.ts, file2.ts]
### Group 2: [Name]
...
## Round Log
(Append after each round)
Each Round
Round Checklist:
- [ ] Pick next incomplete group
- [ ] Apply fixes for that group
- [ ] Run Kantra + build + lint + unit tests
- [ ] Mark group complete in status.md
- [ ] Add new issues to plan if any appeared
- Pick: Select first incomplete group from status.md
- Fix: Apply all fixes for that group. Before renaming any prop or API based on Kantra suggestions, verify the new name exists in the target framework's type definitions (e.g., check the
.d.ts files or run tsc --noEmit). Kantra rules may suggest renames that are not yet reflected in the installed version's types — applying them blindly will break the build.
- Validate: Ensure the frontend analyzer provider is still running (
kill -0 $(cat $WORK_DIR/frontend-provider.pid) 2>/dev/null — restart if needed). Run Kantra (in background with nohup as described above, including all --override-provider-settings and related flags), build, lint, unit tests
- Update: Mark group done, log the round
Run tests concisely:
- Capture output, report only failures
- Format:
PASS: X tests, FAIL: Y tests
- For failures, show test name and error message only
Append to status.md:
### Round N: [Group Name]
- Fixed: [count] issues
- New issues: [count or "none"]
- Build: PASS/FAIL
- Tests: PASS/FAIL/NONE
Exit Check
After each round, check:
| Condition | Done? |
|---|
| All groups complete | ☐ |
| Kantra: 0 real issues (false positives documented in status.md) | ☐ |
| Build: passes | ☐ |
| Unit tests: no new failures vs baseline | ☐ |
- Any unchecked → Continue loop (next group)
- All checked → Proceed to Phase 3
If Stuck (Same Issue 3+ Rounds)
When an issue persists across 3+ rounds, analyze it:
Run persistent issues script:
python3 scripts/persistent_issues_analyzer.py $WORK_DIR
For each persistent issue, determine:
| Question | Check |
|---|
| False positive? | Rule too strict? Pattern actually valid? |
| Fixable? | Multiple approaches failed? Needs manual decision? |
| Blocking factor? | External deps? Domain knowledge needed? |
Categorize:
- Fix: Real issue, try different approach
- Ignore: False positive, document why in status.md
- Document: Real but needs manual intervention, add to status.md
Phase 3: Final Validation
Run E2E/behavioral tests and complete target-specific validation.
E2E Testing
- Run E2E test command discovered in Phase 1
- Report results concisely (pass/fail counts, failure details)
- If tests FAIL → Fix issues, re-run
- If tests PASS → Continue to target validation
Console Plugin Cluster Validation
Only perform this section if IS_CONSOLE_PLUGIN=true (detected in Phase 1 step 2).
Console dynamic plugins must be tested inside an OpenShift Console to verify they load, register their extensions, and render correctly.
1. Load cluster credentials: Read $WORK_DIR/cluster-credentials.json (created in Phase 1). Verify cluster is still running by checking connectivity to the api_server. If not responsive, re-provision the cluster (repeat Phase 1 steps 5a-5f) and update $WORK_DIR/cluster-credentials.json.
2. Build and deploy plugin: Build the plugin container image, load it into kind, create a Deployment + Service + ConsolePlugin CR in the cluster. Use existing deployment manifests if available.
3. Verify plugin: Confirm the plugin appears in the console and check logs for loading errors.
Log the result in $WORK_DIR/status.md:
### Cluster Validation
- Console plugin deployed: YES/NO
- Plugin loaded in console: YES/NO
- Console URL: <url>
Target Validation
Follow all post-migration steps in targets/<target>.md. These steps are mandatory — do not skip them. The migration is not complete until all post-migration validation passes.
Exit Criteria
All must be checked:
Update status.md:
## Complete
- Total rounds: N
- Build: PASS
- Unit tests: PASS
- E2E tests: PASS
- Target validation: PASS
- Console plugin validation: PASS/SKIP
Phase 4: Report
Before generating the report, write the final ## Action Required section to status.md. This must reflect the end state of the migration, including visual fixes.
- Read
$WORK_DIR/visual-diff-report.md — check for any unchecked ([ ]) issues that remain after the visual fix loop
- Read
$WORK_DIR/visual-fixes.md — understand what visual issues were fixed and how
- Remove any
visual_review items from Action Required that were resolved by the visual-fix agent (i.e., the corresponding issues are now [x] in the diff report)
- Add any new items discovered during visual fixing that need user attention (e.g., unfixable visual differences)
Append the final ## Action Required section to status.md listing every item the user should still review. Use bullet format with type prefix:
## Action Required
- **Unresolved Issue**: [description] → [recommendation]
- **False Positive**: [description] → [recommendation]
- **Visual Review** (page: [name]): [description] → [recommendation]
- **Manual Intervention**: [description] → [recommendation]
If nothing requires review:
## Action Required
None
1. Read status.md
Read $WORK_DIR/status.md and extract:
- Complete section: total rounds, build/test/validation status
- Groups: list of groups with their completion status
- Group Details: for each group — name, description, files, issues
- Round Log: each round's fixed count, new issues, build/test results
- Action Required section: items the user must review (unresolved issues, false positives, visual reviews, manual interventions)
2. Read Kantra Assessment
Check for the latest Kantra output directory ($WORK_DIR/round-*/kantra/output.yaml). If a final round exists, note any residual incidents (rule, count, reason for keeping).
If no Kantra output exists, set kantra_residual.total_incidents to 0.
3. Read Visual Comparison Report
If $WORK_DIR/visual-diff-report.md exists, extract per-page results (page name, status, notes). Map unchecked items ([ ]) to fail status and checked items ([x]) to pass.
4. List Screenshot Directories
Check for $WORK_DIR/baseline/ and $WORK_DIR/post-migration/ directories. List filenames in each to populate the visual pages array.
If neither directory exists, set visual.has_screenshots to false.
5. Build report-data.json
Create $WORK_DIR/report-data.json using the following schema:
{
"migration": {
"source": "string",
"target": "string",
"project": "string (project path)",
"timestamp": "ISO 8601",
"workspace": "string (workspace path)"
},
"summary": {
"total_rounds": "number",
"status": "complete|incomplete",
"build": "PASS|FAIL",
"unit_tests": "PASS|FAIL|NONE",
"e2e_tests": "PASS|FAIL|NONE",
"lint": "PASS|FAIL|NONE",
"target_validation": "PASS|FAIL|NONE"
},
"action_required": [
{
"type": "unresolved_issue|false_positive|visual_review|manual_intervention",
"description": "string",
"recommendation": "string (optional)",
"details": "string (optional)",
"page": "string (optional, for visual_review)"
}
],
"groups": [
{
"name": "string",
"status": "complete|incomplete",
"issues_fixed": "number",
"files": ["string"],
"description": "string"
}
],
"rounds": [
{
"number": "number",
"group": "string",
"issues_fixed": "number",
"new_issues": "number",
"build": "PASS|FAIL",
"tests": "string (e.g. '265/265' or '225/262 (37 snapshot mismatches)')"
}
],
"visual": {
"has_screenshots": "boolean",
"baseline_dir": "string (relative to work_dir)",
"post_migration_dir": "string (relative to work_dir)",
"pages": [
{
"name": "string",
"baseline": "string (filename, e.g. 'login.png')",
"post_migration": "string (filename, e.g. 'login.png')",
"status": "pass|fail|info",
"notes": "string"
}
]
},
"kantra_residual": {
"total_incidents": "number",
"categories": [
{
"rule": "string",
"count": "number",
"reason": "string"
}
]
}
}
Field population rules:
migration.source / migration.target: from the source and target technologies
migration.project: the project path
migration.timestamp: current time in ISO 8601
migration.workspace: $WORK_DIR
summary: from the Complete section of status.md
action_required: from the Action Required section of status.md. Parse each bullet into type, description, and recommendation. If "None", use an empty array.
groups: from Groups and Group Details sections. Mark [x] groups as "complete", [ ] as "incomplete". Count issues from Group Details. Extract files list.
rounds: from Round Log entries. Parse fixed count, new issues count, build and test results.
visual: from screenshot directories and visual-diff-report.md. If no screenshots exist, set has_screenshots to false and omit pages.
kantra_residual: from the latest Kantra output. If 0 residual issues, set total_incidents to 0 and categories to empty array.
6. Read Visual Fixes
If $WORK_DIR/visual-fixes.md exists, read it to understand what visual issues were fixed and how. Use this to verify consistency of action_required:
- If a
visual_review item in action_required refers to an issue that is now [x] in visual-diff-report.md, remove it from action_required — it was fixed.
- If
visual-diff-report.md has unchecked ([ ]) issues that are not represented in action_required, add them as visual_review items.
- If
visual-fixes.md documents fixes that contradict notes in action_required (e.g., an item says "not fixable" but visual-fixes.md shows it was fixed), update accordingly.
7. Verify Consistency
Before writing report-data.json, cross-check the data:
summary.status should be complete only if all groups are complete, build passes, and tests pass
action_required should not contain items that are contradicted by other artifacts (e.g., visual issues marked as needing review but already [x] in the diff report)
visual.pages status values should match visual-diff-report.md — [x] → pass, [ ] → fail
- Every screenshot file referenced in
visual.pages should exist in the baseline and post-migration directories
8. Stop Frontend Analyzer Provider
Stop the frontend analyzer provider — it is no longer needed after analysis is complete:
kill $(cat "$WORK_DIR/frontend-provider.pid" 2>/dev/null) 2>/dev/null || true
rm -f "$WORK_DIR/frontend-provider.pid"
9. Generate HTML Report
Run:
python3 scripts/generate_migration_report.py $WORK_DIR
Tell the user the path to the generated report.html.
Guidelines
- One group per round for clear feedback
- Follow planned order - foundation before dependent changes
- Verify each fix - don't break existing features
- Document unfixable issues after 2+ failed approaches
- Use all issue sources - Kantra is just one input
- Report test results concisely - counts and failures only, not full output
- Never renumber, relabel, or remove groups from status.md. Groups are numbered when the plan is created and those numbers are permanent. If a group cannot be fixed after 2+ attempts, mark it as
[!] Group N: [Name] - UNFIXABLE: [reason] — do not delete it, reassign its number to another group, or merge it into a different group.
- Attempt every group. Do not skip a group because it uses deprecated-but-functional APIs. If Kantra flags it, attempt the migration to the new API. Only classify a group as unfixable if the fix breaks the build or tests after 2+ different approaches.
- NEVER run dev servers directly in the shell tool —
npm start, webpack serve, npm run dev, and similar dev server commands are long-running and will hang the shell. Always use $WORK_DIR/start-dev.sh to start them and $WORK_DIR/stop-dev.sh to stop them. These scripts handle backgrounding, PID tracking, log redirection, port cleanup, and readiness polling internally. Never construct dev server commands inline.
- Before starting dev servers, always clean up leftover processes — run
bash $WORK_DIR/stop-dev.sh first, or at minimum fuser -k 9001/tcp 2>/dev/null; fuser -k 9000/tcp 2>/dev/null to avoid EADDRINUSE errors from previous failed runs.