@samahlstrom/forge-cli 0.1.0

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 (100) hide show
  1. package/README.md +175 -0
  2. package/bin/forge.js +2 -0
  3. package/dist/addons/index.d.ts +25 -0
  4. package/dist/addons/index.js +139 -0
  5. package/dist/addons/index.js.map +1 -0
  6. package/dist/commands/add.d.ts +1 -0
  7. package/dist/commands/add.js +61 -0
  8. package/dist/commands/add.js.map +1 -0
  9. package/dist/commands/doctor.d.ts +1 -0
  10. package/dist/commands/doctor.js +177 -0
  11. package/dist/commands/doctor.js.map +1 -0
  12. package/dist/commands/ingest.d.ts +24 -0
  13. package/dist/commands/ingest.js +316 -0
  14. package/dist/commands/ingest.js.map +1 -0
  15. package/dist/commands/init.d.ts +8 -0
  16. package/dist/commands/init.js +557 -0
  17. package/dist/commands/init.js.map +1 -0
  18. package/dist/commands/remove.d.ts +1 -0
  19. package/dist/commands/remove.js +42 -0
  20. package/dist/commands/remove.js.map +1 -0
  21. package/dist/commands/status.d.ts +1 -0
  22. package/dist/commands/status.js +48 -0
  23. package/dist/commands/status.js.map +1 -0
  24. package/dist/commands/upgrade.d.ts +5 -0
  25. package/dist/commands/upgrade.js +190 -0
  26. package/dist/commands/upgrade.js.map +1 -0
  27. package/dist/detect/features.d.ts +10 -0
  28. package/dist/detect/features.js +33 -0
  29. package/dist/detect/features.js.map +1 -0
  30. package/dist/detect/go.d.ts +3 -0
  31. package/dist/detect/go.js +38 -0
  32. package/dist/detect/go.js.map +1 -0
  33. package/dist/detect/index.d.ts +25 -0
  34. package/dist/detect/index.js +32 -0
  35. package/dist/detect/index.js.map +1 -0
  36. package/dist/detect/node.d.ts +3 -0
  37. package/dist/detect/node.js +99 -0
  38. package/dist/detect/node.js.map +1 -0
  39. package/dist/detect/python.d.ts +3 -0
  40. package/dist/detect/python.js +86 -0
  41. package/dist/detect/python.js.map +1 -0
  42. package/dist/index.d.ts +1 -0
  43. package/dist/index.js +51 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/render/engine.d.ts +8 -0
  46. package/dist/render/engine.js +71 -0
  47. package/dist/render/engine.js.map +1 -0
  48. package/dist/render/merge.d.ts +5 -0
  49. package/dist/render/merge.js +33 -0
  50. package/dist/render/merge.js.map +1 -0
  51. package/dist/utils/fs.d.ts +8 -0
  52. package/dist/utils/fs.js +42 -0
  53. package/dist/utils/fs.js.map +1 -0
  54. package/dist/utils/git.d.ts +3 -0
  55. package/dist/utils/git.js +31 -0
  56. package/dist/utils/git.js.map +1 -0
  57. package/dist/utils/hash.d.ts +8 -0
  58. package/dist/utils/hash.js +22 -0
  59. package/dist/utils/hash.js.map +1 -0
  60. package/dist/utils/yaml.d.ts +3 -0
  61. package/dist/utils/yaml.js +12 -0
  62. package/dist/utils/yaml.js.map +1 -0
  63. package/package.json +53 -0
  64. package/templates/addons/beads-dolt-backend/files/dolt-setup.sh +267 -0
  65. package/templates/addons/beads-dolt-backend/manifest.yaml +13 -0
  66. package/templates/addons/browser-testing/files/browser-smoke.sh +196 -0
  67. package/templates/addons/browser-testing/files/visual-qa.md +103 -0
  68. package/templates/addons/browser-testing/manifest.yaml +20 -0
  69. package/templates/addons/compliance-hipaa/files/hipaa-checks.sh +184 -0
  70. package/templates/addons/compliance-hipaa/files/hipaa-context.md +91 -0
  71. package/templates/addons/compliance-hipaa/manifest.yaml +15 -0
  72. package/templates/addons/compliance-soc2/files/soc2-checks.sh +232 -0
  73. package/templates/addons/compliance-soc2/files/soc2-context.md +147 -0
  74. package/templates/addons/compliance-soc2/manifest.yaml +15 -0
  75. package/templates/core/CLAUDE.md.hbs +70 -0
  76. package/templates/core/agents/architect.md.hbs +68 -0
  77. package/templates/core/agents/backend.md.hbs +27 -0
  78. package/templates/core/agents/frontend.md.hbs +25 -0
  79. package/templates/core/agents/quality.md.hbs +40 -0
  80. package/templates/core/agents/security.md.hbs +53 -0
  81. package/templates/core/context/project.md.hbs +60 -0
  82. package/templates/core/forge.yaml.hbs +69 -0
  83. package/templates/core/hooks/post-edit.sh.hbs +8 -0
  84. package/templates/core/hooks/pre-edit.sh.hbs +41 -0
  85. package/templates/core/hooks/session-start.sh.hbs +34 -0
  86. package/templates/core/pipeline/classify.sh.hbs +159 -0
  87. package/templates/core/pipeline/decompose.md.hbs +100 -0
  88. package/templates/core/pipeline/deliver.sh.hbs +171 -0
  89. package/templates/core/pipeline/execute.md.hbs +138 -0
  90. package/templates/core/pipeline/intake.sh.hbs +152 -0
  91. package/templates/core/pipeline/orchestrator.sh.hbs +361 -0
  92. package/templates/core/pipeline/verify.sh.hbs +160 -0
  93. package/templates/core/settings.json.hbs +55 -0
  94. package/templates/core/skill-creator.md.hbs +151 -0
  95. package/templates/core/skill-deliver.md.hbs +46 -0
  96. package/templates/core/skill-ingest.md.hbs +245 -0
  97. package/templates/presets/go/stack.md.hbs +133 -0
  98. package/templates/presets/python-fastapi/stack.md.hbs +101 -0
  99. package/templates/presets/react-next-ts/stack.md.hbs +77 -0
  100. package/templates/presets/sveltekit-ts/stack.md.hbs +116 -0
