@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.
- package/ARCHITECTURE.md +87 -0
- package/LICENSE +21 -0
- package/README.md +289 -0
- package/defaults/AGENTS.md +150 -0
- package/defaults/HEARTBEAT.md +3 -0
- package/defaults/IDENTITY.md +6 -0
- package/defaults/SOUL.md +39 -0
- package/defaults/TOOLS.md +15 -0
- package/defaults/fabrica/prompts/architect.md +147 -0
- package/defaults/fabrica/prompts/developer.md +211 -0
- package/defaults/fabrica/prompts/reviewer.md +114 -0
- package/defaults/fabrica/prompts/security-checklist.md +58 -0
- package/defaults/fabrica/prompts/tester.md +150 -0
- package/defaults/fabrica/workflow.yaml +184 -0
- package/dist/index.js +143075 -0
- package/dist/index.js.map +7 -0
- package/dist/lib/worker.cjs +214 -0
- package/dist/worker.cjs +4754 -0
- package/fabrica.manifest.json +24 -0
- package/genesis/configs/classification-rules.json +32 -0
- package/genesis/configs/interview-templates.json +73 -0
- package/genesis/configs/labels.json +202 -0
- package/genesis/configs/triage-matrix.json +39 -0
- package/genesis/scripts/classify-idea.sh +161 -0
- package/genesis/scripts/conduct-interview.sh +199 -0
- package/genesis/scripts/create-task.sh +797 -0
- package/genesis/scripts/delivery-target-lib.sh +88 -0
- package/genesis/scripts/generate-qa-contract.sh +188 -0
- package/genesis/scripts/generate-spec.sh +171 -0
- package/genesis/scripts/genesis-telemetry.sh +97 -0
- package/genesis/scripts/genesis-utils.sh +617 -0
- package/genesis/scripts/impact-analysis.sh +135 -0
- package/genesis/scripts/interview.sh +98 -0
- package/genesis/scripts/map-project.sh +309 -0
- package/genesis/scripts/receive-idea.sh +69 -0
- package/genesis/scripts/register-project.sh +520 -0
- package/genesis/scripts/research-idea.sh +84 -0
- package/genesis/scripts/scaffold-project.sh +1396 -0
- package/genesis/scripts/security-review.sh +141 -0
- package/genesis/scripts/sideband-lib.sh +243 -0
- package/genesis/scripts/stack-detection-lib.sh +130 -0
- package/genesis/scripts/triage.sh +598 -0
- package/genesis/scripts/validate-step.sh +81 -0
- package/openclaw.plugin.json +45 -0
- 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
|
+
}'
|