@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,520 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Step: Register scaffolded project in DevClaw
|
|
5
|
+
# Input: stdin JSON (from scaffold.stdout)
|
|
6
|
+
# Output: JSON with registration data + sideband file
|
|
7
|
+
# Creates: projects.json entry, workflow.yaml, role prompts, repository labels
|
|
8
|
+
|
|
9
|
+
GENESIS_LOG="${GENESIS_LOG:-$HOME/.openclaw/workspace/logs/genesis.log}"
|
|
10
|
+
mkdir -p "$(dirname "$GENESIS_LOG")"
|
|
11
|
+
exec 2> >(tee -a "$GENESIS_LOG" >&2)
|
|
12
|
+
|
|
13
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
+
WORKSPACE="$HOME/.openclaw/workspace"
|
|
15
|
+
source "$SCRIPT_DIR/sideband-lib.sh"
|
|
16
|
+
source "$SCRIPT_DIR/genesis-telemetry.sh"
|
|
17
|
+
|
|
18
|
+
# Load .env if available
|
|
19
|
+
genesis_load_env_file "$HOME/.openclaw/.env"
|
|
20
|
+
|
|
21
|
+
if [[ -n "${1:-}" && -f "${1:-}" ]]; then
|
|
22
|
+
INPUT="$(cat "$1")"
|
|
23
|
+
else
|
|
24
|
+
INPUT="$(cat)"
|
|
25
|
+
fi
|
|
26
|
+
SESSION_ID="$(echo "$INPUT" | jq -r '.session_id')"
|
|
27
|
+
genesis_metric_start "register-project" "$SESSION_ID"
|
|
28
|
+
echo "=== $(date -Iseconds) | register-project.sh | session=$SESSION_ID ===" >&2
|
|
29
|
+
|
|
30
|
+
SCAFFOLD_CREATED="$(echo "$INPUT" | jq -r '.scaffold.created // false')"
|
|
31
|
+
DRY_RUN="${GENESIS_DRY_RUN:-$(echo "$INPUT" | jq -r '.dry_run // false')}"
|
|
32
|
+
|
|
33
|
+
if [[ "$DRY_RUN" == "true" ]]; then
|
|
34
|
+
echo "Dry-run enabled — skipping project registration" >&2
|
|
35
|
+
echo "$INPUT" | jq '. + {project_registered: false, project_registration_pending: "dry_run"}'
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Non-scaffold: passthrough
|
|
40
|
+
if [[ "$SCAFFOLD_CREATED" != "true" ]]; then
|
|
41
|
+
echo "No scaffold — skipping registration" >&2
|
|
42
|
+
echo "$INPUT" | jq '. + {project_registered: false}'
|
|
43
|
+
exit 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
SLUG="$(echo "$INPUT" | jq -r '.scaffold.project_slug')"
|
|
47
|
+
REPO_URL="$(echo "$INPUT" | jq -r '.scaffold.repo_url')"
|
|
48
|
+
REPO_LOCAL="$(echo "$INPUT" | jq -r '.scaffold.repo_local')"
|
|
49
|
+
STACK="$(echo "$INPUT" | jq -r '.scaffold.stack')"
|
|
50
|
+
REQUESTED_CHANNEL_ID="$(echo "$INPUT" | jq -r '.project_channel_id // .channel_id // .telegram_chat_id // .chat_id // empty')"
|
|
51
|
+
# Prefer Fabrica Projects group channel for new projects
|
|
52
|
+
# Falls back to TELEGRAM_CHAT_ID only if FABRICA_PROJECTS_CHANNEL_ID is not set
|
|
53
|
+
TELEGRAM_CHAT="${FABRICA_PROJECTS_CHANNEL_ID:-${TELEGRAM_CHAT_ID:-}}"
|
|
54
|
+
if [[ -n "$REQUESTED_CHANNEL_ID" ]]; then
|
|
55
|
+
TELEGRAM_CHAT="$REQUESTED_CHANNEL_ID"
|
|
56
|
+
fi
|
|
57
|
+
ALLOW_SHARED_CHANNELS="${GENESIS_ALLOW_SHARED_CHANNELS:-true}"
|
|
58
|
+
ALLOW_SHARED_CHANNELS="${ALLOW_SHARED_CHANNELS,,}"
|
|
59
|
+
CI_WORKFLOWS_REPO="${CI_WORKFLOWS_REPO:-MestreY0d4-Uninter/fabrica-automation}"
|
|
60
|
+
OWNER_REPO="$(genesis_parse_owner_repo "$REPO_URL" || true)"
|
|
61
|
+
if [[ -z "$OWNER_REPO" ]]; then
|
|
62
|
+
echo "ERROR: Invalid GitHub repository reference: $REPO_URL" >&2
|
|
63
|
+
exit 1
|
|
64
|
+
fi
|
|
65
|
+
REPO_REMOTE="https://github.com/$OWNER_REPO.git"
|
|
66
|
+
|
|
67
|
+
echo "Registering project: $SLUG (stack=$STACK)" >&2
|
|
68
|
+
|
|
69
|
+
PROJECTS_JSON="$WORKSPACE/devclaw/projects.json"
|
|
70
|
+
USE_DEVCLAW_PROJECT_REGISTER_CLI=false
|
|
71
|
+
USE_DEVCLAW_PROJECT_LABELS_CLI=false
|
|
72
|
+
if genesis_openclaw_supports devclaw project register; then
|
|
73
|
+
USE_DEVCLAW_PROJECT_REGISTER_CLI=true
|
|
74
|
+
fi
|
|
75
|
+
if genesis_openclaw_supports devclaw project ensure-labels; then
|
|
76
|
+
USE_DEVCLAW_PROJECT_LABELS_CLI=true
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# Shared-channel policy guard: avoid silent collisions
|
|
80
|
+
if [[ -n "$TELEGRAM_CHAT" ]] && [[ -f "$PROJECTS_JSON" ]]; then
|
|
81
|
+
CHANNEL_MATCHES="$(jq -r --arg slug "$SLUG" --arg chatid "$TELEGRAM_CHAT" '
|
|
82
|
+
.projects
|
|
83
|
+
| to_entries[]
|
|
84
|
+
| select(.key != $slug)
|
|
85
|
+
| select((.value.channels // []) | any(.channelId == $chatid))
|
|
86
|
+
| .key
|
|
87
|
+
' "$PROJECTS_JSON" 2>/dev/null || true)"
|
|
88
|
+
|
|
89
|
+
if [[ -n "$CHANNEL_MATCHES" ]]; then
|
|
90
|
+
CHANNEL_MATCHES_CSV="$(echo "$CHANNEL_MATCHES" | paste -sd "," -)"
|
|
91
|
+
if [[ "$ALLOW_SHARED_CHANNELS" != "true" ]]; then
|
|
92
|
+
echo "WARNING: channelId $TELEGRAM_CHAT already linked to projects: $CHANNEL_MATCHES_CSV" >&2
|
|
93
|
+
echo "GENESIS_ALLOW_SHARED_CHANNELS=$ALLOW_SHARED_CHANNELS -> not attaching channel to new project $SLUG" >&2
|
|
94
|
+
TELEGRAM_CHAT=""
|
|
95
|
+
else
|
|
96
|
+
echo "WARNING: channelId $TELEGRAM_CHAT already linked to projects: $CHANNEL_MATCHES_CSV" >&2
|
|
97
|
+
echo "Proceeding with shared channel (GENESIS_ALLOW_SHARED_CHANNELS=true). Use projectSlug in tools when channel is ambiguous." >&2
|
|
98
|
+
fi
|
|
99
|
+
fi
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# --- Check if already registered ---
|
|
103
|
+
PROJECT_ALREADY_REGISTERED=false
|
|
104
|
+
PROJECT_REGISTERED=false
|
|
105
|
+
PROJECT_REGISTRATION_PENDING=""
|
|
106
|
+
|
|
107
|
+
if jq -e --arg slug "$SLUG" '.projects[$slug]' "$PROJECTS_JSON" &>/dev/null; then
|
|
108
|
+
echo "Project $SLUG already registered in projects.json — skipping" >&2
|
|
109
|
+
PROJECT_ALREADY_REGISTERED=true
|
|
110
|
+
PROJECT_REGISTERED=true
|
|
111
|
+
if [[ -n "$TELEGRAM_CHAT" ]]; then
|
|
112
|
+
PROJECT_HAS_CHANNEL="$(jq -r --arg slug "$SLUG" --arg chatid "$TELEGRAM_CHAT" '
|
|
113
|
+
((.projects[$slug].channels // []) | any((.channelId // "") == $chatid))
|
|
114
|
+
' "$PROJECTS_JSON" 2>/dev/null || echo "false")"
|
|
115
|
+
if [[ "$PROJECT_HAS_CHANNEL" != "true" ]]; then
|
|
116
|
+
CHANNEL_LINKED=false
|
|
117
|
+
if genesis_openclaw_supports devclaw channel register; then
|
|
118
|
+
echo "Linking channelId $TELEGRAM_CHAT to existing project $SLUG..." >&2
|
|
119
|
+
if genesis_openclaw_exec devclaw channel register \
|
|
120
|
+
--project "$SLUG" \
|
|
121
|
+
--channel-id "$TELEGRAM_CHAT" \
|
|
122
|
+
--type "telegram" >/dev/null 2>>"$GENESIS_LOG"; then
|
|
123
|
+
echo "Linked channelId $TELEGRAM_CHAT to project $SLUG" >&2
|
|
124
|
+
CHANNEL_LINKED=true
|
|
125
|
+
else
|
|
126
|
+
echo "WARNING: failed to link channelId $TELEGRAM_CHAT to existing project $SLUG" >&2
|
|
127
|
+
fi
|
|
128
|
+
fi
|
|
129
|
+
if [[ "$CHANNEL_LINKED" != "true" ]]; then
|
|
130
|
+
echo "Applying local channel-link fallback for $SLUG -> $TELEGRAM_CHAT" >&2
|
|
131
|
+
if jq --arg slug "$SLUG" --arg chatid "$TELEGRAM_CHAT" '
|
|
132
|
+
.projects[$slug].dispatchEnabled = true
|
|
133
|
+
| .projects[$slug].defaultNotifyChannel = (.projects[$slug].defaultNotifyChannel // "primary")
|
|
134
|
+
| .projects[$slug].projectKind = (.projects[$slug].projectKind // "implementation")
|
|
135
|
+
| .projects[$slug].archived = (.projects[$slug].archived // false)
|
|
136
|
+
| .projects[$slug].channels = (
|
|
137
|
+
((.projects[$slug].channels // []) + [{
|
|
138
|
+
channel: "telegram",
|
|
139
|
+
name: "primary",
|
|
140
|
+
events: ["*"],
|
|
141
|
+
channelId: $chatid
|
|
142
|
+
}]) | unique_by(.channel + ":" + (.channelId // ""))
|
|
143
|
+
)
|
|
144
|
+
' "$PROJECTS_JSON" > "${PROJECTS_JSON}.tmp" \
|
|
145
|
+
&& mv "${PROJECTS_JSON}.tmp" "$PROJECTS_JSON"; then
|
|
146
|
+
echo "Linked channelId $TELEGRAM_CHAT to project $SLUG via local fallback" >&2
|
|
147
|
+
else
|
|
148
|
+
echo "WARNING: local fallback failed to link channelId $TELEGRAM_CHAT to existing project $SLUG" >&2
|
|
149
|
+
fi
|
|
150
|
+
fi
|
|
151
|
+
fi
|
|
152
|
+
fi
|
|
153
|
+
else
|
|
154
|
+
if [[ -z "$TELEGRAM_CHAT" ]]; then
|
|
155
|
+
echo "No TELEGRAM_CHAT_ID available — skipping registration until a channel is linked." >&2
|
|
156
|
+
PROJECT_REGISTRATION_PENDING="missing_channel"
|
|
157
|
+
elif [[ "$USE_DEVCLAW_PROJECT_REGISTER_CLI" == "true" ]]; then
|
|
158
|
+
echo "Registering project via deterministic DevClaw project_register..." >&2
|
|
159
|
+
genesis_openclaw_exec devclaw project register \
|
|
160
|
+
--name "$SLUG" \
|
|
161
|
+
--repo "$REPO_LOCAL" \
|
|
162
|
+
--base-branch "main" \
|
|
163
|
+
--deploy-branch "main" \
|
|
164
|
+
--group-name "Project: $SLUG" \
|
|
165
|
+
--channel-id "$TELEGRAM_CHAT" \
|
|
166
|
+
--channel-type "telegram" \
|
|
167
|
+
--set-default-notify \
|
|
168
|
+
--json >/dev/null 2>>"$GENESIS_LOG"
|
|
169
|
+
PROJECT_REGISTERED=true
|
|
170
|
+
else
|
|
171
|
+
echo "DevClaw project CLI unavailable/incompatible — using local fallback registration." >&2
|
|
172
|
+
echo "Adding $SLUG to projects.json..." >&2
|
|
173
|
+
|
|
174
|
+
# Build the new project entry
|
|
175
|
+
NEW_ENTRY="$(jq -n \
|
|
176
|
+
--arg slug "$SLUG" \
|
|
177
|
+
--arg repo "$REPO_LOCAL" \
|
|
178
|
+
--arg remote "$REPO_REMOTE" \
|
|
179
|
+
--arg chatid "$TELEGRAM_CHAT" \
|
|
180
|
+
'{
|
|
181
|
+
slug: $slug,
|
|
182
|
+
name: $slug,
|
|
183
|
+
repo: $repo,
|
|
184
|
+
repoRemote: $remote,
|
|
185
|
+
groupName: ("Project: " + $slug),
|
|
186
|
+
deployUrl: "",
|
|
187
|
+
baseBranch: "main",
|
|
188
|
+
deployBranch: "main",
|
|
189
|
+
defaultNotifyChannel: (if $chatid != "" then "primary" else "" end),
|
|
190
|
+
projectKind: "implementation",
|
|
191
|
+
archived: false,
|
|
192
|
+
dispatchEnabled: (if $chatid != "" then true else false end),
|
|
193
|
+
channels: (if $chatid != "" then [{
|
|
194
|
+
channel: "telegram",
|
|
195
|
+
name: "primary",
|
|
196
|
+
events: ["*"],
|
|
197
|
+
channelId: $chatid
|
|
198
|
+
}] else [] end),
|
|
199
|
+
provider: "github",
|
|
200
|
+
workers: {
|
|
201
|
+
developer: {
|
|
202
|
+
levels: {
|
|
203
|
+
junior: [
|
|
204
|
+
{active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null},
|
|
205
|
+
{active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null}
|
|
206
|
+
],
|
|
207
|
+
medior: [
|
|
208
|
+
{active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null},
|
|
209
|
+
{active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null}
|
|
210
|
+
],
|
|
211
|
+
senior: [
|
|
212
|
+
{active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null},
|
|
213
|
+
{active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null}
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
reviewer: {
|
|
218
|
+
levels: {
|
|
219
|
+
junior: [
|
|
220
|
+
{active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null},
|
|
221
|
+
{active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null}
|
|
222
|
+
],
|
|
223
|
+
senior: [
|
|
224
|
+
{active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null},
|
|
225
|
+
{active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null}
|
|
226
|
+
]
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
tester: {
|
|
230
|
+
levels: {
|
|
231
|
+
junior: [
|
|
232
|
+
{active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null},
|
|
233
|
+
{active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null}
|
|
234
|
+
],
|
|
235
|
+
medior: [
|
|
236
|
+
{active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null},
|
|
237
|
+
{active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null}
|
|
238
|
+
],
|
|
239
|
+
senior: [
|
|
240
|
+
{active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null},
|
|
241
|
+
{active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null}
|
|
242
|
+
]
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}'
|
|
247
|
+
)"
|
|
248
|
+
|
|
249
|
+
# Merge into projects.json (locked write)
|
|
250
|
+
if command -v flock >/dev/null 2>&1; then
|
|
251
|
+
LOCK_FILE="${PROJECTS_JSON}.lock"
|
|
252
|
+
exec 9>"$LOCK_FILE"
|
|
253
|
+
flock -x 9
|
|
254
|
+
jq --arg slug "$SLUG" --argjson entry "$NEW_ENTRY" \
|
|
255
|
+
'.projects[$slug] = $entry' "$PROJECTS_JSON" > "${PROJECTS_JSON}.tmp" \
|
|
256
|
+
&& mv "${PROJECTS_JSON}.tmp" "$PROJECTS_JSON"
|
|
257
|
+
flock -u 9
|
|
258
|
+
exec 9>&-
|
|
259
|
+
else
|
|
260
|
+
echo "WARNING: flock not available; proceeding without explicit file lock" >&2
|
|
261
|
+
jq --arg slug "$SLUG" --argjson entry "$NEW_ENTRY" \
|
|
262
|
+
'.projects[$slug] = $entry' "$PROJECTS_JSON" > "${PROJECTS_JSON}.tmp" \
|
|
263
|
+
&& mv "${PROJECTS_JSON}.tmp" "$PROJECTS_JSON"
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
echo "Added $SLUG to projects.json" >&2
|
|
267
|
+
PROJECT_REGISTERED=true
|
|
268
|
+
fi
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
if [[ "$PROJECT_REGISTERED" != "true" ]]; then
|
|
272
|
+
REGISTER_PAYLOAD="$(jq -n \
|
|
273
|
+
--arg slug "$SLUG" \
|
|
274
|
+
--arg reason "$PROJECT_REGISTRATION_PENDING" \
|
|
275
|
+
'{project_slug: $slug, project_registered: false, project_registration_pending: $reason}')"
|
|
276
|
+
SIDEBAND="$(genesis_sideband_write "register" "$SESSION_ID" "$REGISTER_PAYLOAD")"
|
|
277
|
+
echo "Sideband written to $SIDEBAND" >&2
|
|
278
|
+
echo "Registration pending for $SLUG ($PROJECT_REGISTRATION_PENDING)" >&2
|
|
279
|
+
echo "$INPUT" | jq \
|
|
280
|
+
--arg slug "$SLUG" \
|
|
281
|
+
--arg reason "$PROJECT_REGISTRATION_PENDING" \
|
|
282
|
+
'. + {project_registered: false, project_slug: $slug, project_registration_pending: $reason}'
|
|
283
|
+
exit 0
|
|
284
|
+
fi
|
|
285
|
+
|
|
286
|
+
# --- Create workflow.yaml ---
|
|
287
|
+
WORKFLOW_DIR="$WORKSPACE/devclaw/projects/$SLUG"
|
|
288
|
+
mkdir -p "$WORKFLOW_DIR"
|
|
289
|
+
|
|
290
|
+
if [[ -f "$WORKFLOW_DIR/workflow.yaml" ]]; then
|
|
291
|
+
echo "workflow.yaml already exists — skipping" >&2
|
|
292
|
+
else
|
|
293
|
+
echo "Creating workflow.yaml..." >&2
|
|
294
|
+
cat > "$WORKFLOW_DIR/workflow.yaml" <<'YAMLEOF'
|
|
295
|
+
# Project workflow — inherits model allocation from workspace workflow.yaml
|
|
296
|
+
# Add role/model overrides here only when this project needs different models.
|
|
297
|
+
|
|
298
|
+
timeouts:
|
|
299
|
+
dispatchMs: 1800000
|
|
300
|
+
staleWorkerHours: 2
|
|
301
|
+
|
|
302
|
+
workflow:
|
|
303
|
+
reviewPolicy: agent
|
|
304
|
+
testPolicy: agent
|
|
305
|
+
maxWorkersPerLevel: 1
|
|
306
|
+
YAMLEOF
|
|
307
|
+
echo "Created workflow.yaml" >&2
|
|
308
|
+
fi
|
|
309
|
+
|
|
310
|
+
# --- Copy role prompts ---
|
|
311
|
+
ROLES_SRC="$WORKSPACE/projects/roles/devclaw-automation"
|
|
312
|
+
ROLES_DEFAULT="$WORKSPACE/projects/roles/default"
|
|
313
|
+
ROLES_WORKSPACE_DEFAULT="$WORKSPACE/devclaw/prompts"
|
|
314
|
+
ROLES_DST="$WORKSPACE/devclaw/projects/$SLUG/prompts"
|
|
315
|
+
mkdir -p "$ROLES_DST"
|
|
316
|
+
|
|
317
|
+
for ROLE in developer reviewer tester; do
|
|
318
|
+
if [[ -f "$ROLES_DST/$ROLE.md" ]]; then
|
|
319
|
+
echo "Role prompt $ROLE.md already exists — skipping" >&2
|
|
320
|
+
continue
|
|
321
|
+
fi
|
|
322
|
+
|
|
323
|
+
# Try project-specific source first, then default
|
|
324
|
+
SRC=""
|
|
325
|
+
if [[ -f "$ROLES_SRC/$ROLE.md" ]]; then
|
|
326
|
+
SRC="$ROLES_SRC/$ROLE.md"
|
|
327
|
+
elif [[ -f "$ROLES_DEFAULT/$ROLE.md" ]]; then
|
|
328
|
+
SRC="$ROLES_DEFAULT/$ROLE.md"
|
|
329
|
+
elif [[ -f "$ROLES_WORKSPACE_DEFAULT/$ROLE.md" ]]; then
|
|
330
|
+
SRC="$ROLES_WORKSPACE_DEFAULT/$ROLE.md"
|
|
331
|
+
fi
|
|
332
|
+
|
|
333
|
+
if [[ -n "$SRC" ]]; then
|
|
334
|
+
# Copy and replace project name references
|
|
335
|
+
sed "s/devclaw-automation/$SLUG/g" "$SRC" > "$ROLES_DST/$ROLE.md"
|
|
336
|
+
echo "Copied $ROLE.md (from $(basename "$(dirname "$SRC")"))" >&2
|
|
337
|
+
else
|
|
338
|
+
echo "WARNING: No source for $ROLE.md — skipping" >&2
|
|
339
|
+
fi
|
|
340
|
+
done
|
|
341
|
+
|
|
342
|
+
# --- Ensure labels in GitHub repo ---
|
|
343
|
+
LABELS_JSON="$SCRIPT_DIR/../configs/labels.json"
|
|
344
|
+
|
|
345
|
+
if [[ -f "$LABELS_JSON" ]]; then
|
|
346
|
+
# Wait for newly-created repo to propagate across GitHub API nodes
|
|
347
|
+
if [[ "$SCAFFOLD_CREATED" == "true" && "$PROJECT_ALREADY_REGISTERED" != "true" ]]; then
|
|
348
|
+
echo "Waiting for repo propagation (3s)..." >&2
|
|
349
|
+
sleep 3
|
|
350
|
+
fi
|
|
351
|
+
|
|
352
|
+
echo "Ensuring labels in $OWNER_REPO..." >&2
|
|
353
|
+
if [[ "$USE_DEVCLAW_PROJECT_LABELS_CLI" == "true" ]]; then
|
|
354
|
+
ENSURE_LABELS_CMD=(devclaw project ensure-labels --project "$SLUG" --labels-file "$LABELS_JSON")
|
|
355
|
+
if [[ -n "$TELEGRAM_CHAT" ]]; then
|
|
356
|
+
ENSURE_LABELS_CMD+=(--notify-channel-id "$TELEGRAM_CHAT")
|
|
357
|
+
fi
|
|
358
|
+
genesis_openclaw_exec "${ENSURE_LABELS_CMD[@]}" --json >/dev/null 2>>"$GENESIS_LOG"
|
|
359
|
+
else
|
|
360
|
+
echo "DevClaw ensure-labels CLI unavailable/incompatible — falling back to gh for custom repo labels." >&2
|
|
361
|
+
LABEL_COUNT=0
|
|
362
|
+
LABEL_TOTAL="$(jq 'length' "$LABELS_JSON")"
|
|
363
|
+
while IFS= read -r label_line; do
|
|
364
|
+
LABEL_NAME="$(echo "$label_line" | jq -r '.name')"
|
|
365
|
+
LABEL_COLOR="$(echo "$label_line" | jq -r '.color')"
|
|
366
|
+
LABEL_DESC="$(echo "$label_line" | jq -r '.description')"
|
|
367
|
+
|
|
368
|
+
if gh label create "$LABEL_NAME" \
|
|
369
|
+
--repo "$OWNER_REPO" \
|
|
370
|
+
--color "$LABEL_COLOR" \
|
|
371
|
+
--description "$LABEL_DESC" \
|
|
372
|
+
--force >>"$GENESIS_LOG" 2>&1; then
|
|
373
|
+
LABEL_COUNT=$((LABEL_COUNT + 1))
|
|
374
|
+
else
|
|
375
|
+
echo " WARN: failed to create label '$LABEL_NAME'" >&2
|
|
376
|
+
fi
|
|
377
|
+
sleep 0.1
|
|
378
|
+
done < <(jq -c '.[]' "$LABELS_JSON")
|
|
379
|
+
echo "Created/updated $LABEL_COUNT / $LABEL_TOTAL labels" >&2
|
|
380
|
+
|
|
381
|
+
if [[ -n "$TELEGRAM_CHAT" ]]; then
|
|
382
|
+
gh label create "notify:$TELEGRAM_CHAT" \
|
|
383
|
+
--repo "$OWNER_REPO" \
|
|
384
|
+
--color "e4e4e4" \
|
|
385
|
+
--description "" \
|
|
386
|
+
--force >>"$GENESIS_LOG" 2>&1 || true
|
|
387
|
+
fi
|
|
388
|
+
fi
|
|
389
|
+
else
|
|
390
|
+
echo "WARNING: labels.json not found — skipping label creation" >&2
|
|
391
|
+
fi
|
|
392
|
+
|
|
393
|
+
# --- Set up CI foundation (Contract Pack + dependabot) ---
|
|
394
|
+
if [[ -n "$REPO_LOCAL" && -d "$REPO_LOCAL" ]]; then
|
|
395
|
+
echo "Setting up CI foundation..." >&2
|
|
396
|
+
GITHUB_DIR="$REPO_LOCAL/.github"
|
|
397
|
+
WORKFLOWS_DIR="$GITHUB_DIR/workflows"
|
|
398
|
+
mkdir -p "$WORKFLOWS_DIR"
|
|
399
|
+
|
|
400
|
+
# Determine contract pack based on stack
|
|
401
|
+
PACK_WORKFLOW=""
|
|
402
|
+
case "${STACK,,}" in
|
|
403
|
+
python|fastapi|flask|django|python-cli)
|
|
404
|
+
PACK_WORKFLOW="contract-pack-python.yml"
|
|
405
|
+
DEPENDABOT_ECOSYSTEM="pip"
|
|
406
|
+
;;
|
|
407
|
+
node|nodejs|express|react|next|vue|typescript)
|
|
408
|
+
PACK_WORKFLOW="contract-pack-node.yml"
|
|
409
|
+
DEPENDABOT_ECOSYSTEM="npm"
|
|
410
|
+
;;
|
|
411
|
+
*)
|
|
412
|
+
DEPENDABOT_ECOSYSTEM=""
|
|
413
|
+
;;
|
|
414
|
+
esac
|
|
415
|
+
|
|
416
|
+
# Create CI workflow that calls shared workflows
|
|
417
|
+
cat > "$WORKFLOWS_DIR/ci.yml" <<CIEOF
|
|
418
|
+
name: CI
|
|
419
|
+
|
|
420
|
+
on:
|
|
421
|
+
push:
|
|
422
|
+
branches: [main]
|
|
423
|
+
pull_request:
|
|
424
|
+
branches: [main]
|
|
425
|
+
|
|
426
|
+
jobs:
|
|
427
|
+
ci:
|
|
428
|
+
uses: ${CI_WORKFLOWS_REPO}/.github/workflows/ci-base.yml@main
|
|
429
|
+
security:
|
|
430
|
+
uses: ${CI_WORKFLOWS_REPO}/.github/workflows/security.yml@main
|
|
431
|
+
CIEOF
|
|
432
|
+
|
|
433
|
+
# Add contract pack if stack is known
|
|
434
|
+
if [[ -n "$PACK_WORKFLOW" ]]; then
|
|
435
|
+
cat >> "$WORKFLOWS_DIR/ci.yml" <<PACKEOF
|
|
436
|
+
quality:
|
|
437
|
+
uses: ${CI_WORKFLOWS_REPO}/.github/workflows/${PACK_WORKFLOW}@main
|
|
438
|
+
PACKEOF
|
|
439
|
+
echo " Contract Pack: $PACK_WORKFLOW" >&2
|
|
440
|
+
fi
|
|
441
|
+
|
|
442
|
+
# Create dependabot config
|
|
443
|
+
cat > "$GITHUB_DIR/dependabot.yml" <<DEPEOF
|
|
444
|
+
version: 2
|
|
445
|
+
updates:
|
|
446
|
+
- package-ecosystem: "github-actions"
|
|
447
|
+
directory: "/"
|
|
448
|
+
schedule:
|
|
449
|
+
interval: "weekly"
|
|
450
|
+
DEPEOF
|
|
451
|
+
|
|
452
|
+
if [[ -n "$DEPENDABOT_ECOSYSTEM" ]]; then
|
|
453
|
+
cat >> "$GITHUB_DIR/dependabot.yml" <<DEPEOF
|
|
454
|
+
- package-ecosystem: "${DEPENDABOT_ECOSYSTEM}"
|
|
455
|
+
directory: "/"
|
|
456
|
+
schedule:
|
|
457
|
+
interval: "weekly"
|
|
458
|
+
DEPEOF
|
|
459
|
+
fi
|
|
460
|
+
|
|
461
|
+
# Commit CI foundation if repo has git
|
|
462
|
+
if [[ -d "$REPO_LOCAL/.git" ]]; then
|
|
463
|
+
(cd "$REPO_LOCAL" && git add .github/ && git commit -m "ci: add shared CI foundation" --no-verify >>"$GENESIS_LOG" 2>&1 && git push origin main >>"$GENESIS_LOG" 2>&1) || true
|
|
464
|
+
fi
|
|
465
|
+
echo "CI foundation set up" >&2
|
|
466
|
+
else
|
|
467
|
+
echo "WARNING: REPO_LOCAL not available — skipping CI setup" >&2
|
|
468
|
+
fi
|
|
469
|
+
|
|
470
|
+
# --- Create Telegram Forum Topic ---
|
|
471
|
+
if [[ -n "$TELEGRAM_CHAT" && "$PROJECT_REGISTERED" == "true" && "$PROJECT_ALREADY_REGISTERED" != "true" ]]; then
|
|
472
|
+
TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-}"
|
|
473
|
+
if [[ -n "$TELEGRAM_BOT_TOKEN" ]]; then
|
|
474
|
+
echo "Creating forum topic for $SLUG in chat $TELEGRAM_CHAT..." >&2
|
|
475
|
+
TOPIC_RESPONSE="$(curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/createForumTopic" \
|
|
476
|
+
-H "Content-Type: application/json" \
|
|
477
|
+
-d "{\"chat_id\": ${TELEGRAM_CHAT}, \"name\": \"${SLUG}\"}" 2>/dev/null || true)"
|
|
478
|
+
TOPIC_OK="$(echo "$TOPIC_RESPONSE" | jq -r '.ok // false' 2>/dev/null || echo "false")"
|
|
479
|
+
if [[ "$TOPIC_OK" == "true" ]]; then
|
|
480
|
+
TOPIC_ID="$(echo "$TOPIC_RESPONSE" | jq -r '.result.message_thread_id')"
|
|
481
|
+
echo "Forum topic created: $SLUG (messageThreadId=$TOPIC_ID)" >&2
|
|
482
|
+
if jq --arg slug "$SLUG" --argjson tid "$TOPIC_ID" --arg chatid "$TELEGRAM_CHAT" '
|
|
483
|
+
.projects[$slug].channels = [
|
|
484
|
+
.projects[$slug].channels[] |
|
|
485
|
+
if .channel == "telegram" and .channelId == $chatid
|
|
486
|
+
then . + {messageThreadId: $tid}
|
|
487
|
+
else .
|
|
488
|
+
end
|
|
489
|
+
]
|
|
490
|
+
' "$PROJECTS_JSON" > "${PROJECTS_JSON}.tmp" \
|
|
491
|
+
&& mv "${PROJECTS_JSON}.tmp" "$PROJECTS_JSON"; then
|
|
492
|
+
echo "Updated projects.json with messageThreadId=$TOPIC_ID" >&2
|
|
493
|
+
else
|
|
494
|
+
echo "WARNING: failed to update projects.json with messageThreadId" >&2
|
|
495
|
+
fi
|
|
496
|
+
else
|
|
497
|
+
TOPIC_ERR="$(echo "$TOPIC_RESPONSE" | jq -r '.description // "unknown error"' 2>/dev/null || echo "unknown")"
|
|
498
|
+
echo "WARNING: forum topic creation failed: $TOPIC_ERR (non-blocking)" >&2
|
|
499
|
+
fi
|
|
500
|
+
else
|
|
501
|
+
echo "TELEGRAM_BOT_TOKEN not set — skipping forum topic creation" >&2
|
|
502
|
+
fi
|
|
503
|
+
fi
|
|
504
|
+
|
|
505
|
+
# --- Write sideband file ---
|
|
506
|
+
REGISTER_PAYLOAD="$(jq -n \
|
|
507
|
+
--arg slug "$SLUG" \
|
|
508
|
+
--argjson registered true \
|
|
509
|
+
'{project_slug: $slug, project_registered: $registered}')"
|
|
510
|
+
SIDEBAND="$(genesis_sideband_write "register" "$SESSION_ID" "$REGISTER_PAYLOAD")"
|
|
511
|
+
|
|
512
|
+
echo "Sideband written to $SIDEBAND" >&2
|
|
513
|
+
echo "Registration complete for $SLUG" >&2
|
|
514
|
+
|
|
515
|
+
# --- Output ---
|
|
516
|
+
echo "$INPUT" | jq \
|
|
517
|
+
--arg slug "$SLUG" \
|
|
518
|
+
'. + {project_registered: true, project_slug: $slug}'
|
|
519
|
+
|
|
520
|
+
genesis_metric_end "ok"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Step 2b: Web research on idea using Gemini web_search
|
|
5
|
+
# Input: stdin JSON (from classify-idea.sh — contains raw_idea + classification)
|
|
6
|
+
# Output: JSON with research findings added to stdout
|
|
7
|
+
# Graceful degradation: if web_search fails, research={} and pipeline continues
|
|
8
|
+
|
|
9
|
+
GENESIS_LOG="${GENESIS_LOG:-$HOME/.openclaw/workspace/logs/genesis.log}"
|
|
10
|
+
mkdir -p "$(dirname "$GENESIS_LOG")"
|
|
11
|
+
exec 2> >(tee -a "$GENESIS_LOG" >&2)
|
|
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
|
+
echo "=== $(date -Iseconds) | research-idea.sh | session=$SESSION_ID ===" >&2
|
|
20
|
+
|
|
21
|
+
RAW_IDEA="$(echo "$INPUT" | jq -r '.raw_idea // .idea // ""')"
|
|
22
|
+
TYPE="$(echo "$INPUT" | jq -r '.classification.type // "feature"')"
|
|
23
|
+
|
|
24
|
+
echo >&2 "[research] session=$SESSION_ID type=$TYPE idea=$(echo "$RAW_IDEA" | cut -c1-80)..."
|
|
25
|
+
|
|
26
|
+
# Check if GEMINI_API_KEY is set (loaded by gateway from .env)
|
|
27
|
+
# If not, passthrough — no blocking
|
|
28
|
+
if [[ -z "${GEMINI_API_KEY:-}" ]]; then
|
|
29
|
+
echo >&2 "[research] GEMINI_API_KEY not set — skipping web research (passthrough)"
|
|
30
|
+
echo "$INPUT" | jq '. + { step: "research", research: {} }'
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Build research prompt for the agent
|
|
35
|
+
RESEARCH_PROMPT="You have access to the web_search tool. Use it to research this software project idea.
|
|
36
|
+
|
|
37
|
+
Idea: $RAW_IDEA
|
|
38
|
+
Type: $TYPE
|
|
39
|
+
|
|
40
|
+
1. Search for the main technologies/frameworks mentioned or implied
|
|
41
|
+
2. Search for best practices and architecture patterns for this type of project
|
|
42
|
+
3. Synthesize your findings
|
|
43
|
+
|
|
44
|
+
Return ONLY valid JSON (no markdown fences, no explanation):
|
|
45
|
+
{
|
|
46
|
+
\"technologies\": [\"tech1 - brief description\", \"tech2 - brief description\"],
|
|
47
|
+
\"best_practices\": [\"practice1\", \"practice2\"],
|
|
48
|
+
\"architecture_patterns\": [\"pattern1\", \"pattern2\"],
|
|
49
|
+
\"references\": [{\"title\": \"...\", \"url\": \"...\"}, {\"title\": \"...\", \"url\": \"...\"}]
|
|
50
|
+
}"
|
|
51
|
+
|
|
52
|
+
RESEARCH="{}"
|
|
53
|
+
|
|
54
|
+
if command -v openclaw &>/dev/null; then
|
|
55
|
+
echo >&2 "[research] Calling web_search via openclaw agent --local..."
|
|
56
|
+
LLM_RAW="$(timeout 90 openclaw agent --local \
|
|
57
|
+
-m "$RESEARCH_PROMPT" \
|
|
58
|
+
--session-id "genesis-research-${SESSION_ID}" \
|
|
59
|
+
--json 2>/dev/null)" || {
|
|
60
|
+
echo >&2 "[research] openclaw agent call failed (exit $?) — continuing with empty research"
|
|
61
|
+
LLM_RAW=""
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if [[ -n "$LLM_RAW" ]]; then
|
|
65
|
+
LLM_TEXT="$(echo "$LLM_RAW" | jq -r '.payloads[0].text // empty' 2>/dev/null || true)"
|
|
66
|
+
# Strip markdown fences if present
|
|
67
|
+
LLM_TEXT="$(echo "$LLM_TEXT" | sed '/^```/d; /^json$/d')"
|
|
68
|
+
|
|
69
|
+
if [[ -n "$LLM_TEXT" ]] && echo "$LLM_TEXT" | jq -e '.' &>/dev/null 2>&1; then
|
|
70
|
+
RESEARCH="$LLM_TEXT"
|
|
71
|
+
TECH_COUNT="$(echo "$RESEARCH" | jq '.technologies // [] | length')"
|
|
72
|
+
REF_COUNT="$(echo "$RESEARCH" | jq '.references // [] | length')"
|
|
73
|
+
echo >&2 "[research] Success: $TECH_COUNT technologies, $REF_COUNT references found"
|
|
74
|
+
else
|
|
75
|
+
echo >&2 "[research] LLM returned invalid JSON — continuing with empty research"
|
|
76
|
+
fi
|
|
77
|
+
fi
|
|
78
|
+
else
|
|
79
|
+
echo >&2 "[research] openclaw not found — skipping web research"
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# Merge research into the pipeline envelope
|
|
83
|
+
echo "$INPUT" | jq --argjson research "$RESEARCH" \
|
|
84
|
+
'. + { step: "research", research: $research }'
|