@@ -0,0 +1,100 @@
1
+ # Architect Agent: Task Decomposition
2
+
3
+ You are the **architect agent** for Forge. Your job is to break a work item into parallel-safe subtasks organized into execution waves.
4
+
5
+ ## Input
6
+
7
+ **Task ID:** `{{bead_id}}`
8
+ **Title:** {{title}}
9
+ **Description:**
10
+ {{description}}
11
+
12
+ **Risk Tier:** {{tier}}
13
+ **Mode:** {{mode}}
14
+
15
+ ### Project Context
16
+ Read these files for architectural understanding:
17
+ {{#each context_files}}
18
+ - `{{this}}`
19
+ {{/each}}
20
+
21
+ ## Your Task
22
+
23
+ Analyze the work description and produce a decomposition plan. You must output **valid JSON** matching the schema below.
24
+
25
+ ## Constraints
26
+
27
+ 1. **Max subtasks:** 8 (prefer fewer; most work needs 2-4)
28
+ 2. **Max waves:** 4 (prefer fewer)
29
+ 3. **No circular dependencies:** If A depends on B, B cannot depend on A
30
+ 4. **No file conflicts within a wave:** Two subtasks in the same wave CANNOT modify the same file
31
+ 5. **Each subtask must be independently verifiable:** It should compile/typecheck on its own after the wave completes
32
+ 6. **Respect the risk tier:**
33
+ - T1: Minimal decomposition needed; 1-2 subtasks is fine
34
+ - T2: Break along service/module boundaries
35
+ - T3: Isolate security-critical changes into their own subtask with explicit verification
36
+ 7. **Agent assignment:** Each subtask gets an agent type:
37
+ - `code` — writes implementation code
38
+ - `test` — writes tests
39
+ - `docs` — writes documentation
40
+ - `config` — modifies configuration files
41
+
42
+ ## Analysis Steps
43
+
44
+ Before producing output, think through:
45
+
46
+ 1. **What files will be touched?** List every file that needs to change.
47
+ 2. **What are the dependency relationships?** Which changes must happen before others?
48
+ 3. **Where are the file conflicts?** Group non-conflicting changes into waves.
49
+ 4. **What is the verification for each subtask?** How do you know it worked?
50
+
51
+ ## Output Schema
52
+
53
+ ```json
54
+ {
55
+ "analysis": {
56
+ "files_affected": ["src/lib/foo.ts", "src/routes/bar/+page.svelte"],
57
+ "dependency_graph": "A -> B means B depends on A",
58
+ "risk_notes": "Any special concerns for this tier"
59
+ },
60
+ "subtasks": [
61
+ {
62
+ "id": "sub-1",
63
+ "title": "Short description of the subtask",
64
+ "agent": "code",
65
+ "files": ["src/lib/foo.ts", "src/lib/foo.test.ts"],
66
+ "dependsOn": [],
67
+ "verification": "npm run check passes; foo.test.ts passes",
68
+ "instructions": "Detailed instructions for what the agent should do. Be specific about function signatures, data shapes, and integration points."
69
+ }
70
+ ],
71
+ "waves": [
72
+ {
73
+ "id": "wave-1",
74
+ "subtasks": ["sub-1", "sub-2"],
75
+ "gate": "typecheck"
76
+ },
77
+ {
78
+ "id": "wave-2",
79
+ "subtasks": ["sub-3"],
80
+ "gate": "typecheck + test"
81
+ }
82
+ ],
83
+ "verification_plan": {
84
+ "after_all_waves": "Full test suite, lint, typecheck",
85
+ "manual_checks": ["Describe any checks that need human verification"]
86
+ }
87
+ }
88
+ ```
89
+
90
+ ## Rules for Good Decomposition
91
+
92
+ - **Prefer wide waves over deep chains.** 3 subtasks in wave-1 is better than 3 sequential waves of 1 subtask each.
93
+ - **Tests go in the same wave as the code they test** (same subtask if the file set is small, separate subtask if large).
94
+ - **Type definitions and interfaces go in wave-1.** Downstream code depends on them.
95
+ - **Database migrations go in their own subtask** in wave-1, before any code that uses the new schema.
96
+ - **Each subtask's `instructions` field should be detailed enough** that an agent with no prior context can execute it. Include: what to create/modify, expected function signatures, data flow, and how it integrates with existing code.
97
+
98
+ ## Output
99
+
100
+ Respond with **only** the JSON object. No markdown fences, no explanation, no commentary. Just the JSON.
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env bash
2
+ # forge pipeline — deliver: branch, commit, push, PR
3
+ # Generated by: forge init
4
+ # Uses bd (steveyegge/beads) for task tracking
5
+ set -euo pipefail
6
+
7
+ FORGE_DIR=".forge"
8
+ PROMPTS_DIR="${FORGE_DIR}/pipeline"
9
+
10
+ TASK_ID="${1:-}"
11
+ PR_BODY_FILE="${2:-}"
12
+
13
+ if [[ -z "$TASK_ID" ]]; then
14
+ echo '{"error":"Usage: deliver.sh <task-id> [pr-body-file]"}' >&2
15
+ exit 1
16
+ fi
17
+
18
+ # --- Read task state from bd ---
19
+
20
+ TASK_JSON=$(bd show "$TASK_ID" --json 2>/dev/null)
21
+ TITLE=$(echo "$TASK_JSON" | jq -r '.title // "untitled"')
22
+ DESCRIPTION=$(echo "$TASK_JSON" | jq -r '.description // ""')
23
+
24
+ # Extract tier and mode from labels
25
+ TIER=$(echo "$TASK_JSON" | jq -r '.labels[]? // empty' | grep "^tier:" | head -1 | sed 's/^tier://')
26
+ TIER="${TIER:-T1}"
27
+ MODE=$(echo "$TASK_JSON" | jq -r '.labels[]? // empty' | grep "^mode:" | head -1 | sed 's/^mode://')
28
+ MODE="${MODE:-normal}"
29
+
30
+ # Detect modified files from git
31
+ MODIFIED_FILES=$(git diff --name-only HEAD 2>/dev/null || true)
32
+ if [[ -z "$MODIFIED_FILES" ]]; then
33
+ MODIFIED_FILES=$(git diff --cached --name-only 2>/dev/null || true)
34
+ fi
35
+ if [[ -z "$MODIFIED_FILES" ]]; then
36
+ MODIFIED_FILES=$(git status --porcelain | grep -v '^\?' | awk '{print $2}')
37
+ fi
38
+
39
+ if [[ -z "$MODIFIED_FILES" ]]; then
40
+ echo '{"error":"No modified files found to deliver"}' >&2
41
+ exit 1
42
+ fi
43
+
44
+ # --- Generate slug from title ---
45
+
46
+ slugify() {
47
+ echo "$1" | tr '[:upper:]' '[:lower:]' | \
48
+ sed 's/[^a-z0-9]/-/g' | \
49
+ sed 's/--*/-/g' | \
50
+ sed 's/^-//' | \
51
+ sed 's/-$//' | \
52
+ cut -c1-40
53
+ }
54
+
55
+ SLUG=$(slugify "$TITLE")
56
+ BRANCH="feature/${TASK_ID}-${SLUG}"
57
+
58
+ # --- Phase 1: Branch + commit + push ---
59
+
60
+ if [[ -z "$PR_BODY_FILE" ]]; then
61
+ # Create branch
62
+ CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || echo "")
63
+ if [[ "$CURRENT_BRANCH" != "$BRANCH" ]]; then
64
+ git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH" 2>/dev/null || {
65
+ echo "{\"error\":\"Failed to create or switch to branch ${BRANCH}\"}" >&2
66
+ exit 1
67
+ }
68
+ fi
69
+
70
+ # Stage files explicitly
71
+ while IFS= read -r file; do
72
+ [[ -z "$file" ]] && continue
73
+ if [[ -f "$file" ]]; then
74
+ git add "$file"
75
+ fi
76
+ done <<< "$MODIFIED_FILES"
77
+
78
+ # Build commit message
79
+ local_type="feat"
80
+ if echo "$TITLE" | grep -qiE '(fix|bug|patch|repair|correct)'; then
81
+ local_type="fix"
82
+ elif echo "$TITLE" | grep -qiE '(refactor|cleanup|reorganize)'; then
83
+ local_type="refactor"
84
+ elif echo "$TITLE" | grep -qiE '(doc|readme|comment)'; then
85
+ local_type="docs"
86
+ elif echo "$TITLE" | grep -qiE '(test|spec)'; then
87
+ local_type="test"
88
+ fi
89
+
90
+ COMMIT_MSG="${local_type}(${SLUG}): ${TITLE}
91
+
92
+ Task: ${TASK_ID}
93
+ Risk: ${TIER}"
94
+
95
+ git commit -m "$COMMIT_MSG" || {
96
+ echo "{\"error\":\"Commit failed\"}" >&2
97
+ exit 1
98
+ }
99
+
100
+ # Push branch
101
+ git push -u origin "$BRANCH" 2>/dev/null || {
102
+ echo "{\"error\":\"Push failed. You may need to set up remote.\"}" >&2
103
+ exit 1
104
+ }
105
+
106
+ # PAUSE: ask LLM to write PR body
107
+ local task_dir="${FORGE_DIR}/pipeline/runs/${TASK_ID}"
108
+ mkdir -p "$task_dir"
109
+ PR_BODY_OUTPUT="${task_dir}/pr-body.md"
110
+
111
+ jq -n \
112
+ --arg status "PAUSE" \
113
+ --arg task "write-pr-body" \
114
+ --arg prompt_file "${PROMPTS_DIR}/pr-body-prompt.md" \
115
+ --arg output_file "$PR_BODY_OUTPUT" \
116
+ --arg branch "$BRANCH" \
117
+ --arg resume "bash ${FORGE_DIR}/pipeline/deliver.sh ${TASK_ID} ${PR_BODY_OUTPUT}" \
118
+ --argjson context '["{{stackFile}}","{{projectFile}}"]' \
119
+ '{status:$status, task:$task, prompt_file:$prompt_file, output_file:$output_file, branch:$branch, context:$context, resume:$resume}'
120
+ exit 0
121
+ fi
122
+
123
+ # --- Phase 2: Create PR with body ---
124
+
125
+ if [[ ! -f "$PR_BODY_FILE" ]]; then
126
+ echo "{\"error\":\"PR body file not found: ${PR_BODY_FILE}\"}" >&2
127
+ exit 1
128
+ fi
129
+
130
+ PR_BODY=$(cat "$PR_BODY_FILE")
131
+
132
+ PR_TITLE="${TITLE}"
133
+ if [[ ${#PR_TITLE} -gt 70 ]]; then
134
+ PR_TITLE=$(echo "$PR_TITLE" | cut -c1-67)"..."
135
+ fi
136
+
137
+ # Add risk badge
138
+ RISK_BADGE=""
139
+ case "$TIER" in
140
+ T1) RISK_BADGE="<!-- risk:T1 --> **Risk: T1** (Low)" ;;
141
+ T2) RISK_BADGE="<!-- risk:T2 --> **Risk: T2** (Moderate)" ;;
142
+ T3) RISK_BADGE="<!-- risk:T3 --> **Risk: T3** (Critical)" ;;
143
+ esac
144
+
145
+ FULL_BODY="${RISK_BADGE}
146
+
147
+ ${PR_BODY}
148
+
149
+ ---
150
+ Task: \`${TASK_ID}\`"
151
+
152
+ PR_URL=$(gh pr create \
153
+ --title "$PR_TITLE" \
154
+ --body "$FULL_BODY" \
155
+ --head "$BRANCH" 2>/dev/null) || {
156
+ echo "{\"error\":\"gh pr create failed. Is gh authenticated?\"}" >&2
157
+ exit 1
158
+ }
159
+
160
+ # Add labels based on tier
161
+ case "$TIER" in
162
+ T3) gh pr edit "$PR_URL" --add-label "critical,security-review" 2>/dev/null || true ;;
163
+ T2) gh pr edit "$PR_URL" --add-label "needs-review" 2>/dev/null || true ;;
164
+ esac
165
+
166
+ jq -n \
167
+ --arg pr_url "$PR_URL" \
168
+ --arg branch "$BRANCH" \
169
+ --arg task_id "$TASK_ID" \
170
+ --arg tier "$TIER" \
171
+ '{pr_url:$pr_url, branch:$branch, task_id:$task_id, tier:$tier}'
@@ -0,0 +1,138 @@
1
+ # Execution Dispatcher: Wave-by-Wave Agent Orchestration
2
+
3
+ You are the **execution dispatcher** for Forge. Your job is to execute a decomposition plan by launching agents wave-by-wave, verifying between waves, and tracking progress via `bd`.
4
+
5
+ ## Input
6
+
7
+ **Task ID:** `{{bead_id}}`
8
+ **Title:** {{title}}
9
+ **Risk Tier:** {{tier}}
10
+
11
+ ### Decomposition Plan
12
+ The decomposition is at: `{{decomposition_file}}`
13
+
14
+ Read it now. It contains `subtasks[]` and `waves[]`.
15
+
16
+ ### Project Context
17
+ {{#each context_files}}
18
+ - `{{this}}`
19
+ {{/each}}
20
+
21
+ ## Execution Protocol
22
+
23
+ For each wave in order:
24
+
25
+ ### 1. Pre-Wave Setup
26
+
27
+ - Read the wave definition to get the list of subtask IDs
28
+ - For each subtask in this wave, read its `files[]`, `instructions`, and `verification`
29
+
30
+ ### 2. Execute Subtasks in Parallel
31
+
32
+ For each subtask in the wave, launch a subagent with this context:
33
+
34
+ **Subagent prompt template:**
35
+ ```
36
+ You are a {{subtask.agent}} agent. Your task:
37
+
38
+ Title: {{subtask.title}}
39
+
40
+ Instructions:
41
+ {{subtask.instructions}}
42
+
43
+ Files to modify:
44
+ {{#each subtask.files}}
45
+ - {{this}}
46
+ {{/each}}
47
+
48
+ Verification: {{subtask.verification}}
49
+
50
+ Rules:
51
+ - Only modify the files listed above
52
+ - Follow the project conventions in the context files
53
+ - After making changes, verify: {{subtask.verification}}
54
+ - If you encounter an error you cannot resolve, output:
55
+ {"status": "blocked", "subtask": "{{subtask.id}}", "error": "<description>"}
56
+ ```
57
+
58
+ Each subagent receives:
59
+ 1. The subagent prompt above
60
+ 2. The project context files (stack.md, project.md)
61
+ 3. The agent.md file matching its agent type (if it exists)
62
+
63
+ ### 3. Post-Wave Verification Gate
64
+
65
+ After ALL subtasks in a wave complete:
66
+
67
+ 1. **Run typecheck:** `{{commands.typecheck}}`
68
+ 2. **If the wave definition has `gate: "typecheck + test"`**, also run: `{{commands.test}}`
69
+
70
+ ### 4. Handle Failures
71
+
72
+ If the verification gate fails after a wave:
73
+
74
+ 1. **Identify the breaking subtask:**
75
+ - Check which files were modified in this wave
76
+ - Run typecheck/test with verbose output
77
+ - Match errors to specific files and subtasks
78
+
79
+ 2. **Retry the breaking subtask:**
80
+ - Provide the subagent with the error output as additional context
81
+ - Include the failing test output or typecheck errors
82
+ - Limit to 2 retries per subtask
83
+
84
+ 3. **If retry fails after 2 attempts:**
85
+ - Revert the breaking subtask's file changes: `git checkout -- <files>`
86
+ - Continue to the next wave (the breaking subtask becomes a "deferred" item)
87
+ - Track deferred subtasks for the PR summary
88
+
89
+ ## Progress Reporting
90
+
91
+ After each wave, output a progress update:
92
+
93
+ ```json
94
+ {
95
+ "wave": "wave-1",
96
+ "status": "complete",
97
+ "subtasks_completed": ["sub-1", "sub-2"],
98
+ "subtasks_failed": [],
99
+ "subtasks_deferred": [],
100
+ "verification": "passed",
101
+ "files_modified": ["src/lib/foo.ts", "src/lib/bar.ts"]
102
+ }
103
+ ```
104
+
105
+ ## Completion
106
+
107
+ After all waves are done:
108
+
109
+ 1. Run the full verification suite:
110
+ - `{{commands.typecheck}}`
111
+ - `{{commands.lint}}`
112
+ - `{{commands.test}}`
113
+
114
+ 2. Compile the execution summary:
115
+
116
+ ```json
117
+ {
118
+ "status": "complete",
119
+ "waves_executed": 2,
120
+ "subtasks_completed": ["sub-1", "sub-2", "sub-3"],
121
+ "subtasks_deferred": [],
122
+ "files_modified": ["list", "of", "all", "files"],
123
+ "verification": {
124
+ "typecheck": "passed",
125
+ "lint": "passed",
126
+ "test": "passed"
127
+ }
128
+ }
129
+ ```
130
+
131
+ 3. Write the summary to: `.forge/pipeline/runs/{{bead_id}}/execution.json`
132
+
133
+ ## Rules
134
+
135
+ - **Never skip a wave.** Execute them in order.
136
+ - **Never modify files not listed in a subtask's `files[]` array.**
137
+ - **If a subtask has `dependsOn` entries not in its wave, those were already completed in prior waves.** You can assume their output exists.
138
+ - **Keep each subagent focused.** It gets only its subtask, not the full decomposition.
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env bash
2
+ # forge pipeline — intake: parse input and score quality
3
+ # Generated by: forge init
4
+ set -euo pipefail
5
+
6
+ # --- Quality Scoring ---
7
+ # Scores a work description from 0.0 to 1.0 based on:
8
+ # What (25%) — Does it describe what to build/fix?
9
+ # Why (15%) — Does it explain why this is needed?
10
+ # Where (15%) — Does it mention files, components, or areas?
11
+ # Scope (15%) — Is the scope bounded (not "refactor everything")?
12
+ # Layers (20%)— Does it mention affected layers (UI, API, DB, etc.)?
13
+ # Criteria (10%) — Does it include acceptance criteria or done conditions?
14
+
15
+ score_quality() {
16
+ local desc="$1"
17
+ local desc_lower
18
+ desc_lower=$(echo "$desc" | tr '[:upper:]' '[:lower:]')
19
+ local total=0
20
+
21
+ # What (25 pts): presence of action verbs and object nouns
22
+ local what_score=0
23
+ if echo "$desc_lower" | grep -qE '(add|create|build|implement|fix|update|remove|delete|refactor|migrate|change|replace|move|rename|extract|split|merge)'; then
24
+ what_score=$((what_score + 15))
25
+ fi
26
+ if echo "$desc_lower" | grep -qE '(component|page|endpoint|api|function|service|hook|store|model|schema|table|route|handler|middleware|view|controller|module)'; then
27
+ what_score=$((what_score + 10))
28
+ fi
29
+ total=$((total + what_score))
30
+
31
+ # Why (15 pts): rationale keywords
32
+ local why_score=0
33
+ if echo "$desc_lower" | grep -qE '(because|so that|in order to|to enable|to fix|to prevent|to improve|to support|to allow|currently|problem|issue|bug|broken|incorrect|missing|needed)'; then
34
+ why_score=15
35
+ fi
36
+ total=$((total + why_score))
37
+
38
+ # Where (15 pts): file paths, component names, or area references
39
+ local where_score=0
40
+ if echo "$desc_lower" | grep -qE '(\.[a-z]{1,4}$|/[a-z]|src/|lib/|components/|pages/|routes/|api/|server/|client/|services/)'; then
41
+ where_score=$((where_score + 10))
42
+ fi
43
+ if echo "$desc_lower" | grep -qE '(in the|on the|at the|within|inside|under|header|footer|sidebar|navbar|modal|form|table|list|card|dashboard|settings|profile|login|signup)'; then
44
+ where_score=$((where_score + 5))
45
+ fi
46
+ total=$((total + where_score))
47
+
48
+ # Scope (15 pts): bounded descriptions score higher
49
+ local scope_score=10 # default: assume moderate scope
50
+ if echo "$desc_lower" | grep -qE '(all|every|entire|whole|everything|refactor the entire|rewrite)'; then
51
+ scope_score=0 # unbounded = bad
52
+ fi
53
+ if echo "$desc_lower" | grep -qE '(only|just|single|specific|one|the [a-z]+ (component|page|endpoint|function))'; then
54
+ scope_score=15 # well-bounded = good
55
+ fi
56
+ total=$((total + scope_score))
57
+
58
+ # Layers (20 pts): mentions of architectural layers
59
+ local layers_score=0
60
+ local layer_count=0
61
+ for layer in "ui" "frontend" "client" "backend" "server" "api" "database" "db" "schema" "migration" "state" "store" "style" "css" "test" "auth" "middleware"; do
62
+ if echo "$desc_lower" | grep -qw "$layer"; then
63
+ layer_count=$((layer_count + 1))
64
+ fi
65
+ done
66
+ if [[ $layer_count -ge 3 ]]; then
67
+ layers_score=20
68
+ elif [[ $layer_count -ge 2 ]]; then
69
+ layers_score=15
70
+ elif [[ $layer_count -ge 1 ]]; then
71
+ layers_score=10
72
+ fi
73
+ total=$((total + layers_score))
74
+
75
+ # Criteria (10 pts): acceptance criteria or done conditions
76
+ local criteria_score=0
77
+ if echo "$desc_lower" | grep -qE '(should|must|expect|when .* then|given .* when|acceptance|criteria|done when|verify that|ensure that|assert)'; then
78
+ criteria_score=10
79
+ fi
80
+ total=$((total + criteria_score))
81
+
82
+ # Normalize to 0.0-1.0
83
+ echo "scale=2; $total / 100" | bc | sed 's/^\./0./'
84
+ }
85
+
86
+ # --- Input Parsing ---
87
+
88
+ MODE="normal"
89
+ SOURCE="text"
90
+ ISSUE_NUMBER=""
91
+ DESCRIPTION=""
92
+ TITLE=""
93
+
94
+ while [[ $# -gt 0 ]]; do
95
+ case "$1" in
96
+ --quick)
97
+ MODE="quick"; shift ;;
98
+ --hotfix)
99
+ MODE="hotfix"; shift ;;
100
+ --issue)
101
+ ISSUE_NUMBER="$2"; SOURCE="issue"; shift 2 ;;
102
+ *)
103
+ if [[ -z "$DESCRIPTION" ]]; then
104
+ DESCRIPTION="$1"
105
+ else
106
+ DESCRIPTION="${DESCRIPTION} $1"
107
+ fi
108
+ shift ;;
109
+ esac
110
+ done
111
+
112
+ # Fetch from GitHub issue if --issue
113
+ if [[ "$SOURCE" == "issue" && -n "$ISSUE_NUMBER" ]]; then
114
+ if ! command -v gh &>/dev/null; then
115
+ echo '{"error":"gh CLI not installed, cannot fetch issue"}' >&2
116
+ exit 1
117
+ fi
118
+
119
+ issue_json=$(gh issue view "$ISSUE_NUMBER" --json title,body 2>/dev/null) || {
120
+ echo '{"error":"Failed to fetch issue '"$ISSUE_NUMBER"'"}' >&2
121
+ exit 1
122
+ }
123
+
124
+ TITLE=$(echo "$issue_json" | jq -r '.title // ""')
125
+ issue_body=$(echo "$issue_json" | jq -r '.body // ""')
126
+ DESCRIPTION="${TITLE}. ${issue_body}"
127
+ fi
128
+
129
+ if [[ -z "$DESCRIPTION" ]]; then
130
+ echo '{"error":"No work description provided. Usage: forge work \"<description>\" | --issue N"}' >&2
131
+ exit 1
132
+ fi
133
+
134
+ # Generate title from description if not set
135
+ if [[ -z "$TITLE" ]]; then
136
+ # Take first sentence or first 80 chars
137
+ TITLE=$(echo "$DESCRIPTION" | head -1 | cut -c1-80)
138
+ # Remove trailing period if present
139
+ TITLE="${TITLE%.}"
140
+ fi
141
+
142
+ # Score quality
143
+ QUALITY_SCORE=$(score_quality "$DESCRIPTION")
144
+
145
+ # Output JSON result
146
+ jq -n \
147
+ --arg title "$TITLE" \
148
+ --arg description "$DESCRIPTION" \
149
+ --arg source "$SOURCE" \
150
+ --arg mode "$MODE" \
151
+ --arg quality_score "$QUALITY_SCORE" \
152
+ '{title:$title, description:$description, source:$source, mode:$mode, quality_score:($quality_score | tonumber)}'