@mestreyoda/fabrica 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 (45) hide show
  1. package/ARCHITECTURE.md +87 -0
  2. package/LICENSE +21 -0
  3. package/README.md +289 -0
  4. package/defaults/AGENTS.md +150 -0
  5. package/defaults/HEARTBEAT.md +3 -0
  6. package/defaults/IDENTITY.md +6 -0
  7. package/defaults/SOUL.md +39 -0
  8. package/defaults/TOOLS.md +15 -0
  9. package/defaults/fabrica/prompts/architect.md +147 -0
  10. package/defaults/fabrica/prompts/developer.md +211 -0
  11. package/defaults/fabrica/prompts/reviewer.md +114 -0
  12. package/defaults/fabrica/prompts/security-checklist.md +58 -0
  13. package/defaults/fabrica/prompts/tester.md +150 -0
  14. package/defaults/fabrica/workflow.yaml +184 -0
  15. package/dist/index.js +143075 -0
  16. package/dist/index.js.map +7 -0
  17. package/dist/lib/worker.cjs +214 -0
  18. package/dist/worker.cjs +4754 -0
  19. package/fabrica.manifest.json +24 -0
  20. package/genesis/configs/classification-rules.json +32 -0
  21. package/genesis/configs/interview-templates.json +73 -0
  22. package/genesis/configs/labels.json +202 -0
  23. package/genesis/configs/triage-matrix.json +39 -0
  24. package/genesis/scripts/classify-idea.sh +161 -0
  25. package/genesis/scripts/conduct-interview.sh +199 -0
  26. package/genesis/scripts/create-task.sh +797 -0
  27. package/genesis/scripts/delivery-target-lib.sh +88 -0
  28. package/genesis/scripts/generate-qa-contract.sh +188 -0
  29. package/genesis/scripts/generate-spec.sh +171 -0
  30. package/genesis/scripts/genesis-telemetry.sh +97 -0
  31. package/genesis/scripts/genesis-utils.sh +617 -0
  32. package/genesis/scripts/impact-analysis.sh +135 -0
  33. package/genesis/scripts/interview.sh +98 -0
  34. package/genesis/scripts/map-project.sh +309 -0
  35. package/genesis/scripts/receive-idea.sh +69 -0
  36. package/genesis/scripts/register-project.sh +520 -0
  37. package/genesis/scripts/research-idea.sh +84 -0
  38. package/genesis/scripts/scaffold-project.sh +1396 -0
  39. package/genesis/scripts/security-review.sh +141 -0
  40. package/genesis/scripts/sideband-lib.sh +243 -0
  41. package/genesis/scripts/stack-detection-lib.sh +130 -0
  42. package/genesis/scripts/triage.sh +598 -0
  43. package/genesis/scripts/validate-step.sh +81 -0
  44. package/openclaw.plugin.json +45 -0
  45. package/package.json +60 -0
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Step 6: Impact analysis — cross-reference spec against project map
5
+ # Input: stdin JSON (spec + project_map)
6
+ # Output: JSON with impact report to stdout
7
+
8
+ if [[ -n "${1:-}" && -f "${1:-}" ]]; then
9
+ INPUT="$(cat "$1")"
10
+ else
11
+ INPUT="$(cat)"
12
+ fi
13
+ SESSION_ID="$(echo "$INPUT" | jq -r '.session_id')"
14
+ IS_GREENFIELD="$(echo "$INPUT" | jq -r '.is_greenfield // false')"
15
+ SPEC="$(echo "$INPUT" | jq '.spec // {}')"
16
+ PROJECT_MAP="$(echo "$INPUT" | jq '.project_map // {}')"
17
+ METADATA="$(echo "$INPUT" | jq '.metadata // {}')"
18
+ CLASSIFICATION="$(echo "$INPUT" | jq '.classification // {}')"
19
+ INTERVIEW="$(echo "$INPUT" | jq '.interview // {}')"
20
+
21
+ echo "Running impact analysis for session $SESSION_ID..." >&2
22
+
23
+ # Greenfield — no existing code to analyze
24
+ if [[ "$IS_GREENFIELD" == "true" ]]; then
25
+ echo "Greenfield project — estimating new files needed" >&2
26
+
27
+ # Estimate new files from scope items
28
+ SCOPE_COUNT="$(echo "$SPEC" | jq '.scope_v1 | length')"
29
+ AC_COUNT="$(echo "$SPEC" | jq '.acceptance_criteria | length')"
30
+
31
+ jq -n \
32
+ --arg sid "$SESSION_ID" \
33
+ --argjson spec "$SPEC" \
34
+ --argjson map "$PROJECT_MAP" \
35
+ --argjson cls "$CLASSIFICATION" \
36
+ --argjson interview "$INTERVIEW" \
37
+ --argjson meta "$METADATA" \
38
+ --argjson sc "$SCOPE_COUNT" \
39
+ --argjson ac "$AC_COUNT" \
40
+ '{
41
+ session_id: $sid,
42
+ step: "impact",
43
+ impact: {
44
+ affected_files: [],
45
+ new_files_needed: ["README.md", "package.json or pyproject.toml", "src/ (main source)", "tests/ (test suite)"],
46
+ affected_modules: [],
47
+ risk_areas: ["New project — no established patterns yet"],
48
+ estimated_files_changed: ($sc * 2 + 4),
49
+ is_greenfield: true
50
+ },
51
+ spec: $spec,
52
+ project_map: $map,
53
+ classification: $cls,
54
+ interview: $interview,
55
+ metadata: $meta
56
+ }'
57
+ exit 0
58
+ fi
59
+
60
+ echo "Cross-referencing spec keywords against project symbols..." >&2
61
+
62
+ # Extract keywords from spec for matching
63
+ SPEC_KEYWORDS="$(echo "$SPEC" | jq -r '
64
+ [.title, .objective, (.scope_v1 // [] | .[]), (.acceptance_criteria // [] | .[])]
65
+ | map(select(. != null and . != "") | ascii_downcase | split(" ") | .[] | select(length > 3))
66
+ | unique | .[]
67
+ ')"
68
+
69
+ # Get all symbols from project map
70
+ SYMBOLS="$(echo "$PROJECT_MAP" | jq '.symbols // []')"
71
+ FILES="$(echo "$PROJECT_MAP" | jq '[.symbols // [] | .[].file] | unique')"
72
+
73
+ # Match keywords against symbol names and file paths
74
+ AFFECTED_FILES="[]"
75
+ AFFECTED_MODULES="[]"
76
+ RISK_AREAS="[]"
77
+
78
+ while IFS= read -r keyword; do
79
+ [[ -z "$keyword" ]] && continue
80
+
81
+ # Match against symbol names
82
+ MATCHES="$(echo "$SYMBOLS" | jq --arg kw "$keyword" '[.[] | select(.name | ascii_downcase | contains($kw)) | .file] | unique')"
83
+
84
+ if [[ "$(echo "$MATCHES" | jq 'length')" -gt 0 ]]; then
85
+ AFFECTED_FILES="$(echo "$AFFECTED_FILES" | jq --argjson m "$MATCHES" '. + $m | unique')"
86
+ fi
87
+
88
+ # Match against file paths
89
+ FILE_MATCHES="$(echo "$FILES" | jq --arg kw "$keyword" '[.[] | select(ascii_downcase | contains($kw))]')"
90
+ if [[ "$(echo "$FILE_MATCHES" | jq 'length')" -gt 0 ]]; then
91
+ AFFECTED_FILES="$(echo "$AFFECTED_FILES" | jq --argjson m "$FILE_MATCHES" '. + $m | unique')"
92
+ fi
93
+ done <<< "$SPEC_KEYWORDS"
94
+
95
+ # Extract modules (directories of affected files)
96
+ AFFECTED_MODULES="$(echo "$AFFECTED_FILES" | jq '[.[] | split("/") | if length > 1 then .[0:-1] | join("/") else "root" end] | unique')"
97
+
98
+ # Risk areas from spec
99
+ RISK_AREAS="$(echo "$SPEC" | jq '.risks // []')"
100
+
101
+ # Estimate total files changed
102
+ AFFECTED_COUNT="$(echo "$AFFECTED_FILES" | jq 'length')"
103
+ SCOPE_COUNT="$(echo "$SPEC" | jq '.scope_v1 | length')"
104
+ ESTIMATED="$((AFFECTED_COUNT > SCOPE_COUNT ? AFFECTED_COUNT : SCOPE_COUNT))"
105
+
106
+ echo "Impact: $AFFECTED_COUNT files matched, $ESTIMATED estimated total" >&2
107
+
108
+ jq -n \
109
+ --arg sid "$SESSION_ID" \
110
+ --argjson affected "$AFFECTED_FILES" \
111
+ --argjson modules "$AFFECTED_MODULES" \
112
+ --argjson risks "$RISK_AREAS" \
113
+ --argjson est "$ESTIMATED" \
114
+ --argjson spec "$SPEC" \
115
+ --argjson map "$PROJECT_MAP" \
116
+ --argjson cls "$CLASSIFICATION" \
117
+ --argjson interview "$INTERVIEW" \
118
+ --argjson meta "$METADATA" \
119
+ '{
120
+ session_id: $sid,
121
+ step: "impact",
122
+ impact: {
123
+ affected_files: $affected,
124
+ new_files_needed: [],
125
+ affected_modules: $modules,
126
+ risk_areas: $risks,
127
+ estimated_files_changed: $est,
128
+ is_greenfield: false
129
+ },
130
+ spec: $spec,
131
+ project_map: $map,
132
+ classification: $cls,
133
+ interview: $interview,
134
+ metadata: $meta
135
+ }'
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Step 3: Generate adaptive interview questions
5
+ # Input: stdin JSON (from classify-idea.sh)
6
+ # Output: JSON with questions array and guidelines to stdout
7
+ # Note: This script GENERATES questions. The actual interview is conducted by
8
+ # the LLM via llm-task in the Lobster workflow.
9
+
10
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ TEMPLATES="$SCRIPT_DIR/../configs/interview-templates.json"
12
+
13
+ if [[ -n "${1:-}" && -f "${1:-}" ]]; then
14
+ INPUT="$(cat "$1")"
15
+ else
16
+ INPUT="$(cat)"
17
+ fi
18
+ SESSION_ID="$(echo "$INPUT" | jq -r '.session_id')"
19
+ RAW_IDEA="$(echo "$INPUT" | jq -r '.raw_idea')"
20
+ TYPE="$(echo "$INPUT" | jq -r '.classification.type')"
21
+ CONFIDENCE="$(echo "$INPUT" | jq -r '.classification.confidence')"
22
+ METADATA="$(echo "$INPUT" | jq '.metadata // {}')"
23
+ CLASSIFICATION="$(echo "$INPUT" | jq '.classification')"
24
+
25
+ echo "Generating interview questions for type=$TYPE (confidence=$CONFIDENCE)..." >&2
26
+
27
+ # Get round1 questions for this type
28
+ ROUND1="$(jq --arg t "$TYPE" '.types[$t].round1 // []' "$TEMPLATES")"
29
+
30
+ # Determine detail level from the idea text length and specificity
31
+ IDEA_LENGTH="${#RAW_IDEA}"
32
+ if [[ "$IDEA_LENGTH" -lt 30 ]]; then
33
+ DETAIL_LEVEL="low"
34
+ echo "Short idea detected — will include follow-ups and non-technical questions" >&2
35
+ elif [[ "$IDEA_LENGTH" -lt 100 ]]; then
36
+ DETAIL_LEVEL="medium"
37
+ echo "Medium detail — standard question set" >&2
38
+ else
39
+ DETAIL_LEVEL="high"
40
+ echo "Detailed idea — will include technical additions" >&2
41
+ fi
42
+
43
+ # Build question set based on detail level
44
+ # Deterministic baseline: start from required questions only.
45
+ QUESTIONS="$(echo "$ROUND1" | jq '[.[] | select(.required == true)]')"
46
+
47
+ # Detect explicit technical intent instead of relying only on text length.
48
+ if echo "$RAW_IDEA" | grep -Eqi '(api|endpoint|sdk|lat[eê]ncia|throughput|schema|tabela|cole[cç][aã]o|banco|deploy|infra|stack|servi[cç]o externo|integra[cç][aã]o)'; then
49
+ TECH_SIGNAL="true"
50
+ else
51
+ TECH_SIGNAL="false"
52
+ fi
53
+
54
+ if [[ "$DETAIL_LEVEL" == "high" ]]; then
55
+ # Add technical additions only when the user idea carries technical signals.
56
+ if [[ "$TECH_SIGNAL" == "true" ]]; then
57
+ TECH="$(jq --arg t "$TYPE" '.types[$t].technical_additions // [] | .[:1]' "$TEMPLATES")"
58
+ QUESTIONS="$(echo "$QUESTIONS" | jq --argjson tech "$TECH" '. + $tech')"
59
+ fi
60
+ elif [[ "$DETAIL_LEVEL" == "low" ]]; then
61
+ # Vague idea: add at most one non-technical clarifier.
62
+ NON_TECH="$(jq --arg t "$TYPE" '.types[$t].non_technical_additions // [] | .[:1]' "$TEMPLATES")"
63
+ QUESTIONS="$(echo "$QUESTIONS" | jq --argjson nt "$NON_TECH" '. + $nt')"
64
+ fi
65
+
66
+ # Hard cap to avoid long interviews in chat channels.
67
+ QUESTIONS="$(echo "$QUESTIONS" | jq '.[0:4]')"
68
+
69
+ # If confidence is low, add questions about alternative types
70
+ ALT_NOTE=""
71
+ if [[ "$(echo "$CONFIDENCE < 0.6" | bc 2>/dev/null || echo 0)" == "1" ]]; then
72
+ ALT_NOTE="Low confidence in classification ($CONFIDENCE). Ask the user to confirm the type: is this a $TYPE, or something else?"
73
+ fi
74
+
75
+ # Guidelines for the LLM conducting the interview
76
+ GUIDELINES="Follow SOUL.md tone. Ask only the provided questions in order. Keep language non-technical by default and only go technical if the user introduces technical constraints. Maximum 2 interview rounds. If an answer is vague and has follow_up_if_vague, use one concise follow-up. ${ALT_NOTE}"
77
+
78
+ jq -n \
79
+ --arg sid "$SESSION_ID" \
80
+ --arg idea "$RAW_IDEA" \
81
+ --argjson cls "$CLASSIFICATION" \
82
+ --argjson questions "$QUESTIONS" \
83
+ --arg guidelines "$GUIDELINES" \
84
+ --arg detail "$DETAIL_LEVEL" \
85
+ --argjson meta "$METADATA" \
86
+ '{
87
+ session_id: $sid,
88
+ step: "interview",
89
+ raw_idea: $idea,
90
+ classification: $cls,
91
+ interview: {
92
+ round: 1,
93
+ questions: $questions,
94
+ detail_level: $detail,
95
+ guidelines: $guidelines
96
+ },
97
+ metadata: $meta
98
+ }'
@@ -0,0 +1,309 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Step 5: Map project structure using project_mapper
5
+ # Input: $1 = repo path (or stdin JSON with metadata.repo_url)
6
+ # Output: JSON with project_map to stdout
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ PYTHON="${OPENCLAW_PYTHON:-$HOME/.openclaw/.venv/bin/python3}"
10
+ MAPPER_V3="$HOME/.openclaw/workspace/scripts/project_mapper_v3.py"
11
+ MAPPER_V1="$HOME/.openclaw/workspace/scripts/project_mapper.py"
12
+ source "$SCRIPT_DIR/sideband-lib.sh"
13
+
14
+ PROJECT_REF_INPUT="${1:-}"
15
+ REPO_PATH="${PROJECT_REF_INPUT}"
16
+ SESSION_ID=""
17
+ STATE_INPUT=""
18
+ STATE_HAS_INPUT=false
19
+
20
+ # Always try to load stdin state if present, even when $1 is set.
21
+ # This keeps session_id/spec/metadata flowing through the pipeline.
22
+ if [[ ! -t 0 ]]; then
23
+ STATE_if [[ -n "${1:-}" && -f "${1:-}" ]]; then
24
+ INPUT="$(cat "$1")"
25
+ else
26
+ INPUT="$(cat)"
27
+ fi
28
+ if [[ -n "$STATE_INPUT" ]]; then
29
+ STATE_HAS_INPUT=true
30
+ SESSION_ID="$(echo "$STATE_INPUT" | jq -r '.session_id // ""')"
31
+ TARGET_RESOLUTION="$(genesis_resolve_canonical_target "$STATE_INPUT" || jq -n '{metadata:{}}')"
32
+ STATE_INPUT="$(printf '%s' "$STATE_INPUT" | jq --argjson resolved "$TARGET_RESOLUTION" '
33
+ .metadata = ((.metadata // {}) + ($resolved.metadata // {}))
34
+ ')"
35
+ fi
36
+ fi
37
+
38
+ # If no $1, try reading from stdin JSON
39
+ if [[ -z "$REPO_PATH" ]] && $STATE_HAS_INPUT; then
40
+ REPO_PATH="$(echo "$STATE_INPUT" | jq -r '.metadata.repo_url // .metadata.repo_path // .repo_url // .repo // ""')"
41
+ fi
42
+ if [[ -n "$REPO_PATH" && "$REPO_PATH" == "~"* ]]; then
43
+ REPO_PATH="$(genesis_expand_path "$REPO_PATH")"
44
+ fi
45
+
46
+ REPO_URL=""
47
+ if [[ "$REPO_PATH" == http* ]]; then
48
+ REPO_URL="$REPO_PATH"
49
+ fi
50
+
51
+ PROJECT_SLUG=""
52
+ PROJECT_NAME=""
53
+
54
+ if $STATE_HAS_INPUT; then
55
+ if [[ -z "$PROJECT_SLUG" ]]; then
56
+ PROJECT_SLUG="$(echo "$STATE_INPUT" | jq -r '.metadata.project_slug // empty')"
57
+ fi
58
+ if [[ -z "$PROJECT_NAME" ]]; then
59
+ PROJECT_NAME="$(echo "$STATE_INPUT" | jq -r '.metadata.project_name // empty')"
60
+ fi
61
+ fi
62
+
63
+ if [[ -n "$REPO_PATH" && "$REPO_PATH" != http* ]] && [[ ! -d "$REPO_PATH" ]]; then
64
+ if PROJECT_REF="$(genesis_project_resolve_ref "$REPO_PATH" || true)"; then
65
+ PROJECT_SLUG="$(printf '%s' "$PROJECT_REF" | cut -f1)"
66
+ PROJECT_NAME="$(printf '%s' "$PROJECT_REF" | cut -f2)"
67
+ RESOLVED_REMOTE="$(printf '%s' "$PROJECT_REF" | cut -f3)"
68
+ RESOLVED_LOCAL="$(printf '%s' "$PROJECT_REF" | cut -f4)"
69
+ if [[ -n "$RESOLVED_REMOTE" ]]; then
70
+ REPO_URL="$RESOLVED_REMOTE"
71
+ fi
72
+ if [[ -n "$RESOLVED_LOCAL" ]]; then
73
+ REPO_PATH="$(genesis_expand_path "$RESOLVED_LOCAL")"
74
+ fi
75
+ echo "Resolved project reference '$PROJECT_REF_INPUT' -> slug=$PROJECT_SLUG repo=$REPO_PATH" >&2
76
+ fi
77
+ fi
78
+
79
+ FACTORY_INTENT_TEXT=""
80
+ FACTORY_CHANGE_EXPLICIT=false
81
+ if $STATE_HAS_INPUT; then
82
+ FACTORY_INTENT_TEXT="$(echo "$STATE_INPUT" | jq -r '[
83
+ .raw_idea // "",
84
+ .spec.title // "",
85
+ .spec.objective // "",
86
+ .classification.type // ""
87
+ ] | join("\n")')"
88
+ if genesis_payload_factory_change "$STATE_INPUT"; then
89
+ FACTORY_CHANGE_EXPLICIT=true
90
+ fi
91
+ fi
92
+
93
+ if [[ -n "$PROJECT_SLUG" ]] && genesis_is_factory_project_slug "$PROJECT_SLUG"; then
94
+ if [[ "$FACTORY_CHANGE_EXPLICIT" != "true" ]]; then
95
+ echo "ERROR: Target project \"$PROJECT_SLUG\" is reserved for Factory-internal changes. User/product requests must target a dedicated project repository." >&2
96
+ exit 1
97
+ fi
98
+ fi
99
+
100
+ if [[ -n "$PROJECT_SLUG" ]] && [[ -n "$REPO_PATH" ]] && [[ ! -d "$REPO_PATH" ]] && [[ "$REPO_PATH" != http* ]]; then
101
+ echo "ERROR: Registered project \"$PROJECT_SLUG\" resolved to local path \"$REPO_PATH\", but that directory does not exist. Refusing greenfield fallback." >&2
102
+ exit 1
103
+ fi
104
+
105
+ REMOTE_REPO_EXISTS=false
106
+ OWNER_REPO_REMOTE=""
107
+
108
+ # If repo_path is a URL, try to find local clone
109
+ if [[ "$REPO_PATH" == http* ]]; then
110
+ REPO_NAME="$(basename "$REPO_PATH" .git)"
111
+ # Common locations for clones
112
+ for candidate in "./$REPO_NAME" "$HOME/$REPO_NAME" "$HOME/git/$REPO_NAME" "$HOME/projects/$REPO_NAME" "$HOME/code/$REPO_NAME"; do
113
+ if [[ -d "$candidate/.git" ]]; then
114
+ REPO_PATH="$candidate"
115
+ echo "Found local clone at $REPO_PATH" >&2
116
+ break
117
+ fi
118
+ done
119
+
120
+ OWNER_REPO_REMOTE="$(genesis_parse_owner_repo "$REPO_PATH" || true)"
121
+ if [[ -n "$OWNER_REPO_REMOTE" ]] && command -v gh >/dev/null 2>&1; then
122
+ if gh repo view "$OWNER_REPO_REMOTE" &>/dev/null; then
123
+ REMOTE_REPO_EXISTS=true
124
+ REPO_URL="https://github.com/$OWNER_REPO_REMOTE"
125
+ [[ -n "$PROJECT_SLUG" ]] || PROJECT_SLUG="${OWNER_REPO_REMOTE##*/}"
126
+ echo "Remote repository exists ($OWNER_REPO_REMOTE) even without local clone" >&2
127
+ fi
128
+ fi
129
+ fi
130
+
131
+ # If session_id is missing, keep output stable.
132
+ if [[ -z "$SESSION_ID" ]]; then
133
+ SESSION_ID="unknown"
134
+ fi
135
+
136
+ # Greenfield project — no repo to map
137
+ if [[ -z "$REPO_PATH" ]] || [[ ! -d "$REPO_PATH" ]]; then
138
+ if [[ "$REMOTE_REPO_EXISTS" == "true" ]]; then
139
+ REMOTE_PROJECT="${PROJECT_SLUG:-$(basename "${OWNER_REPO_REMOTE:-remote-project}")}"
140
+ REMOTE_MAP="$(jq -n \
141
+ --arg project "$REMOTE_PROJECT" \
142
+ --arg repo_url "$REPO_URL" \
143
+ '{
144
+ version: "remote-only",
145
+ project: $project,
146
+ root: null,
147
+ repo_url: $repo_url,
148
+ stats: { files_scanned: 0, symbols_found: 0, languages: [] },
149
+ symbols: [],
150
+ note: "Remote repository detected without local clone; treated as existing project."
151
+ }')"
152
+ if $STATE_HAS_INPUT; then
153
+ echo "$STATE_INPUT" | jq \
154
+ --arg sid "$SESSION_ID" \
155
+ --argjson map "$REMOTE_MAP" \
156
+ --arg repo_url "$REPO_URL" \
157
+ --arg project_slug "$PROJECT_SLUG" \
158
+ --arg project_name "$PROJECT_NAME" \
159
+ '. + {
160
+ session_id: $sid,
161
+ step: "map",
162
+ project_map: $map,
163
+ is_greenfield: false,
164
+ map_path: null,
165
+ metadata: (
166
+ (.metadata // {})
167
+ + (if $repo_url != "" then {repo_url: $repo_url} else {} end)
168
+ + (if $project_slug != "" then {project_slug: $project_slug} else {} end)
169
+ + (if $project_name != "" then {project_name: $project_name} else {} end)
170
+ )
171
+ }'
172
+ else
173
+ jq -n \
174
+ --arg sid "$SESSION_ID" \
175
+ --argjson map "$REMOTE_MAP" \
176
+ --arg repo_url "$REPO_URL" \
177
+ '{
178
+ session_id: $sid,
179
+ step: "map",
180
+ project_map: $map,
181
+ is_greenfield: false,
182
+ map_path: null,
183
+ metadata: { repo_url: $repo_url }
184
+ }'
185
+ fi
186
+ exit 0
187
+ fi
188
+
189
+ echo "No repository found — treating as greenfield project" >&2
190
+ if $STATE_HAS_INPUT; then
191
+ echo "$STATE_INPUT" | jq \
192
+ --arg sid "$SESSION_ID" \
193
+ '. + {
194
+ session_id: $sid,
195
+ step: "map",
196
+ project_map: {
197
+ version: "3.0",
198
+ project: "greenfield",
199
+ root: null,
200
+ stats: { files_scanned: 0, symbols_found: 0, languages: [] },
201
+ symbols: []
202
+ },
203
+ is_greenfield: true,
204
+ map_path: null
205
+ }'
206
+ else
207
+ jq -n \
208
+ --arg sid "$SESSION_ID" \
209
+ '{
210
+ session_id: $sid,
211
+ step: "map",
212
+ project_map: {
213
+ version: "3.0",
214
+ project: "greenfield",
215
+ root: null,
216
+ stats: { files_scanned: 0, symbols_found: 0, languages: [] },
217
+ symbols: []
218
+ },
219
+ is_greenfield: true,
220
+ map_path: null
221
+ }'
222
+ fi
223
+ exit 0
224
+ fi
225
+
226
+ echo "Mapping project at $REPO_PATH..." >&2
227
+
228
+ MAP_OUTPUT=""
229
+ MAP_PATH=""
230
+
231
+ # Try v3 (Tree-sitter) first
232
+ if [[ -f "$MAPPER_V3" ]] && "$PYTHON" -c "import tree_sitter" 2>/dev/null; then
233
+ echo "Using project_mapper_v3 (Tree-sitter)..." >&2
234
+ MAP_PATH="$REPO_PATH/PROJECT_MAP.json"
235
+ "$PYTHON" "$MAPPER_V3" "$REPO_PATH" >&2 2>&1 || true
236
+ if [[ -f "$MAP_PATH" ]]; then
237
+ MAP_OUTPUT="$(cat "$MAP_PATH")"
238
+ fi
239
+ fi
240
+
241
+ # Fallback to v1 (AST stdlib)
242
+ if [[ -z "$MAP_OUTPUT" ]] && [[ -f "$MAPPER_V1" ]]; then
243
+ echo "Falling back to project_mapper v1 (AST stdlib)..." >&2
244
+ MAP_PATH="$REPO_PATH/PROJECT_MAP.json"
245
+ "$PYTHON" "$MAPPER_V1" "$REPO_PATH" >&2 2>&1 || true
246
+ if [[ -f "$MAP_PATH" ]]; then
247
+ MAP_OUTPUT="$(cat "$MAP_PATH")"
248
+ fi
249
+ fi
250
+
251
+ # If no mapper available, create a basic file listing
252
+ if [[ -z "$MAP_OUTPUT" ]]; then
253
+ echo "No mapper available — generating basic file listing..." >&2
254
+ FILE_LIST="$(find "$REPO_PATH" -type f \
255
+ -not -path '*/.git/*' \
256
+ -not -path '*/node_modules/*' \
257
+ -not -path '*/__pycache__/*' \
258
+ -not -path '*/.venv/*' \
259
+ -not -path '*/venv/*' \
260
+ -not -path '*/dist/*' \
261
+ -not -path '*/build/*' \
262
+ 2>/dev/null | head -200 | sort)"
263
+
264
+ MAP_OUTPUT="$(echo "$FILE_LIST" | jq -R -s 'split("\n") | map(select(length > 0)) | {
265
+ version: "1.0-basic",
266
+ project: "'"$(basename "$REPO_PATH")"'",
267
+ root: "'"$REPO_PATH"'",
268
+ stats: { files_scanned: length, symbols_found: 0, languages: [] },
269
+ symbols: [],
270
+ files: .
271
+ }')"
272
+ fi
273
+
274
+ if $STATE_HAS_INPUT; then
275
+ echo "$STATE_INPUT" | jq \
276
+ --arg sid "$SESSION_ID" \
277
+ --argjson map "$MAP_OUTPUT" \
278
+ --arg mp "${MAP_PATH:-}" \
279
+ --arg repo_path "$REPO_PATH" \
280
+ --arg repo_url "$REPO_URL" \
281
+ --arg project_slug "$PROJECT_SLUG" \
282
+ --arg project_name "$PROJECT_NAME" \
283
+ '. + {
284
+ session_id: $sid,
285
+ step: "map",
286
+ project_map: $map,
287
+ is_greenfield: false,
288
+ map_path: (if $mp != "" then $mp else null end),
289
+ metadata: (
290
+ (.metadata // {})
291
+ + (if $repo_url != "" then {repo_url: $repo_url} else {} end)
292
+ + (if $repo_path != "" and ($repo_path | startswith("http") | not) then {repo_path: $repo_path} else {} end)
293
+ + (if $project_slug != "" then {project_slug: $project_slug} else {} end)
294
+ + (if $project_name != "" then {project_name: $project_name} else {} end)
295
+ )
296
+ }'
297
+ else
298
+ jq -n \
299
+ --arg sid "$SESSION_ID" \
300
+ --argjson map "$MAP_OUTPUT" \
301
+ --arg mp "${MAP_PATH:-}" \
302
+ '{
303
+ session_id: $sid,
304
+ step: "map",
305
+ project_map: $map,
306
+ is_greenfield: false,
307
+ map_path: (if $mp != "" then $mp else null end)
308
+ }'
309
+ fi
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Step 1: Receive and envelope a raw idea
5
+ # Input: $1 = idea text, $2 = session_id (optional)
6
+ # Output: JSON envelope to stdout
7
+
8
+ IDEA="${1:-${GENESIS_IDEA:-}}"
9
+ SESSION_ID="${2:-}"
10
+ FACTORY_CHANGE_RAW="${GENESIS_FACTORY_CHANGE:-false}"
11
+ ANSWERS_JSON_RAW="${GENESIS_ANSWERS_JSON:-}"
12
+
13
+ if [[ -z "$IDEA" ]]; then
14
+ echo "Usage: receive-idea.sh <idea> [session-id] (or set GENESIS_IDEA)" >&2
15
+ exit 1
16
+ fi
17
+
18
+ if [[ -z "$SESSION_ID" ]]; then
19
+ SESSION_ID="$(uuidgen | tr '[:upper:]' '[:lower:]')"
20
+ echo "Generated session: $SESSION_ID" >&2
21
+ fi
22
+
23
+ TIMESTAMP="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
24
+ FACTORY_CHANGE_NORMALIZED="$(printf '%s' "$FACTORY_CHANGE_RAW" | tr '[:upper:]' '[:lower:]')"
25
+ FACTORY_CHANGE=false
26
+ if [[ "$FACTORY_CHANGE_NORMALIZED" == "true" || "$FACTORY_CHANGE_NORMALIZED" == "1" || "$FACTORY_CHANGE_NORMALIZED" == "yes" || "$FACTORY_CHANGE_NORMALIZED" == "on" ]]; then
27
+ FACTORY_CHANGE=true
28
+ fi
29
+
30
+ DEFAULT_REPO_URL=""
31
+ DEFAULT_PROJECT_NAME=""
32
+ if [[ "$FACTORY_CHANGE" == "true" ]]; then
33
+ DEFAULT_REPO_URL="${GENESIS_REPO_URL:-}"
34
+ DEFAULT_PROJECT_NAME="${GENESIS_DEFAULT_PROJECT:-}"
35
+ fi
36
+
37
+ ANSWERS_JSON='{}'
38
+ if [[ -n "$ANSWERS_JSON_RAW" ]]; then
39
+ if printf '%s' "$ANSWERS_JSON_RAW" | jq -e 'type == "object"' >/dev/null 2>&1; then
40
+ ANSWERS_JSON="$ANSWERS_JSON_RAW"
41
+ else
42
+ echo "WARNING: GENESIS_ANSWERS_JSON is not a valid JSON object; ignoring" >&2
43
+ fi
44
+ fi
45
+
46
+ jq -n \
47
+ --arg sid "$SESSION_ID" \
48
+ --arg ts "$TIMESTAMP" \
49
+ --arg idea "$IDEA" \
50
+ --arg repo "$DEFAULT_REPO_URL" \
51
+ --arg project "$DEFAULT_PROJECT_NAME" \
52
+ --arg factory_change "$FACTORY_CHANGE_RAW" \
53
+ --argjson answers "$ANSWERS_JSON" \
54
+ '{
55
+ session_id: $sid,
56
+ timestamp: $ts,
57
+ step: "receive",
58
+ raw_idea: $idea,
59
+ answers: $answers,
60
+ metadata: {
61
+ source: "genesis-cli",
62
+ repo_url: (if $repo != "" then $repo else null end),
63
+ project_name: (if $project != "" then $project else null end),
64
+ factory_change: (
65
+ ($factory_change | ascii_downcase) as $fc
66
+ | ($fc == "true" or $fc == "1" or $fc == "yes" or $fc == "on")
67
+ )
68
+ }
69
+ }'