@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,797 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Step 9: Create GitHub issue from session state
|
|
5
|
+
# Input: stdin JSON (complete session state)
|
|
6
|
+
# Output: JSON with issue data to stdout
|
|
7
|
+
# Requires: openclaw CLI + DevClaw plugin (deterministic path),
|
|
8
|
+
# gh CLI authenticated (idempotency checks/fallback),
|
|
9
|
+
# GENESIS_REPO_URL in env or metadata
|
|
10
|
+
|
|
11
|
+
GENESIS_LOG="${GENESIS_LOG:-$HOME/.openclaw/workspace/logs/genesis.log}"
|
|
12
|
+
mkdir -p "$(dirname "$GENESIS_LOG")"
|
|
13
|
+
exec 2> >(tee -a "$GENESIS_LOG" >&2)
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
source "$SCRIPT_DIR/sideband-lib.sh"
|
|
17
|
+
source "$SCRIPT_DIR/genesis-telemetry.sh"
|
|
18
|
+
|
|
19
|
+
genesis_normalize_text() {
|
|
20
|
+
tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/ /g; s/^[[:space:]]+//; s/[[:space:]]+$//; s/[[:space:]]+/ /g'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
genesis_sha1() {
|
|
24
|
+
if command -v sha1sum >/dev/null 2>&1; then
|
|
25
|
+
sha1sum | awk '{print $1}'
|
|
26
|
+
return 0
|
|
27
|
+
fi
|
|
28
|
+
if command -v shasum >/dev/null 2>&1; then
|
|
29
|
+
shasum -a 1 | awk '{print $1}'
|
|
30
|
+
return 0
|
|
31
|
+
fi
|
|
32
|
+
return 1
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
genesis_backlog_label_for_type() {
|
|
36
|
+
local type="${1:-feature}"
|
|
37
|
+
case "$type" in
|
|
38
|
+
research)
|
|
39
|
+
printf '%s\n' "backlog:meta"
|
|
40
|
+
;;
|
|
41
|
+
*)
|
|
42
|
+
printf '%s\n' "backlog:canonical"
|
|
43
|
+
;;
|
|
44
|
+
esac
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
genesis_backlog_label_from_input() {
|
|
48
|
+
local input_json="${1:-}"
|
|
49
|
+
local spec_type="${2:-feature}"
|
|
50
|
+
local raw=""
|
|
51
|
+
|
|
52
|
+
raw="$(printf '%s' "$input_json" | jq -r '
|
|
53
|
+
.metadata.backlog.label
|
|
54
|
+
// .metadata.backlog.kind
|
|
55
|
+
// .backlog.label
|
|
56
|
+
// .backlog.kind
|
|
57
|
+
// empty
|
|
58
|
+
' 2>/dev/null || true)"
|
|
59
|
+
raw="$(genesis_trim "$raw")"
|
|
60
|
+
if [[ -n "$raw" ]]; then
|
|
61
|
+
case "$raw" in
|
|
62
|
+
canonical|duplicate|tracking|meta)
|
|
63
|
+
printf 'backlog:%s\n' "$raw"
|
|
64
|
+
return 0
|
|
65
|
+
;;
|
|
66
|
+
backlog:canonical|backlog:duplicate|backlog:tracking|backlog:meta)
|
|
67
|
+
printf '%s\n' "$raw"
|
|
68
|
+
return 0
|
|
69
|
+
;;
|
|
70
|
+
esac
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
genesis_backlog_label_for_type "$spec_type"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
genesis_backlog_metadata_json() {
|
|
77
|
+
local input_json="${1:-}"
|
|
78
|
+
local project_slug="${2:-}"
|
|
79
|
+
local spec_type="${3:-feature}"
|
|
80
|
+
local default_series default_order
|
|
81
|
+
|
|
82
|
+
default_series="${project_slug:-genesis-default}"
|
|
83
|
+
default_order="10"
|
|
84
|
+
|
|
85
|
+
printf '%s' "$input_json" | jq -c \
|
|
86
|
+
--arg default_series "$default_series" \
|
|
87
|
+
--arg default_type "$spec_type" \
|
|
88
|
+
--argjson default_order "$default_order" '
|
|
89
|
+
def parse_refs($value):
|
|
90
|
+
if $value == null then []
|
|
91
|
+
elif ($value | type) == "array" then [$value[] | tonumber? // empty]
|
|
92
|
+
elif ($value | type) == "number" then [$value]
|
|
93
|
+
elif ($value | type) == "string" then (
|
|
94
|
+
$value
|
|
95
|
+
| split(",")
|
|
96
|
+
| map(gsub("^\\s+|\\s+$"; ""))
|
|
97
|
+
| map(select(length > 0))
|
|
98
|
+
| map(tonumber? // empty)
|
|
99
|
+
)
|
|
100
|
+
else []
|
|
101
|
+
end;
|
|
102
|
+
def parse_optional_refs($value):
|
|
103
|
+
(parse_refs($value)) as $refs
|
|
104
|
+
| if ($refs | length) > 0 then $refs else null end;
|
|
105
|
+
. as $root
|
|
106
|
+
| ($root.metadata.backlog // $root.backlog // {}) as $backlog
|
|
107
|
+
| {
|
|
108
|
+
series: (
|
|
109
|
+
$backlog.series
|
|
110
|
+
// $root.metadata.backlog_series
|
|
111
|
+
// $root.metadata.project_slug
|
|
112
|
+
// $root.project_slug
|
|
113
|
+
// $root.metadata.project_name
|
|
114
|
+
// $default_series
|
|
115
|
+
),
|
|
116
|
+
order: (
|
|
117
|
+
$backlog.order
|
|
118
|
+
// $root.metadata.backlog_order
|
|
119
|
+
// $default_order
|
|
120
|
+
),
|
|
121
|
+
dependsOn: (
|
|
122
|
+
parse_refs(
|
|
123
|
+
$backlog.dependsOn
|
|
124
|
+
// $backlog.depends_on
|
|
125
|
+
// $root.metadata.backlog_depends_on
|
|
126
|
+
)
|
|
127
|
+
),
|
|
128
|
+
supersededBy: (
|
|
129
|
+
parse_optional_refs(
|
|
130
|
+
$backlog.supersededBy
|
|
131
|
+
// $backlog.superseded_by
|
|
132
|
+
// $root.metadata.backlog_superseded_by
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
| .order |= (tonumber? // $default_order)
|
|
137
|
+
' 2>/dev/null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Load .env if available
|
|
141
|
+
genesis_load_env_file "$HOME/.openclaw/.env"
|
|
142
|
+
|
|
143
|
+
# Dry-run: output preview without creating issue
|
|
144
|
+
if [[ "${GENESIS_DRY_RUN:-false}" == "true" ]]; then
|
|
145
|
+
if [[ -n "${1:-}" && -f "${1:-}" ]]; then
|
|
146
|
+
INPUT="$(cat "$1")"
|
|
147
|
+
else
|
|
148
|
+
INPUT="$(cat)"
|
|
149
|
+
fi
|
|
150
|
+
echo '{"step":"create_task","dry_run":true,"message":"Dry run — issue creation skipped. Pipeline complete.","session_id":"'"$(echo "$INPUT" | jq -r '.session_id')"'"}' >&1
|
|
151
|
+
exit 0
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
if [[ -n "${1:-}" && -f "${1:-}" ]]; then
|
|
155
|
+
INPUT="$(cat "$1")"
|
|
156
|
+
else
|
|
157
|
+
INPUT="$(cat)"
|
|
158
|
+
fi
|
|
159
|
+
TARGET_RESOLUTION="$(genesis_resolve_canonical_target "$INPUT" || jq -n '{metadata:{}}')"
|
|
160
|
+
INPUT="$(printf '%s' "$INPUT" | jq --argjson resolved "$TARGET_RESOLUTION" '
|
|
161
|
+
.metadata = ((.metadata // {}) + ($resolved.metadata // {}))
|
|
162
|
+
')"
|
|
163
|
+
SESSION_ID="$(echo "$INPUT" | jq -r '.session_id')"
|
|
164
|
+
genesis_metric_start "create-task" "$SESSION_ID"
|
|
165
|
+
echo "=== $(date -Iseconds) | create-task.sh | session=$SESSION_ID ===" >&2
|
|
166
|
+
SPEC="$(echo "$INPUT" | jq '.spec // {}')"
|
|
167
|
+
SECURITY="$(echo "$INPUT" | jq '.security // {}')"
|
|
168
|
+
QA_CONTRACT="$(echo "$INPUT" | jq '.qa_contract // {}')"
|
|
169
|
+
METADATA="$(echo "$INPUT" | jq '.metadata // {}')"
|
|
170
|
+
CLASSIFICATION="$(echo "$INPUT" | jq '.classification // {}')"
|
|
171
|
+
INTERVIEW="$(echo "$INPUT" | jq '.interview // {}')"
|
|
172
|
+
IMPACT="$(echo "$INPUT" | jq '.impact // {}')"
|
|
173
|
+
PROJECT_MAP="$(echo "$INPUT" | jq '.project_map // {}')"
|
|
174
|
+
SPEC_TITLE_SIGNAL="$(echo "$SPEC" | jq -r '.title // ""')"
|
|
175
|
+
SPEC_OBJECTIVE_SIGNAL="$(echo "$SPEC" | jq -r '.objective // ""')"
|
|
176
|
+
SPEC_TYPE="$(echo "$SPEC" | jq -r '.type // "feature"')"
|
|
177
|
+
SPEC_DELIVERY_TARGET="$(echo "$SPEC" | jq -r '.delivery_target // "unknown"')"
|
|
178
|
+
FACTORY_CHANGE_FROM_SPEC=false
|
|
179
|
+
if genesis_payload_factory_change "$INPUT"; then
|
|
180
|
+
FACTORY_CHANGE_FROM_SPEC=true
|
|
181
|
+
elif genesis_request_is_factory_change "$(printf '%s\n%s\n' "$SPEC_TITLE_SIGNAL" "$SPEC_OBJECTIVE_SIGNAL")"; then
|
|
182
|
+
FACTORY_CHANGE_FROM_SPEC=true
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
echo "Creating GitHub issue for session $SESSION_ID..." >&2
|
|
186
|
+
|
|
187
|
+
# Determine repo (prefer current pipeline state; avoid implicit default target for product flows)
|
|
188
|
+
REPO_URL="$(echo "$INPUT" | jq -r '.scaffold.repo_url // .metadata.repo_url // ""')"
|
|
189
|
+
CANDIDATE_PROJECT_SLUG="$(echo "$INPUT" | jq -r '.project_slug // .metadata.project_slug // .scaffold.project_slug // empty')"
|
|
190
|
+
if [[ -z "$CANDIDATE_PROJECT_SLUG" || "$CANDIDATE_PROJECT_SLUG" == "null" ]]; then
|
|
191
|
+
CANDIDATE_PROJECT_SLUG="$(echo "$INPUT" | jq -r '.metadata.project_name // empty')"
|
|
192
|
+
fi
|
|
193
|
+
REQUESTED_CHANNEL_ID="$(echo "$INPUT" | jq -r '.project_channel_id // empty')"
|
|
194
|
+
PROJECT_SLUG=""
|
|
195
|
+
|
|
196
|
+
# Sideband: if scaffold created a new repo, use that (validated + TTL-bound)
|
|
197
|
+
SCAFFOLD_PAYLOAD="$(genesis_sideband_read_payload "scaffold" "$SESSION_ID" "${GENESIS_SIDEBAND_TTL_SECONDS:-1800}" || true)"
|
|
198
|
+
if [[ -z "$REPO_URL" && -n "$SCAFFOLD_PAYLOAD" ]]; then
|
|
199
|
+
SCAFFOLD_REPO="$(echo "$SCAFFOLD_PAYLOAD" | jq -r '.scaffold.repo_url // empty')"
|
|
200
|
+
if [[ -n "$SCAFFOLD_REPO" ]]; then
|
|
201
|
+
REPO_URL="$SCAFFOLD_REPO"
|
|
202
|
+
echo "Using scaffolded repo: $REPO_URL" >&2
|
|
203
|
+
fi
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
if [[ -z "$REPO_URL" ]]; then
|
|
207
|
+
if [[ -n "$CANDIDATE_PROJECT_SLUG" && "$CANDIDATE_PROJECT_SLUG" != "null" ]]; then
|
|
208
|
+
PROJECT_REF="$(genesis_project_resolve_ref "$CANDIDATE_PROJECT_SLUG" || true)"
|
|
209
|
+
if [[ -n "$PROJECT_REF" ]]; then
|
|
210
|
+
RESOLVED_REMOTE="$(printf '%s' "$PROJECT_REF" | cut -f3)"
|
|
211
|
+
if [[ -n "$RESOLVED_REMOTE" ]]; then
|
|
212
|
+
REPO_URL="$RESOLVED_REMOTE"
|
|
213
|
+
echo "Resolved repo from project slug '$CANDIDATE_PROJECT_SLUG': $REPO_URL" >&2
|
|
214
|
+
fi
|
|
215
|
+
fi
|
|
216
|
+
fi
|
|
217
|
+
fi
|
|
218
|
+
if [[ -z "$REPO_URL" ]]; then
|
|
219
|
+
if [[ "$FACTORY_CHANGE_FROM_SPEC" == "true" && -n "${GENESIS_REPO_URL:-}" ]]; then
|
|
220
|
+
REPO_URL="${GENESIS_REPO_URL}"
|
|
221
|
+
echo "Using GENESIS_REPO_URL fallback for factory/internal change." >&2
|
|
222
|
+
fi
|
|
223
|
+
fi
|
|
224
|
+
if [[ -z "$REPO_URL" ]]; then
|
|
225
|
+
echo "ERROR: No repo URL resolved from pipeline state (scaffold/metadata). Refusing implicit fallback for product request." >&2
|
|
226
|
+
exit 1
|
|
227
|
+
fi
|
|
228
|
+
if [[ -n "$REPO_URL" && "$REPO_URL" == "~"* ]]; then
|
|
229
|
+
REPO_URL="$(genesis_expand_path "$REPO_URL")"
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
# If repo URL was not explicit owner/repo, resolve project ref deterministically.
|
|
233
|
+
if [[ -n "$REPO_URL" ]] && ! genesis_parse_owner_repo "$REPO_URL" >/dev/null 2>&1; then
|
|
234
|
+
PROJECT_REF="$(genesis_project_resolve_ref "$REPO_URL" || true)"
|
|
235
|
+
if [[ -n "$PROJECT_REF" ]]; then
|
|
236
|
+
PROJECT_SLUG="$(printf '%s' "$PROJECT_REF" | cut -f1)"
|
|
237
|
+
RESOLVED_REMOTE="$(printf '%s' "$PROJECT_REF" | cut -f3)"
|
|
238
|
+
if [[ -n "$RESOLVED_REMOTE" ]]; then
|
|
239
|
+
REPO_URL="$RESOLVED_REMOTE"
|
|
240
|
+
echo "Resolved repo reference via project map: $REPO_URL" >&2
|
|
241
|
+
fi
|
|
242
|
+
fi
|
|
243
|
+
fi
|
|
244
|
+
|
|
245
|
+
# Extract owner/repo from URL
|
|
246
|
+
OWNER_REPO="$(genesis_parse_owner_repo "$REPO_URL" || true)"
|
|
247
|
+
if [[ -z "$OWNER_REPO" ]]; then
|
|
248
|
+
echo "ERROR: Invalid GitHub repository reference: $REPO_URL" >&2
|
|
249
|
+
exit 1
|
|
250
|
+
fi
|
|
251
|
+
OWNER="$(echo "$OWNER_REPO" | cut -d/ -f1)"
|
|
252
|
+
REPO="$(echo "$OWNER_REPO" | cut -d/ -f2)"
|
|
253
|
+
|
|
254
|
+
# Resolve project slug deterministically from projects.json.
|
|
255
|
+
REPO_PROJECT_SLUG="$(genesis_find_project_slug_by_repo "$REPO_URL" || true)"
|
|
256
|
+
if [[ -n "$REPO_PROJECT_SLUG" ]]; then
|
|
257
|
+
if [[ -n "$CANDIDATE_PROJECT_SLUG" && "$CANDIDATE_PROJECT_SLUG" != "$REPO_PROJECT_SLUG" ]]; then
|
|
258
|
+
echo "WARNING: Provided project slug '$CANDIDATE_PROJECT_SLUG' does not match repo mapping; using '$REPO_PROJECT_SLUG'." >&2
|
|
259
|
+
fi
|
|
260
|
+
PROJECT_SLUG="$REPO_PROJECT_SLUG"
|
|
261
|
+
elif [[ -n "$CANDIDATE_PROJECT_SLUG" ]] && genesis_project_exists "$CANDIDATE_PROJECT_SLUG"; then
|
|
262
|
+
PROJECT_SLUG="$CANDIDATE_PROJECT_SLUG"
|
|
263
|
+
elif [[ -n "$CANDIDATE_PROJECT_SLUG" ]]; then
|
|
264
|
+
echo "WARNING: Provided project slug '$CANDIDATE_PROJECT_SLUG' is not registered in projects.json." >&2
|
|
265
|
+
fi
|
|
266
|
+
|
|
267
|
+
PROJECT_KIND="implementation"
|
|
268
|
+
PROJECT_ARCHIVED="false"
|
|
269
|
+
if [[ -n "$PROJECT_SLUG" ]] && genesis_project_exists "$PROJECT_SLUG"; then
|
|
270
|
+
PROJECT_KIND="$(genesis_project_kind "$PROJECT_SLUG" || echo implementation)"
|
|
271
|
+
PROJECT_ARCHIVED="$(genesis_project_archived "$PROJECT_SLUG" || echo false)"
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
if [[ "$PROJECT_ARCHIVED" == "true" ]]; then
|
|
275
|
+
echo "ERROR: Target project \"$PROJECT_SLUG\" is archived and cannot receive new issues. Redirect to the canonical active project before retrying." >&2
|
|
276
|
+
exit 1
|
|
277
|
+
fi
|
|
278
|
+
|
|
279
|
+
if [[ "$PROJECT_KIND" == "pointer" ]] && [[ "$SPEC_TYPE" != "research" ]]; then
|
|
280
|
+
echo "ERROR: Target project \"$PROJECT_SLUG\" is marked as pointer/scaffold and cannot receive implementation issues." >&2
|
|
281
|
+
exit 1
|
|
282
|
+
fi
|
|
283
|
+
|
|
284
|
+
if [[ -n "$PROJECT_SLUG" ]] && genesis_is_factory_project_slug "$PROJECT_SLUG"; then
|
|
285
|
+
if ! genesis_payload_factory_change "$INPUT"; then
|
|
286
|
+
echo "ERROR: Target project \"$PROJECT_SLUG\" is reserved for Factory-internal changes. User/product requests must target a dedicated project repository." >&2
|
|
287
|
+
exit 1
|
|
288
|
+
fi
|
|
289
|
+
fi
|
|
290
|
+
|
|
291
|
+
PROJECT_CHANNEL_ID=""
|
|
292
|
+
if [[ -n "$PROJECT_SLUG" ]]; then
|
|
293
|
+
PROJECT_CHANNEL_ID="$(genesis_project_channel_id "$PROJECT_SLUG" "$REQUESTED_CHANNEL_ID" || true)"
|
|
294
|
+
if [[ -n "$REQUESTED_CHANNEL_ID" && -n "$PROJECT_CHANNEL_ID" && "$REQUESTED_CHANNEL_ID" != "$PROJECT_CHANNEL_ID" ]]; then
|
|
295
|
+
echo "WARNING: Requested channel '$REQUESTED_CHANNEL_ID' is not valid for '$PROJECT_SLUG'; using '$PROJECT_CHANNEL_ID' from projects.json." >&2
|
|
296
|
+
fi
|
|
297
|
+
fi
|
|
298
|
+
USE_DEVCLAW_TASKS=false
|
|
299
|
+
if [[ -n "$PROJECT_SLUG" && -n "$PROJECT_CHANNEL_ID" ]] && genesis_openclaw_bin >/dev/null 2>&1 && genesis_openclaw_supports devclaw task; then
|
|
300
|
+
USE_DEVCLAW_TASKS=true
|
|
301
|
+
fi
|
|
302
|
+
|
|
303
|
+
echo "Target: $OWNER/$REPO" >&2
|
|
304
|
+
|
|
305
|
+
# Lock by repo+session to avoid duplicate issue creation under concurrent runs.
|
|
306
|
+
CREATE_LOCK_DIR="$HOME/.openclaw/workspace/devclaw/log"
|
|
307
|
+
LOCK_SAFE_KEY="$(printf '%s_%s_%s' "$OWNER" "$REPO" "$SESSION_ID" | tr -c 'A-Za-z0-9._-' '_')"
|
|
308
|
+
CREATE_LOCK_FILE="$CREATE_LOCK_DIR/create-task-${LOCK_SAFE_KEY}.lock"
|
|
309
|
+
mkdir -p "$CREATE_LOCK_DIR"
|
|
310
|
+
if command -v flock >/dev/null 2>&1; then
|
|
311
|
+
exec 9>"$CREATE_LOCK_FILE"
|
|
312
|
+
flock -x 9
|
|
313
|
+
fi
|
|
314
|
+
|
|
315
|
+
# === Idempotency check: prevent duplicate issues for the same session ===
|
|
316
|
+
echo "Checking for existing issue with session_id $SESSION_ID..." >&2
|
|
317
|
+
|
|
318
|
+
EXISTING_ISSUE="$(gh issue list \
|
|
319
|
+
--repo "$OWNER/$REPO" \
|
|
320
|
+
--state all \
|
|
321
|
+
--search "Session: $SESSION_ID in:body" \
|
|
322
|
+
--json number,url,title,state \
|
|
323
|
+
--limit 1 2>/dev/null || echo "[]")"
|
|
324
|
+
|
|
325
|
+
EXISTING_NUMBER="$(echo "$EXISTING_ISSUE" | jq -r '.[0].number // empty' 2>/dev/null || true)"
|
|
326
|
+
|
|
327
|
+
if [[ -n "$EXISTING_NUMBER" ]]; then
|
|
328
|
+
EXISTING_URL="$(echo "$EXISTING_ISSUE" | jq -r '.[0].url')"
|
|
329
|
+
EXISTING_TITLE="$(echo "$EXISTING_ISSUE" | jq -r '.[0].title')"
|
|
330
|
+
EXISTING_STATE="$(echo "$EXISTING_ISSUE" | jq -r '.[0].state')"
|
|
331
|
+
|
|
332
|
+
echo "Found existing issue #$EXISTING_NUMBER ($EXISTING_STATE) for session $SESSION_ID — skipping creation" >&2
|
|
333
|
+
|
|
334
|
+
# Reopen if closed
|
|
335
|
+
if [[ "$EXISTING_STATE" == "CLOSED" ]]; then
|
|
336
|
+
echo "Reopening existing issue #$EXISTING_NUMBER..." >&2
|
|
337
|
+
gh issue reopen "$EXISTING_NUMBER" --repo "$OWNER/$REPO" >/dev/null 2>&1 || true
|
|
338
|
+
fi
|
|
339
|
+
|
|
340
|
+
jq -n \
|
|
341
|
+
--arg sid "$SESSION_ID" \
|
|
342
|
+
--argjson num "$EXISTING_NUMBER" \
|
|
343
|
+
--arg title "$EXISTING_TITLE" \
|
|
344
|
+
--arg url "$EXISTING_URL" \
|
|
345
|
+
--arg project_slug "$PROJECT_SLUG" \
|
|
346
|
+
--arg project_channel_id "$PROJECT_CHANNEL_ID" \
|
|
347
|
+
--arg labels "Planning" \
|
|
348
|
+
--argjson factory_change "$([[ "$FACTORY_CHANGE_FROM_SPEC" == "true" ]] && echo "true" || echo "false")" \
|
|
349
|
+
--argjson spec "$SPEC" \
|
|
350
|
+
--argjson cls "$CLASSIFICATION" \
|
|
351
|
+
--argjson interview "$INTERVIEW" \
|
|
352
|
+
--argjson impact "$IMPACT" \
|
|
353
|
+
--argjson qa "$QA_CONTRACT" \
|
|
354
|
+
--argjson sec "$SECURITY" \
|
|
355
|
+
--argjson map "$PROJECT_MAP" \
|
|
356
|
+
--argjson meta "$METADATA" \
|
|
357
|
+
'{
|
|
358
|
+
session_id: $sid,
|
|
359
|
+
step: "create_task",
|
|
360
|
+
duplicate_prevented: true,
|
|
361
|
+
factory_change: $factory_change,
|
|
362
|
+
project_slug: (if $project_slug != "" then $project_slug else null end),
|
|
363
|
+
project_channel_id: (if $project_channel_id != "" then $project_channel_id else null end),
|
|
364
|
+
issues: [{
|
|
365
|
+
number: $num,
|
|
366
|
+
title: $title,
|
|
367
|
+
url: $url,
|
|
368
|
+
labels: ($labels | split(",")),
|
|
369
|
+
state: "open"
|
|
370
|
+
}],
|
|
371
|
+
spec: $spec,
|
|
372
|
+
classification: $cls,
|
|
373
|
+
interview: $interview,
|
|
374
|
+
impact: $impact,
|
|
375
|
+
qa_contract: $qa,
|
|
376
|
+
security: $sec,
|
|
377
|
+
project_map: $map,
|
|
378
|
+
metadata: $meta
|
|
379
|
+
}'
|
|
380
|
+
exit 0
|
|
381
|
+
fi
|
|
382
|
+
# === End idempotency check ===
|
|
383
|
+
|
|
384
|
+
# Extract spec fields
|
|
385
|
+
TITLE="$(echo "$SPEC" | jq -r '.title')"
|
|
386
|
+
TYPE="$(echo "$SPEC" | jq -r '.type')"
|
|
387
|
+
OBJECTIVE="$(echo "$SPEC" | jq -r '.objective')"
|
|
388
|
+
CONSTRAINTS="$(echo "$SPEC" | jq -r '.constraints // "None"')"
|
|
389
|
+
DELIVERY_TARGET="$(echo "$SPEC" | jq -r '.delivery_target // "unknown"')"
|
|
390
|
+
|
|
391
|
+
DEDUPE_SOURCE="$OBJECTIVE"
|
|
392
|
+
if [[ -z "${DEDUPE_SOURCE// }" || "$DEDUPE_SOURCE" == "null" ]]; then
|
|
393
|
+
DEDUPE_SOURCE="$TITLE"
|
|
394
|
+
fi
|
|
395
|
+
DEDUPE_KEY=""
|
|
396
|
+
DEDUPE_NORM="$(printf '%s' "$DEDUPE_SOURCE" | genesis_normalize_text | cut -c1-240)"
|
|
397
|
+
if [[ -n "$DEDUPE_NORM" ]]; then
|
|
398
|
+
DEDUPE_KEY="$(printf '%s' "$DEDUPE_NORM" | genesis_sha1 || true)"
|
|
399
|
+
fi
|
|
400
|
+
|
|
401
|
+
if [[ -n "$DEDUPE_KEY" ]]; then
|
|
402
|
+
EXISTING_DEDUPE_ISSUE="$(gh issue list \
|
|
403
|
+
--repo "$OWNER/$REPO" \
|
|
404
|
+
--state open \
|
|
405
|
+
--search "dedupe-key:$DEDUPE_KEY in:body" \
|
|
406
|
+
--json number,url,title,state \
|
|
407
|
+
--limit 1 2>/dev/null || echo "[]")"
|
|
408
|
+
DEDUPE_NUMBER="$(echo "$EXISTING_DEDUPE_ISSUE" | jq -r '.[0].number // empty' 2>/dev/null || true)"
|
|
409
|
+
if [[ -n "$DEDUPE_NUMBER" ]]; then
|
|
410
|
+
DEDUPE_URL="$(echo "$EXISTING_DEDUPE_ISSUE" | jq -r '.[0].url')"
|
|
411
|
+
DEDUPE_TITLE="$(echo "$EXISTING_DEDUPE_ISSUE" | jq -r '.[0].title')"
|
|
412
|
+
echo "Found existing open issue #$DEDUPE_NUMBER by dedupe-key ($DEDUPE_KEY) — skipping creation" >&2
|
|
413
|
+
jq -n \
|
|
414
|
+
--arg sid "$SESSION_ID" \
|
|
415
|
+
--argjson num "$DEDUPE_NUMBER" \
|
|
416
|
+
--arg title "$DEDUPE_TITLE" \
|
|
417
|
+
--arg url "$DEDUPE_URL" \
|
|
418
|
+
--arg project_slug "$PROJECT_SLUG" \
|
|
419
|
+
--arg project_channel_id "$PROJECT_CHANNEL_ID" \
|
|
420
|
+
--arg labels "Planning" \
|
|
421
|
+
--arg dedupe_key "$DEDUPE_KEY" \
|
|
422
|
+
--argjson factory_change "$([[ "$FACTORY_CHANGE_FROM_SPEC" == "true" ]] && echo "true" || echo "false")" \
|
|
423
|
+
--argjson spec "$SPEC" \
|
|
424
|
+
--argjson cls "$CLASSIFICATION" \
|
|
425
|
+
--argjson interview "$INTERVIEW" \
|
|
426
|
+
--argjson impact "$IMPACT" \
|
|
427
|
+
--argjson qa "$QA_CONTRACT" \
|
|
428
|
+
--argjson sec "$SECURITY" \
|
|
429
|
+
--argjson map "$PROJECT_MAP" \
|
|
430
|
+
--argjson meta "$METADATA" \
|
|
431
|
+
'{
|
|
432
|
+
session_id: $sid,
|
|
433
|
+
step: "create_task",
|
|
434
|
+
duplicate_prevented: true,
|
|
435
|
+
duplicate_reason: "dedupe-key",
|
|
436
|
+
dedupe_key: $dedupe_key,
|
|
437
|
+
factory_change: $factory_change,
|
|
438
|
+
project_slug: (if $project_slug != "" then $project_slug else null end),
|
|
439
|
+
project_channel_id: (if $project_channel_id != "" then $project_channel_id else null end),
|
|
440
|
+
issues: [{
|
|
441
|
+
number: $num,
|
|
442
|
+
title: $title,
|
|
443
|
+
url: $url,
|
|
444
|
+
labels: ($labels | split(",")),
|
|
445
|
+
state: "open"
|
|
446
|
+
}],
|
|
447
|
+
spec: $spec,
|
|
448
|
+
classification: $cls,
|
|
449
|
+
interview: $interview,
|
|
450
|
+
impact: $impact,
|
|
451
|
+
qa_contract: $qa,
|
|
452
|
+
security: $sec,
|
|
453
|
+
project_map: $map,
|
|
454
|
+
metadata: $meta
|
|
455
|
+
}'
|
|
456
|
+
exit 0
|
|
457
|
+
fi
|
|
458
|
+
fi
|
|
459
|
+
|
|
460
|
+
# Build issue body using template-like expansion
|
|
461
|
+
BACKLOG_LABEL="$(genesis_backlog_label_from_input "$INPUT" "$TYPE")"
|
|
462
|
+
BACKLOG_METADATA_JSON="$(genesis_backlog_metadata_json "$INPUT" "$PROJECT_SLUG" "$TYPE")"
|
|
463
|
+
if [[ -z "$BACKLOG_METADATA_JSON" || "$BACKLOG_METADATA_JSON" == "null" ]]; then
|
|
464
|
+
BACKLOG_METADATA_JSON="$(jq -cn --arg series "${PROJECT_SLUG:-genesis-default}" '{series: $series, order: 10, dependsOn: [], supersededBy: null}')"
|
|
465
|
+
fi
|
|
466
|
+
METADATA="$(printf '%s' "$METADATA" | jq -c --arg label "$BACKLOG_LABEL" --argjson backlog "$BACKLOG_METADATA_JSON" '
|
|
467
|
+
(. // {}) + {backlog: (($backlog + {label: $label}) | with_entries(select(.value != "")))}
|
|
468
|
+
')"
|
|
469
|
+
BODY="## Objetivo
|
|
470
|
+
|
|
471
|
+
$OBJECTIVE
|
|
472
|
+
|
|
473
|
+
## Tipo de Entrega
|
|
474
|
+
|
|
475
|
+
$DELIVERY_TARGET
|
|
476
|
+
|
|
477
|
+
## Escopo V1
|
|
478
|
+
|
|
479
|
+
$(echo "$SPEC" | jq -r '.scope_v1 // [] | .[] | "- " + .')
|
|
480
|
+
|
|
481
|
+
## Fora de Escopo
|
|
482
|
+
|
|
483
|
+
$(echo "$SPEC" | jq -r '.out_of_scope // [] | .[] | "- " + .')
|
|
484
|
+
|
|
485
|
+
## Acceptance Criteria
|
|
486
|
+
|
|
487
|
+
$(echo "$SPEC" | jq -r '.acceptance_criteria // [] | .[] | "- [ ] " + .')
|
|
488
|
+
|
|
489
|
+
## Definition of Done
|
|
490
|
+
|
|
491
|
+
$(echo "$SPEC" | jq -r '.definition_of_done // [] | .[] | "- [ ] " + .')
|
|
492
|
+
|
|
493
|
+
## Restrições
|
|
494
|
+
|
|
495
|
+
$CONSTRAINTS"
|
|
496
|
+
|
|
497
|
+
if [[ -n "$DEDUPE_KEY" ]]; then
|
|
498
|
+
BODY="$BODY
|
|
499
|
+
|
|
500
|
+
<!-- dedupe-key:$DEDUPE_KEY -->"
|
|
501
|
+
fi
|
|
502
|
+
|
|
503
|
+
BODY="$BODY
|
|
504
|
+
|
|
505
|
+
<!-- devclaw-backlog: $BACKLOG_METADATA_JSON -->"
|
|
506
|
+
|
|
507
|
+
# Add risks if present
|
|
508
|
+
RISKS="$(echo "$SPEC" | jq -r '.risks // [] | .[]')"
|
|
509
|
+
if [[ -n "$RISKS" ]]; then
|
|
510
|
+
BODY="$BODY
|
|
511
|
+
|
|
512
|
+
## Riscos
|
|
513
|
+
|
|
514
|
+
$(echo "$SPEC" | jq -r '.risks[] | "- " + .')"
|
|
515
|
+
fi
|
|
516
|
+
|
|
517
|
+
# Add security notes if present
|
|
518
|
+
SEC_NOTES="$(echo "$SECURITY" | jq -r '.spec_security_notes // [] | .[]')"
|
|
519
|
+
if [[ -n "$SEC_NOTES" ]]; then
|
|
520
|
+
BODY="$BODY
|
|
521
|
+
|
|
522
|
+
## Security Notes
|
|
523
|
+
|
|
524
|
+
$(echo "$SECURITY" | jq -r '.spec_security_notes[] | "- " + .')"
|
|
525
|
+
fi
|
|
526
|
+
|
|
527
|
+
BODY="$BODY
|
|
528
|
+
|
|
529
|
+
---
|
|
530
|
+
_Generated by Genesis Flow | Session: ${SESSION_ID}_"
|
|
531
|
+
|
|
532
|
+
# Determine labels
|
|
533
|
+
DELIVERY_LABEL="delivery:implementation"
|
|
534
|
+
if [[ "$TYPE" == "research" ]]; then
|
|
535
|
+
DELIVERY_LABEL="delivery:research"
|
|
536
|
+
fi
|
|
537
|
+
LABELS="Planning,type:$TYPE,$DELIVERY_LABEL"
|
|
538
|
+
EXTRA_LABELS="type:$TYPE,$DELIVERY_LABEL"
|
|
539
|
+
LABELS="$LABELS,$BACKLOG_LABEL"
|
|
540
|
+
EXTRA_LABELS="$EXTRA_LABELS,$BACKLOG_LABEL"
|
|
541
|
+
if [[ "$TYPE" == "research" ]]; then
|
|
542
|
+
LABELS="$LABELS,no-pr-required"
|
|
543
|
+
EXTRA_LABELS="$EXTRA_LABELS,no-pr-required"
|
|
544
|
+
fi
|
|
545
|
+
SEC_REC="$(echo "$SECURITY" | jq -r '.recommendation // ""')"
|
|
546
|
+
if [[ "$SEC_REC" == HIGH* ]]; then
|
|
547
|
+
LABELS="$LABELS,security:high"
|
|
548
|
+
EXTRA_LABELS="$EXTRA_LABELS,security:high"
|
|
549
|
+
fi
|
|
550
|
+
FACTORY_CHANGE="$FACTORY_CHANGE_FROM_SPEC"
|
|
551
|
+
FACTORY_CHANGE_FLAG=()
|
|
552
|
+
if printf '%s\n%s\n' "$TITLE" "$BODY" | tr '[:upper:]' '[:lower:]' | grep -Eq 'factory|openclaw|devclaw|workflow|pipeline|orchestr'; then
|
|
553
|
+
FACTORY_CHANGE=true
|
|
554
|
+
FACTORY_CHANGE_FLAG=(--factory-change)
|
|
555
|
+
fi
|
|
556
|
+
|
|
557
|
+
echo "Creating issue: '$TITLE' with labels: $LABELS" >&2
|
|
558
|
+
|
|
559
|
+
ISSUE_NUMBER="0"
|
|
560
|
+
ISSUE_URL=""
|
|
561
|
+
if [[ "$USE_DEVCLAW_TASKS" == "true" ]]; then
|
|
562
|
+
echo "Creating issue via deterministic DevClaw task_create..." >&2
|
|
563
|
+
BODY_FILE="$(mktemp)"
|
|
564
|
+
printf '%s\n' "$BODY" > "$BODY_FILE"
|
|
565
|
+
TASK_CREATE_CMD=(create --project "$PROJECT_SLUG" --title "$TITLE" --body-file "$BODY_FILE")
|
|
566
|
+
if [[ -n "$PROJECT_CHANNEL_ID" ]]; then
|
|
567
|
+
TASK_CREATE_CMD+=(--channel-id "$PROJECT_CHANNEL_ID")
|
|
568
|
+
fi
|
|
569
|
+
if [[ "${#FACTORY_CHANGE_FLAG[@]}" -gt 0 ]]; then
|
|
570
|
+
TASK_CREATE_CMD+=("${FACTORY_CHANGE_FLAG[@]}")
|
|
571
|
+
fi
|
|
572
|
+
TASK_CREATE_JSON="$(
|
|
573
|
+
genesis_devclaw_task_json \
|
|
574
|
+
"${TASK_CREATE_CMD[@]}" \
|
|
575
|
+
2>>"$GENESIS_LOG"
|
|
576
|
+
)" || {
|
|
577
|
+
rm -f "$BODY_FILE"
|
|
578
|
+
echo "ERROR: DevClaw task_create failed" >&2
|
|
579
|
+
exit 1
|
|
580
|
+
}
|
|
581
|
+
rm -f "$BODY_FILE"
|
|
582
|
+
|
|
583
|
+
ISSUE_NUMBER="$(echo "$TASK_CREATE_JSON" | jq -r '.issue.id // 0' 2>/dev/null || echo 0)"
|
|
584
|
+
ISSUE_URL="$(echo "$TASK_CREATE_JSON" | jq -r '.issue.url // ""' 2>/dev/null || echo "")"
|
|
585
|
+
if [[ "$ISSUE_NUMBER" != "0" && -n "$EXTRA_LABELS" ]]; then
|
|
586
|
+
echo "Applying extra labels via deterministic DevClaw task labels: $EXTRA_LABELS" >&2
|
|
587
|
+
TASK_LABELS_CMD=(labels --project "$PROJECT_SLUG" --issue-id "$ISSUE_NUMBER" --add "$EXTRA_LABELS")
|
|
588
|
+
if [[ -n "$PROJECT_CHANNEL_ID" ]]; then
|
|
589
|
+
TASK_LABELS_CMD+=(--channel-id "$PROJECT_CHANNEL_ID")
|
|
590
|
+
fi
|
|
591
|
+
if [[ "${#FACTORY_CHANGE_FLAG[@]}" -gt 0 ]]; then
|
|
592
|
+
TASK_LABELS_CMD+=("${FACTORY_CHANGE_FLAG[@]}")
|
|
593
|
+
fi
|
|
594
|
+
genesis_devclaw_task_json \
|
|
595
|
+
"${TASK_LABELS_CMD[@]}" \
|
|
596
|
+
>/dev/null 2>>"$GENESIS_LOG" || {
|
|
597
|
+
echo "ERROR: DevClaw task labels failed" >&2
|
|
598
|
+
exit 1
|
|
599
|
+
}
|
|
600
|
+
fi
|
|
601
|
+
else
|
|
602
|
+
echo "DevClaw deterministic mode unavailable (missing project slug/channel or openclaw bin); falling back to gh issue create." >&2
|
|
603
|
+
gh label create "$DELIVERY_LABEL" --repo "$OWNER/$REPO" --color 5319E7 --force >/dev/null 2>>"$GENESIS_LOG" || true
|
|
604
|
+
gh label create "$BACKLOG_LABEL" --repo "$OWNER/$REPO" --color BFD4F2 --force >/dev/null 2>>"$GENESIS_LOG" || true
|
|
605
|
+
if [[ "$TYPE" == "research" ]]; then
|
|
606
|
+
gh label create "no-pr-required" --repo "$OWNER/$REPO" --color 0e8a16 --force >/dev/null 2>>"$GENESIS_LOG" || true
|
|
607
|
+
fi
|
|
608
|
+
ISSUE_OUTPUT="$(gh issue create \
|
|
609
|
+
--repo "$OWNER/$REPO" \
|
|
610
|
+
--title "$TITLE" \
|
|
611
|
+
--body "$BODY" \
|
|
612
|
+
--label "$LABELS" \
|
|
613
|
+
2>&1)" || {
|
|
614
|
+
echo "ERROR: gh issue create failed: $ISSUE_OUTPUT" >&2
|
|
615
|
+
exit 1
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
ISSUE_URL="$(printf '%s\n' "$ISSUE_OUTPUT" | grep -Eo 'https://github\.com/[^[:space:]]+/issues/[0-9]+' | head -1 || true)"
|
|
619
|
+
ISSUE_NUMBER="$(printf '%s' "$ISSUE_URL" | grep -Eo '/issues/[0-9]+' | tr -dc '0-9' || true)"
|
|
620
|
+
if [[ -z "$ISSUE_NUMBER" ]]; then
|
|
621
|
+
ISSUE_NUMBER="0"
|
|
622
|
+
fi
|
|
623
|
+
fi
|
|
624
|
+
|
|
625
|
+
if [[ "$ISSUE_NUMBER" == "0" || -z "$ISSUE_URL" ]]; then
|
|
626
|
+
# Fallback: refetch by session marker to avoid abort on parser mismatch.
|
|
627
|
+
REFETCH_ISSUE="$(gh issue list \
|
|
628
|
+
--repo "$OWNER/$REPO" \
|
|
629
|
+
--state all \
|
|
630
|
+
--search "Session: $SESSION_ID in:body" \
|
|
631
|
+
--json number,url \
|
|
632
|
+
--limit 1 2>/dev/null || echo "[]")"
|
|
633
|
+
ISSUE_NUMBER="$(echo "$REFETCH_ISSUE" | jq -r '.[0].number // 0')"
|
|
634
|
+
ISSUE_URL="$(echo "$REFETCH_ISSUE" | jq -r '.[0].url // ""')"
|
|
635
|
+
fi
|
|
636
|
+
|
|
637
|
+
if [[ "$ISSUE_NUMBER" == "0" || -z "$ISSUE_URL" ]]; then
|
|
638
|
+
echo "ERROR: Could not resolve created issue number/url" >&2
|
|
639
|
+
exit 1
|
|
640
|
+
fi
|
|
641
|
+
|
|
642
|
+
echo "Created issue #$ISSUE_NUMBER: $ISSUE_URL" >&2
|
|
643
|
+
|
|
644
|
+
# Add QA contract as a comment
|
|
645
|
+
QA_SCRIPT="$(echo "$QA_CONTRACT" | jq -r '.script_content // ""')"
|
|
646
|
+
if [[ -n "$QA_SCRIPT" ]] && [[ "$QA_SCRIPT" != "null" ]]; then
|
|
647
|
+
echo "Attaching QA contract as comment..." >&2
|
|
648
|
+
QA_COMMENT="## QA Contract (scripts/qa.sh)
|
|
649
|
+
|
|
650
|
+
\`\`\`bash
|
|
651
|
+
$QA_SCRIPT
|
|
652
|
+
\`\`\`
|
|
653
|
+
|
|
654
|
+
**Gates:** $(echo "$QA_CONTRACT" | jq -r '.gates // [] | join(", ")')
|
|
655
|
+
**Coverage threshold:** $(echo "$QA_CONTRACT" | jq -r '.coverage_threshold // 80')%"
|
|
656
|
+
|
|
657
|
+
if [[ "$USE_DEVCLAW_TASKS" == "true" ]]; then
|
|
658
|
+
echo "QA comment via DevClaw task_comment..." >&2
|
|
659
|
+
QA_COMMENT_FILE="$(mktemp)"
|
|
660
|
+
printf '%s\n' "$QA_COMMENT" > "$QA_COMMENT_FILE"
|
|
661
|
+
TASK_COMMENT_QA_CMD=(comment --project "$PROJECT_SLUG" --issue-id "$ISSUE_NUMBER" --body-file "$QA_COMMENT_FILE")
|
|
662
|
+
if [[ -n "$PROJECT_CHANNEL_ID" ]]; then
|
|
663
|
+
TASK_COMMENT_QA_CMD+=(--channel-id "$PROJECT_CHANNEL_ID")
|
|
664
|
+
fi
|
|
665
|
+
genesis_devclaw_task_json \
|
|
666
|
+
"${TASK_COMMENT_QA_CMD[@]}" \
|
|
667
|
+
>/dev/null 2>>"$GENESIS_LOG" || echo "WARNING: Failed to attach QA comment via task_comment" >&2
|
|
668
|
+
rm -f "$QA_COMMENT_FILE"
|
|
669
|
+
else
|
|
670
|
+
echo "QA comment via gh issue comment..." >&2
|
|
671
|
+
gh issue comment "$ISSUE_NUMBER" \
|
|
672
|
+
--repo "$OWNER/$REPO" \
|
|
673
|
+
--body "$QA_COMMENT" >/dev/null 2>>"$GENESIS_LOG" || echo "WARNING: Failed to attach QA comment" >&2
|
|
674
|
+
fi
|
|
675
|
+
echo "QA comment step finished." >&2
|
|
676
|
+
fi
|
|
677
|
+
|
|
678
|
+
# Attach map/impact summary as a comment (compact; avoid dumping huge JSON).
|
|
679
|
+
FILES_SCANNED="$(echo "$PROJECT_MAP" | jq -r '.stats.files_scanned // 0' 2>/dev/null || echo 0)"
|
|
680
|
+
SYMBOLS_FOUND="$(echo "$PROJECT_MAP" | jq -r '.stats.symbols_found // 0' 2>/dev/null || echo 0)"
|
|
681
|
+
LANGUAGES="$(echo "$PROJECT_MAP" | jq -r '.stats.languages // [] | join(", ")' 2>/dev/null || echo "")"
|
|
682
|
+
MAP_ROOT="$(echo "$PROJECT_MAP" | jq -r '.root // ""' 2>/dev/null || echo "")"
|
|
683
|
+
|
|
684
|
+
IS_GREENFIELD="$(echo "$IMPACT" | jq -r '.is_greenfield // false' 2>/dev/null || echo false)"
|
|
685
|
+
ESTIMATED_CHANGED="$(echo "$IMPACT" | jq -r '.estimated_files_changed // ""' 2>/dev/null || echo "")"
|
|
686
|
+
RISK_AREAS="$(echo "$IMPACT" | jq -r '.risk_areas // [] | .[:10] | .[]' 2>/dev/null || true)"
|
|
687
|
+
NEW_FILES="$(echo "$IMPACT" | jq -r '.new_files_needed // [] | .[:10] | .[]' 2>/dev/null || true)"
|
|
688
|
+
AFFECTED_FILES="$(echo "$IMPACT" | jq -r '.affected_files // [] | .[:25] | .[]' 2>/dev/null || true)"
|
|
689
|
+
|
|
690
|
+
MAP_HAS_SIGNAL=false
|
|
691
|
+
if [[ "$FILES_SCANNED" != "0" || "$SYMBOLS_FOUND" != "0" || -n "$LANGUAGES" || -n "$MAP_ROOT" ]]; then
|
|
692
|
+
MAP_HAS_SIGNAL=true
|
|
693
|
+
fi
|
|
694
|
+
|
|
695
|
+
IMPACT_HAS_SIGNAL=false
|
|
696
|
+
if [[ -n "$ESTIMATED_CHANGED" || -n "$RISK_AREAS" || -n "$NEW_FILES" || -n "$AFFECTED_FILES" ]]; then
|
|
697
|
+
IMPACT_HAS_SIGNAL=true
|
|
698
|
+
fi
|
|
699
|
+
|
|
700
|
+
if $MAP_HAS_SIGNAL || $IMPACT_HAS_SIGNAL; then
|
|
701
|
+
echo "Attaching map/impact summary as comment..." >&2
|
|
702
|
+
|
|
703
|
+
MAP_LINES=""
|
|
704
|
+
if [[ -n "$MAP_ROOT" ]]; then
|
|
705
|
+
MAP_LINES="$MAP_LINES\n**Map root:** $MAP_ROOT"
|
|
706
|
+
fi
|
|
707
|
+
MAP_LINES="$MAP_LINES\n**Map stats:** files_scanned=$FILES_SCANNED, symbols_found=$SYMBOLS_FOUND"
|
|
708
|
+
if [[ -n "$LANGUAGES" ]]; then
|
|
709
|
+
MAP_LINES="$MAP_LINES\n**Languages:** $LANGUAGES"
|
|
710
|
+
fi
|
|
711
|
+
|
|
712
|
+
IMPACT_LINES="\n**Greenfield:** $IS_GREENFIELD"
|
|
713
|
+
if [[ -n "$ESTIMATED_CHANGED" ]]; then
|
|
714
|
+
IMPACT_LINES="$IMPACT_LINES\n**Estimated files changed:** $ESTIMATED_CHANGED"
|
|
715
|
+
fi
|
|
716
|
+
|
|
717
|
+
LIST_BLOCKS=""
|
|
718
|
+
if [[ -n "$AFFECTED_FILES" ]]; then
|
|
719
|
+
LIST_BLOCKS="$LIST_BLOCKS\n\n### Affected files (top 25)\n$(printf '%s\n' "$AFFECTED_FILES" | sed 's/^/- /')\n"
|
|
720
|
+
fi
|
|
721
|
+
if [[ -n "$NEW_FILES" ]]; then
|
|
722
|
+
LIST_BLOCKS="$LIST_BLOCKS\n\n### New files needed (top 10)\n$(printf '%s\n' "$NEW_FILES" | sed 's/^/- /')\n"
|
|
723
|
+
fi
|
|
724
|
+
if [[ -n "$RISK_AREAS" ]]; then
|
|
725
|
+
LIST_BLOCKS="$LIST_BLOCKS\n\n### Risk areas (top 10)\n$(printf '%s\n' "$RISK_AREAS" | sed 's/^/- /')\n"
|
|
726
|
+
fi
|
|
727
|
+
|
|
728
|
+
IMPACT_COMMENT="## Map/Impact Summary
|
|
729
|
+
$MAP_LINES
|
|
730
|
+
|
|
731
|
+
$IMPACT_LINES
|
|
732
|
+
$LIST_BLOCKS"
|
|
733
|
+
|
|
734
|
+
if [[ "$USE_DEVCLAW_TASKS" == "true" ]]; then
|
|
735
|
+
echo "Map/impact comment via DevClaw task_comment..." >&2
|
|
736
|
+
IMPACT_COMMENT_FILE="$(mktemp)"
|
|
737
|
+
printf '%s\n' "$IMPACT_COMMENT" > "$IMPACT_COMMENT_FILE"
|
|
738
|
+
TASK_COMMENT_IMPACT_CMD=(comment --project "$PROJECT_SLUG" --issue-id "$ISSUE_NUMBER" --body-file "$IMPACT_COMMENT_FILE")
|
|
739
|
+
if [[ -n "$PROJECT_CHANNEL_ID" ]]; then
|
|
740
|
+
TASK_COMMENT_IMPACT_CMD+=(--channel-id "$PROJECT_CHANNEL_ID")
|
|
741
|
+
fi
|
|
742
|
+
genesis_devclaw_task_json \
|
|
743
|
+
"${TASK_COMMENT_IMPACT_CMD[@]}" \
|
|
744
|
+
>/dev/null 2>>"$GENESIS_LOG" || echo "WARNING: Failed to attach impact/map comment via task_comment" >&2
|
|
745
|
+
rm -f "$IMPACT_COMMENT_FILE"
|
|
746
|
+
else
|
|
747
|
+
echo "Map/impact comment via gh issue comment..." >&2
|
|
748
|
+
gh issue comment "$ISSUE_NUMBER" \
|
|
749
|
+
--repo "$OWNER/$REPO" \
|
|
750
|
+
--body "$IMPACT_COMMENT" >/dev/null 2>>"$GENESIS_LOG" || echo "WARNING: Failed to attach impact/map comment" >&2
|
|
751
|
+
fi
|
|
752
|
+
echo "Map/impact comment step finished." >&2
|
|
753
|
+
fi
|
|
754
|
+
|
|
755
|
+
echo "create-task.sh completed for session $SESSION_ID" >&2
|
|
756
|
+
|
|
757
|
+
jq -n \
|
|
758
|
+
--arg sid "$SESSION_ID" \
|
|
759
|
+
--argjson num "$ISSUE_NUMBER" \
|
|
760
|
+
--arg title "$TITLE" \
|
|
761
|
+
--arg url "$ISSUE_URL" \
|
|
762
|
+
--arg project_slug "$PROJECT_SLUG" \
|
|
763
|
+
--arg project_channel_id "$PROJECT_CHANNEL_ID" \
|
|
764
|
+
--arg labels "$LABELS" \
|
|
765
|
+
--argjson factory_change "$([[ "$FACTORY_CHANGE" == "true" ]] && echo "true" || echo "false")" \
|
|
766
|
+
--argjson spec "$SPEC" \
|
|
767
|
+
--argjson cls "$CLASSIFICATION" \
|
|
768
|
+
--argjson interview "$INTERVIEW" \
|
|
769
|
+
--argjson impact "$IMPACT" \
|
|
770
|
+
--argjson qa "$QA_CONTRACT" \
|
|
771
|
+
--argjson sec "$SECURITY" \
|
|
772
|
+
--argjson map "$PROJECT_MAP" \
|
|
773
|
+
--argjson meta "$METADATA" \
|
|
774
|
+
'{
|
|
775
|
+
session_id: $sid,
|
|
776
|
+
step: "create_task",
|
|
777
|
+
factory_change: $factory_change,
|
|
778
|
+
project_slug: (if $project_slug != "" then $project_slug else null end),
|
|
779
|
+
project_channel_id: (if $project_channel_id != "" then $project_channel_id else null end),
|
|
780
|
+
issues: [{
|
|
781
|
+
number: $num,
|
|
782
|
+
title: $title,
|
|
783
|
+
url: $url,
|
|
784
|
+
labels: ($labels | split(",")),
|
|
785
|
+
state: "open"
|
|
786
|
+
}],
|
|
787
|
+
spec: $spec,
|
|
788
|
+
classification: $cls,
|
|
789
|
+
interview: $interview,
|
|
790
|
+
impact: $impact,
|
|
791
|
+
qa_contract: $qa,
|
|
792
|
+
security: $sec,
|
|
793
|
+
project_map: $map,
|
|
794
|
+
metadata: $meta
|
|
795
|
+
}'
|
|
796
|
+
|
|
797
|
+
genesis_metric_end "ok"
|