@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,598 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Step 10: Triage — prioritize and dispatch issue to DevClaw pipeline
|
|
5
|
+
# Input: stdin JSON (issue data + spec)
|
|
6
|
+
# Output: JSON with triage decision to stdout
|
|
7
|
+
# Applies labels + workflow transitions to dispatch DevClaw safely
|
|
8
|
+
|
|
9
|
+
GENESIS_LOG="${GENESIS_LOG:-$HOME/.openclaw/workspace/logs/genesis.log}"
|
|
10
|
+
mkdir -p "$(dirname "$GENESIS_LOG")"
|
|
11
|
+
|
|
12
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
|
+
TRIAGE_MATRIX="$SCRIPT_DIR/../configs/triage-matrix.json"
|
|
14
|
+
source "$SCRIPT_DIR/sideband-lib.sh"
|
|
15
|
+
source "$SCRIPT_DIR/genesis-telemetry.sh"
|
|
16
|
+
|
|
17
|
+
# Load .env if available
|
|
18
|
+
genesis_load_env_file "$HOME/.openclaw/.env"
|
|
19
|
+
|
|
20
|
+
if [[ -n "${1:-}" && -f "${1:-}" ]]; then
|
|
21
|
+
INPUT="$(cat "$1")"
|
|
22
|
+
else
|
|
23
|
+
INPUT="$(cat)"
|
|
24
|
+
fi
|
|
25
|
+
TARGET_RESOLUTION="$(genesis_resolve_canonical_target "$INPUT" || jq -n '{metadata:{}}')"
|
|
26
|
+
INPUT="$(printf '%s' "$INPUT" | jq --argjson resolved "$TARGET_RESOLUTION" '
|
|
27
|
+
.metadata = ((.metadata // {}) + ($resolved.metadata // {}))
|
|
28
|
+
')"
|
|
29
|
+
if ! printf '%s' "$INPUT" | jq -e . >/dev/null 2>&1; then
|
|
30
|
+
echo "ERROR: triage received non-JSON input (likely previous step leaked stdout)" >&2
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
SESSION_ID="$(echo "$INPUT" | jq -r '.session_id')"
|
|
34
|
+
genesis_metric_start "triage" "$SESSION_ID"
|
|
35
|
+
SPEC="$(echo "$INPUT" | jq '.spec // {}')"
|
|
36
|
+
ISSUES="$(echo "$INPUT" | jq '.issues // []')"
|
|
37
|
+
IMPACT="$(echo "$INPUT" | jq '.impact // {}')"
|
|
38
|
+
SECURITY="$(echo "$INPUT" | jq '.security // {}')"
|
|
39
|
+
DRY_RUN="$(echo "$INPUT" | jq -r '.dry_run // false')"
|
|
40
|
+
SPEC_TYPE="$(echo "$INPUT" | jq -r '.spec.type // "feature"')"
|
|
41
|
+
SPEC_DELIVERY_TARGET="$(echo "$INPUT" | jq -r '.spec.delivery_target // "unknown"')"
|
|
42
|
+
|
|
43
|
+
if ! echo "$SPEC" | jq -e 'type == "object"' >/dev/null 2>&1; then
|
|
44
|
+
echo "ERROR: triage spec payload must be an object" >&2
|
|
45
|
+
exit 1
|
|
46
|
+
fi
|
|
47
|
+
if ! echo "$ISSUES" | jq -e 'type == "array"' >/dev/null 2>&1; then
|
|
48
|
+
echo "ERROR: triage issues payload must be an array" >&2
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
echo "Running triage for session $SESSION_ID..." >&2
|
|
53
|
+
|
|
54
|
+
# Dry-run mode: keep pipeline deterministic without touching labels.
|
|
55
|
+
if [[ "$DRY_RUN" == "true" ]]; then
|
|
56
|
+
echo "$INPUT" | jq '. + {
|
|
57
|
+
step: "triage",
|
|
58
|
+
triage: {
|
|
59
|
+
skipped: true,
|
|
60
|
+
reason: "dry_run",
|
|
61
|
+
ready_for_dispatch: false,
|
|
62
|
+
errors: []
|
|
63
|
+
}
|
|
64
|
+
}'
|
|
65
|
+
exit 0
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# Get issue number
|
|
69
|
+
ISSUE_NUMBER="$(echo "$ISSUES" | jq -r '.[0].number // 0')"
|
|
70
|
+
if [[ "$ISSUE_NUMBER" == "0" ]]; then
|
|
71
|
+
echo "ERROR: No issue number found in input" >&2
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# Determine repo (prefer current pipeline state; avoid implicit default target for product flows)
|
|
76
|
+
REPO_URL="$(echo "$INPUT" | jq -r '.scaffold.repo_url // .metadata.repo_url // ""')"
|
|
77
|
+
|
|
78
|
+
# Sideband: if scaffold created a new repo, use that (validated + TTL-bound)
|
|
79
|
+
SCAFFOLD_PAYLOAD="$(genesis_sideband_read_payload "scaffold" "$SESSION_ID" "${GENESIS_SIDEBAND_TTL_SECONDS:-1800}" || true)"
|
|
80
|
+
if [[ -z "$REPO_URL" && -n "$SCAFFOLD_PAYLOAD" ]]; then
|
|
81
|
+
SCAFFOLD_REPO="$(echo "$SCAFFOLD_PAYLOAD" | jq -r '.scaffold.repo_url // empty')"
|
|
82
|
+
if [[ -n "$SCAFFOLD_REPO" ]]; then
|
|
83
|
+
REPO_URL="$SCAFFOLD_REPO"
|
|
84
|
+
echo "Using scaffolded repo URL for triage: $REPO_URL" >&2
|
|
85
|
+
fi
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
CANDIDATE_PROJECT_SLUG="$(echo "$INPUT" | jq -r '.project_slug // .metadata.project_slug // .scaffold.project_slug // empty')"
|
|
89
|
+
if [[ -z "$CANDIDATE_PROJECT_SLUG" || "$CANDIDATE_PROJECT_SLUG" == "null" ]]; then
|
|
90
|
+
CANDIDATE_PROJECT_SLUG="$(echo "$INPUT" | jq -r '.metadata.project_name // empty')"
|
|
91
|
+
fi
|
|
92
|
+
REQUESTED_CHANNEL_ID="$(echo "$INPUT" | jq -r '.project_channel_id // empty')"
|
|
93
|
+
PROJECT_SLUG=""
|
|
94
|
+
|
|
95
|
+
if [[ -z "$REPO_URL" ]]; then
|
|
96
|
+
if [[ -n "$CANDIDATE_PROJECT_SLUG" && "$CANDIDATE_PROJECT_SLUG" != "null" ]]; then
|
|
97
|
+
PROJECT_REF="$(genesis_project_resolve_ref "$CANDIDATE_PROJECT_SLUG" || true)"
|
|
98
|
+
if [[ -n "$PROJECT_REF" ]]; then
|
|
99
|
+
RESOLVED_REMOTE="$(printf '%s' "$PROJECT_REF" | cut -f3)"
|
|
100
|
+
if [[ -n "$RESOLVED_REMOTE" ]]; then
|
|
101
|
+
REPO_URL="$RESOLVED_REMOTE"
|
|
102
|
+
echo "Resolved repo from project slug '$CANDIDATE_PROJECT_SLUG': $REPO_URL" >&2
|
|
103
|
+
fi
|
|
104
|
+
fi
|
|
105
|
+
fi
|
|
106
|
+
fi
|
|
107
|
+
if [[ -n "$REPO_URL" && "$REPO_URL" == "~"* ]]; then
|
|
108
|
+
REPO_URL="$(genesis_expand_path "$REPO_URL")"
|
|
109
|
+
fi
|
|
110
|
+
if [[ -z "$REPO_URL" ]]; then
|
|
111
|
+
echo "ERROR: No repo URL resolved from pipeline state for triage (scaffold/metadata)." >&2
|
|
112
|
+
exit 1
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
if [[ -n "$REPO_URL" ]] && ! genesis_parse_owner_repo "$REPO_URL" >/dev/null 2>&1; then
|
|
116
|
+
PROJECT_REF="$(genesis_project_resolve_ref "$REPO_URL" || true)"
|
|
117
|
+
if [[ -n "$PROJECT_REF" ]]; then
|
|
118
|
+
PROJECT_SLUG="$(printf '%s' "$PROJECT_REF" | cut -f1)"
|
|
119
|
+
RESOLVED_REMOTE="$(printf '%s' "$PROJECT_REF" | cut -f3)"
|
|
120
|
+
if [[ -n "$RESOLVED_REMOTE" ]]; then
|
|
121
|
+
REPO_URL="$RESOLVED_REMOTE"
|
|
122
|
+
fi
|
|
123
|
+
fi
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
OWNER_REPO="$(genesis_parse_owner_repo "$REPO_URL" || true)"
|
|
127
|
+
if [[ -z "$OWNER_REPO" ]]; then
|
|
128
|
+
echo "ERROR: Invalid GitHub repository reference: $REPO_URL" >&2
|
|
129
|
+
exit 1
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
# Resolve project slug deterministically from projects.json.
|
|
133
|
+
REPO_PROJECT_SLUG="$(genesis_find_project_slug_by_repo "$REPO_URL" || true)"
|
|
134
|
+
if [[ -n "$REPO_PROJECT_SLUG" ]]; then
|
|
135
|
+
if [[ -n "$CANDIDATE_PROJECT_SLUG" && "$CANDIDATE_PROJECT_SLUG" != "$REPO_PROJECT_SLUG" ]]; then
|
|
136
|
+
echo "WARNING: Provided project slug '$CANDIDATE_PROJECT_SLUG' does not match repo mapping; using '$REPO_PROJECT_SLUG'." >&2
|
|
137
|
+
fi
|
|
138
|
+
PROJECT_SLUG="$REPO_PROJECT_SLUG"
|
|
139
|
+
elif [[ -n "$CANDIDATE_PROJECT_SLUG" ]] && genesis_project_exists "$CANDIDATE_PROJECT_SLUG"; then
|
|
140
|
+
PROJECT_SLUG="$CANDIDATE_PROJECT_SLUG"
|
|
141
|
+
elif [[ -n "$CANDIDATE_PROJECT_SLUG" ]]; then
|
|
142
|
+
echo "WARNING: Provided project slug '$CANDIDATE_PROJECT_SLUG' is not registered in projects.json." >&2
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
PROJECT_KIND="implementation"
|
|
146
|
+
PROJECT_ARCHIVED="false"
|
|
147
|
+
if [[ -n "$PROJECT_SLUG" ]] && genesis_project_exists "$PROJECT_SLUG"; then
|
|
148
|
+
PROJECT_KIND="$(genesis_project_kind "$PROJECT_SLUG" || echo implementation)"
|
|
149
|
+
PROJECT_ARCHIVED="$(genesis_project_archived "$PROJECT_SLUG" || echo false)"
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
if [[ "$PROJECT_ARCHIVED" == "true" ]]; then
|
|
153
|
+
echo "ERROR: Target project \"$PROJECT_SLUG\" is archived and cannot be triaged or dispatched." >&2
|
|
154
|
+
exit 1
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
if [[ "$PROJECT_KIND" == "pointer" ]] && [[ "$SPEC_TYPE" != "research" ]]; then
|
|
158
|
+
echo "ERROR: Target project \"$PROJECT_SLUG\" is marked as pointer/scaffold and cannot receive implementation dispatch." >&2
|
|
159
|
+
exit 1
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
if [[ -n "$PROJECT_SLUG" ]] && genesis_is_factory_project_slug "$PROJECT_SLUG"; then
|
|
163
|
+
if ! genesis_payload_factory_change "$INPUT"; then
|
|
164
|
+
echo "ERROR: Target project \"$PROJECT_SLUG\" is reserved for Factory-internal changes. User/product requests must target a dedicated project repository." >&2
|
|
165
|
+
exit 1
|
|
166
|
+
fi
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
PROJECT_CHANNEL_ID=""
|
|
170
|
+
if [[ -n "$PROJECT_SLUG" ]]; then
|
|
171
|
+
PROJECT_CHANNEL_ID="$(genesis_project_channel_id "$PROJECT_SLUG" "$REQUESTED_CHANNEL_ID" || true)"
|
|
172
|
+
if [[ -n "$REQUESTED_CHANNEL_ID" && -n "$PROJECT_CHANNEL_ID" && "$REQUESTED_CHANNEL_ID" != "$PROJECT_CHANNEL_ID" ]]; then
|
|
173
|
+
echo "WARNING: Requested channel '$REQUESTED_CHANNEL_ID' is not valid for '$PROJECT_SLUG'; using '$PROJECT_CHANNEL_ID' from projects.json." >&2
|
|
174
|
+
fi
|
|
175
|
+
fi
|
|
176
|
+
USE_DEVCLAW_TASKS=false
|
|
177
|
+
if [[ -n "$PROJECT_SLUG" && -n "$PROJECT_CHANNEL_ID" ]] && genesis_openclaw_bin >/dev/null 2>&1 && genesis_openclaw_supports devclaw task; then
|
|
178
|
+
USE_DEVCLAW_TASKS=true
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
if [[ ! -f "$TRIAGE_MATRIX" ]]; then
|
|
182
|
+
echo "ERROR: Missing triage matrix file: $TRIAGE_MATRIX" >&2
|
|
183
|
+
exit 1
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
TYPE="$(echo "$SPEC" | jq -r '.type // "feature"')"
|
|
187
|
+
TITLE="$(echo "$SPEC" | jq -r '.title // ""')"
|
|
188
|
+
AC_COUNT="$(echo "$SPEC" | jq '.acceptance_criteria // [] | length')"
|
|
189
|
+
SCOPE_COUNT="$(echo "$SPEC" | jq '.scope_v1 // [] | length')"
|
|
190
|
+
DOD_COUNT="$(echo "$SPEC" | jq '.definition_of_done // [] | length')"
|
|
191
|
+
DELIVERY_TARGET="$(echo "$SPEC" | jq -r '.delivery_target // "unknown"')"
|
|
192
|
+
OBJECTIVE_RAW="$(echo "$SPEC" | jq -r '.objective // ""')"
|
|
193
|
+
OBJECTIVE="$(genesis_trim "$OBJECTIVE_RAW")"
|
|
194
|
+
OBJECTIVE_LOWER="$(printf '%s' "$OBJECTIVE" | tr '[:upper:]' '[:lower:]')"
|
|
195
|
+
RAW_IDEA_LOWER="$(echo "$INPUT" | jq -r '.raw_idea // ""' | tr '[:upper:]' '[:lower:]')"
|
|
196
|
+
META_AUTH_SIGNAL="$(echo "$INPUT" | jq -r '.metadata.auth_gate.signal // false')"
|
|
197
|
+
FACTORY_CHANGE_RAW="$(echo "$INPUT" | jq -r '.factory_change // false')"
|
|
198
|
+
FACTORY_CHANGE=false
|
|
199
|
+
if [[ "$FACTORY_CHANGE_RAW" == "true" ]]; then
|
|
200
|
+
FACTORY_CHANGE=true
|
|
201
|
+
elif printf '%s\n%s\n' "$TITLE" "$OBJECTIVE" | tr '[:upper:]' '[:lower:]' | grep -Eq 'factory|openclaw|devclaw|workflow|pipeline|orchestr'; then
|
|
202
|
+
FACTORY_CHANGE=true
|
|
203
|
+
fi
|
|
204
|
+
FILES_CHANGED="$(echo "$IMPACT" | jq '.estimated_files_changed // 0')"
|
|
205
|
+
RISK_COUNT="$(echo "$IMPACT" | jq '.risk_areas // [] | length')"
|
|
206
|
+
SEC_NOTES="$(echo "$SECURITY" | jq '.spec_security_notes // [] | length')"
|
|
207
|
+
TOTAL_RISKS=$((RISK_COUNT + SEC_NOTES))
|
|
208
|
+
|
|
209
|
+
echo "Type=$TYPE, ACs=$AC_COUNT, Files=$FILES_CHANGED, Risks=$TOTAL_RISKS" >&2
|
|
210
|
+
|
|
211
|
+
# Calculate effort
|
|
212
|
+
EFFORT="medium"
|
|
213
|
+
if [[ "$FILES_CHANGED" -le 3 ]] && [[ "$AC_COUNT" -le 3 ]]; then
|
|
214
|
+
EFFORT="small"
|
|
215
|
+
elif [[ "$FILES_CHANGED" -le 10 ]] && [[ "$AC_COUNT" -le 7 ]]; then
|
|
216
|
+
EFFORT="medium"
|
|
217
|
+
elif [[ "$FILES_CHANGED" -le 25 ]] && [[ "$AC_COUNT" -le 15 ]]; then
|
|
218
|
+
EFFORT="large"
|
|
219
|
+
else
|
|
220
|
+
EFFORT="xlarge"
|
|
221
|
+
fi
|
|
222
|
+
|
|
223
|
+
EFFORT_LABEL="$(jq -r --arg e "$EFFORT" '.effort_rules[$e].label // empty' "$TRIAGE_MATRIX")"
|
|
224
|
+
|
|
225
|
+
# Calculate priority (walk the rules in order, first match wins)
|
|
226
|
+
PRIORITY="P3"
|
|
227
|
+
PRIORITY_LABEL="priority:normal"
|
|
228
|
+
PRIORITY_MATCHED=false
|
|
229
|
+
|
|
230
|
+
while IFS= read -r rule; do
|
|
231
|
+
[[ -n "$rule" ]] || continue
|
|
232
|
+
RULE_TYPE="$(echo "$rule" | jq -r '.when.type // empty')"
|
|
233
|
+
RULE_EFFORT="$(echo "$rule" | jq -r '.when.effort // empty')"
|
|
234
|
+
RULE_MIN_RISK="$(echo "$rule" | jq -r '.when.min_risk_count // empty')"
|
|
235
|
+
RULE_MAX_RISK="$(echo "$rule" | jq -r '.when.max_risk_count // empty')"
|
|
236
|
+
RULE_PRIORITY="$(echo "$rule" | jq -r '.priority // empty')"
|
|
237
|
+
RULE_LABEL="$(echo "$rule" | jq -r '.label // empty')"
|
|
238
|
+
|
|
239
|
+
[[ -n "$RULE_PRIORITY" && -n "$RULE_LABEL" ]] || continue
|
|
240
|
+
[[ -z "$RULE_TYPE" || "$RULE_TYPE" == "$TYPE" ]] || continue
|
|
241
|
+
[[ -z "$RULE_EFFORT" || "$RULE_EFFORT" == "$EFFORT" ]] || continue
|
|
242
|
+
if [[ -n "$RULE_MIN_RISK" && "$RULE_MIN_RISK" != "null" ]]; then
|
|
243
|
+
[[ "$TOTAL_RISKS" -ge "$RULE_MIN_RISK" ]] || continue
|
|
244
|
+
fi
|
|
245
|
+
if [[ -n "$RULE_MAX_RISK" && "$RULE_MAX_RISK" != "null" ]]; then
|
|
246
|
+
[[ "$TOTAL_RISKS" -le "$RULE_MAX_RISK" ]] || continue
|
|
247
|
+
fi
|
|
248
|
+
|
|
249
|
+
PRIORITY="$RULE_PRIORITY"
|
|
250
|
+
PRIORITY_LABEL="$RULE_LABEL"
|
|
251
|
+
PRIORITY_MATCHED=true
|
|
252
|
+
break
|
|
253
|
+
done < <(jq -c '.priority_rules_v2 // [] | .[]' "$TRIAGE_MATRIX")
|
|
254
|
+
|
|
255
|
+
if [[ "$PRIORITY_MATCHED" != "true" ]]; then
|
|
256
|
+
if [[ "$TYPE" == "bugfix" ]] && [[ "$TOTAL_RISKS" -gt 2 ]]; then
|
|
257
|
+
PRIORITY="P0"; PRIORITY_LABEL="priority:critical"
|
|
258
|
+
elif [[ "$TYPE" == "bugfix" ]]; then
|
|
259
|
+
PRIORITY="P1"; PRIORITY_LABEL="priority:high"
|
|
260
|
+
elif [[ "$TYPE" == "infra" ]]; then
|
|
261
|
+
PRIORITY="P2"; PRIORITY_LABEL="priority:medium"
|
|
262
|
+
elif [[ "$TYPE" == "feature" ]] && [[ "$EFFORT" == "small" ]]; then
|
|
263
|
+
PRIORITY="P2"; PRIORITY_LABEL="priority:medium"
|
|
264
|
+
elif [[ "$TYPE" == "feature" ]]; then
|
|
265
|
+
PRIORITY="P3"; PRIORITY_LABEL="priority:normal"
|
|
266
|
+
else
|
|
267
|
+
PRIORITY="P3"; PRIORITY_LABEL="priority:normal"
|
|
268
|
+
fi
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
# Get type label
|
|
272
|
+
TYPE_LABEL="$(jq -r --arg t "$TYPE" '.auto_labels[$t] // empty' "$TRIAGE_MATRIX")"
|
|
273
|
+
DISPATCH_LABEL="$(jq -r '.dispatch_label // empty' "$TRIAGE_MATRIX")"
|
|
274
|
+
|
|
275
|
+
if [[ -z "$EFFORT_LABEL" || -z "$PRIORITY_LABEL" || -z "$DISPATCH_LABEL" ]]; then
|
|
276
|
+
echo "ERROR: Invalid triage matrix labels (effort/priority/dispatch)" >&2
|
|
277
|
+
exit 1
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
echo "Triage: $PRIORITY ($PRIORITY_LABEL), effort=$EFFORT ($EFFORT_LABEL)" >&2
|
|
281
|
+
|
|
282
|
+
READY_FOR_DISPATCH=true
|
|
283
|
+
TRIAGE_ERRORS=()
|
|
284
|
+
DISPATCH_STAGE_APPLIED=false
|
|
285
|
+
LEVEL_LABEL_APPLIED=false
|
|
286
|
+
TARGET_TRANSITIONED=false
|
|
287
|
+
|
|
288
|
+
# Definition of Ready (DoR) gate: fail-closed before dispatch.
|
|
289
|
+
if [[ -z "$OBJECTIVE" ]]; then
|
|
290
|
+
READY_FOR_DISPATCH=false
|
|
291
|
+
TRIAGE_ERRORS+=("dor_missing_objective")
|
|
292
|
+
fi
|
|
293
|
+
if [[ "$SCOPE_COUNT" -lt 1 ]]; then
|
|
294
|
+
READY_FOR_DISPATCH=false
|
|
295
|
+
TRIAGE_ERRORS+=("dor_missing_scope")
|
|
296
|
+
fi
|
|
297
|
+
if [[ "$AC_COUNT" -lt 1 ]]; then
|
|
298
|
+
READY_FOR_DISPATCH=false
|
|
299
|
+
TRIAGE_ERRORS+=("dor_missing_acceptance_criteria")
|
|
300
|
+
fi
|
|
301
|
+
if [[ "$DOD_COUNT" -lt 1 ]]; then
|
|
302
|
+
READY_FOR_DISPATCH=false
|
|
303
|
+
TRIAGE_ERRORS+=("dor_missing_definition_of_done")
|
|
304
|
+
fi
|
|
305
|
+
AC_LOWER="$(echo "$SPEC" | jq -r '.acceptance_criteria // [] | join(" ")' | tr '[:upper:]' '[:lower:]')"
|
|
306
|
+
SCOPE_LOWER="$(echo "$SPEC" | jq -r '.scope_v1 // [] | join(" ")' | tr '[:upper:]' '[:lower:]')"
|
|
307
|
+
OOS_LOWER="$(echo "$SPEC" | jq -r '.out_of_scope // [] | join(" ")' | tr '[:upper:]' '[:lower:]')"
|
|
308
|
+
if [[ "$DELIVERY_TARGET" == "web-ui" ]]; then
|
|
309
|
+
if ! echo "$AC_LOWER $SCOPE_LOWER" | grep -Eqi '\b(tela|página|pagina|ui|interface|dashboard|fluxo)\b'; then
|
|
310
|
+
READY_FOR_DISPATCH=false
|
|
311
|
+
TRIAGE_ERRORS+=("dor_web_ui_missing_ui_evidence")
|
|
312
|
+
fi
|
|
313
|
+
fi
|
|
314
|
+
if [[ "$DELIVERY_TARGET" == "api" ]]; then
|
|
315
|
+
if ! echo "$AC_LOWER $SCOPE_LOWER" | grep -Eqi '\b(api|endpoint|rota|route|http|rest)\b'; then
|
|
316
|
+
READY_FOR_DISPATCH=false
|
|
317
|
+
TRIAGE_ERRORS+=("dor_api_missing_endpoint_evidence")
|
|
318
|
+
fi
|
|
319
|
+
fi
|
|
320
|
+
if [[ "$DELIVERY_TARGET" == "hybrid" ]]; then
|
|
321
|
+
if ! echo "$AC_LOWER $SCOPE_LOWER" | grep -Eqi '\b(tela|página|pagina|ui|interface|dashboard|fluxo)\b'; then
|
|
322
|
+
READY_FOR_DISPATCH=false
|
|
323
|
+
TRIAGE_ERRORS+=("dor_hybrid_missing_ui_evidence")
|
|
324
|
+
fi
|
|
325
|
+
if ! echo "$AC_LOWER $SCOPE_LOWER" | grep -Eqi '\b(api|endpoint|rota|route|http|rest)\b'; then
|
|
326
|
+
READY_FOR_DISPATCH=false
|
|
327
|
+
TRIAGE_ERRORS+=("dor_hybrid_missing_api_evidence")
|
|
328
|
+
fi
|
|
329
|
+
fi
|
|
330
|
+
|
|
331
|
+
# Auth critical requirements gate.
|
|
332
|
+
AUTH_REGEX='\b(login|autentic|senha|perfil|permiss|acesso|rbac|admin)\b'
|
|
333
|
+
AUTH_SIGNAL=false
|
|
334
|
+
if [[ "$META_AUTH_SIGNAL" == "true" ]]; then
|
|
335
|
+
AUTH_SIGNAL=true
|
|
336
|
+
elif echo "$RAW_IDEA_LOWER $OBJECTIVE_LOWER" | grep -Eqi "$AUTH_REGEX"; then
|
|
337
|
+
AUTH_SIGNAL=true
|
|
338
|
+
fi
|
|
339
|
+
if [[ "$AUTH_SIGNAL" == "true" ]]; then
|
|
340
|
+
if ! echo "$AC_LOWER $SCOPE_LOWER $OBJECTIVE_LOWER" | grep -Eqi "$AUTH_REGEX"; then
|
|
341
|
+
READY_FOR_DISPATCH=false
|
|
342
|
+
TRIAGE_ERRORS+=("dor_auth_requirements_missing")
|
|
343
|
+
fi
|
|
344
|
+
if echo "$OOS_LOWER" | grep -Eqi "$AUTH_REGEX" && ! echo "$AC_LOWER" | grep -Eqi "$AUTH_REGEX"; then
|
|
345
|
+
READY_FOR_DISPATCH=false
|
|
346
|
+
TRIAGE_ERRORS+=("dor_auth_moved_to_out_of_scope_without_acceptance")
|
|
347
|
+
fi
|
|
348
|
+
fi
|
|
349
|
+
|
|
350
|
+
# Route by type (config-driven).
|
|
351
|
+
TARGET_QUEUE_LABEL="$(jq -r --arg t "$TYPE" '.target_state_by_type[$t] // .target_state_by_type.default // "To Do"' "$TRIAGE_MATRIX")"
|
|
352
|
+
if [[ -z "$TARGET_QUEUE_LABEL" || "$TARGET_QUEUE_LABEL" == "null" ]]; then
|
|
353
|
+
TARGET_QUEUE_LABEL="To Do"
|
|
354
|
+
fi
|
|
355
|
+
|
|
356
|
+
# Apply labels via gh
|
|
357
|
+
ALL_LABELS="$PRIORITY_LABEL,$EFFORT_LABEL"
|
|
358
|
+
[[ -n "$TYPE_LABEL" ]] && ALL_LABELS="$ALL_LABELS,$TYPE_LABEL"
|
|
359
|
+
if [[ "$READY_FOR_DISPATCH" != "true" ]]; then
|
|
360
|
+
ALL_LABELS="$ALL_LABELS,needs-human"
|
|
361
|
+
fi
|
|
362
|
+
|
|
363
|
+
echo "Applying labels to issue #$ISSUE_NUMBER: $ALL_LABELS" >&2
|
|
364
|
+
|
|
365
|
+
if [[ "$USE_DEVCLAW_TASKS" == "true" ]]; then
|
|
366
|
+
TASK_LABELS_CMD=(labels --project "$PROJECT_SLUG" --issue-id "$ISSUE_NUMBER" --add "$ALL_LABELS")
|
|
367
|
+
if [[ -n "$PROJECT_CHANNEL_ID" ]]; then
|
|
368
|
+
TASK_LABELS_CMD+=(--channel-id "$PROJECT_CHANNEL_ID")
|
|
369
|
+
fi
|
|
370
|
+
if [[ "$FACTORY_CHANGE" == "true" ]]; then
|
|
371
|
+
TASK_LABELS_CMD+=(--factory-change)
|
|
372
|
+
fi
|
|
373
|
+
if ! genesis_devclaw_task_json "${TASK_LABELS_CMD[@]}" >/dev/null 2>>"$GENESIS_LOG"; then
|
|
374
|
+
READY_FOR_DISPATCH=false
|
|
375
|
+
TRIAGE_ERRORS+=("apply_labels_failed")
|
|
376
|
+
fi
|
|
377
|
+
else
|
|
378
|
+
if ! gh issue edit "$ISSUE_NUMBER" \
|
|
379
|
+
--repo "$OWNER_REPO" \
|
|
380
|
+
--add-label "$ALL_LABELS" >/dev/null 2>&1; then
|
|
381
|
+
READY_FOR_DISPATCH=false
|
|
382
|
+
TRIAGE_ERRORS+=("apply_labels_failed")
|
|
383
|
+
else
|
|
384
|
+
:
|
|
385
|
+
fi
|
|
386
|
+
fi
|
|
387
|
+
|
|
388
|
+
if [[ "$READY_FOR_DISPATCH" != "true" ]]; then
|
|
389
|
+
DOR_COMMENT="## Triage blocked by Definition of Ready
|
|
390
|
+
|
|
391
|
+
The issue stayed in **Planning** because required fields are missing:
|
|
392
|
+
$(printf '%s\n' "${TRIAGE_ERRORS[@]}" | sed 's/^/- /')
|
|
393
|
+
"
|
|
394
|
+
if [[ "$USE_DEVCLAW_TASKS" == "true" ]]; then
|
|
395
|
+
DOR_COMMENT_FILE="$(mktemp)"
|
|
396
|
+
printf '%s\n' "$DOR_COMMENT" > "$DOR_COMMENT_FILE"
|
|
397
|
+
TASK_DOR_CMD=(comment --project "$PROJECT_SLUG" --issue-id "$ISSUE_NUMBER" --body-file "$DOR_COMMENT_FILE")
|
|
398
|
+
if [[ -n "$PROJECT_CHANNEL_ID" ]]; then
|
|
399
|
+
TASK_DOR_CMD+=(--channel-id "$PROJECT_CHANNEL_ID")
|
|
400
|
+
fi
|
|
401
|
+
genesis_devclaw_task_json "${TASK_DOR_CMD[@]}" >/dev/null 2>>"$GENESIS_LOG" || true
|
|
402
|
+
rm -f "$DOR_COMMENT_FILE"
|
|
403
|
+
else
|
|
404
|
+
gh issue comment "$ISSUE_NUMBER" --repo "$OWNER_REPO" --body "$DOR_COMMENT" >/dev/null 2>&1 || true
|
|
405
|
+
fi
|
|
406
|
+
fi
|
|
407
|
+
|
|
408
|
+
echo "Issue #$ISSUE_NUMBER triaged (dispatch label '$DISPATCH_LABEL' requested)" >&2
|
|
409
|
+
|
|
410
|
+
# === Auto-transition: Planning → target queue ===
|
|
411
|
+
# Determine worker level from effort/complexity
|
|
412
|
+
LEVEL="medior"
|
|
413
|
+
if [[ "$EFFORT" == "small" ]]; then
|
|
414
|
+
LEVEL="junior"
|
|
415
|
+
elif [[ "$EFFORT" == "large" ]] || [[ "$EFFORT" == "xlarge" ]]; then
|
|
416
|
+
LEVEL="senior"
|
|
417
|
+
fi
|
|
418
|
+
if [[ "$TARGET_QUEUE_LABEL" == "To Research" && "$LEVEL" == "medior" ]]; then
|
|
419
|
+
LEVEL="junior"
|
|
420
|
+
fi
|
|
421
|
+
|
|
422
|
+
if [[ "$READY_FOR_DISPATCH" == "true" && "$TARGET_QUEUE_LABEL" == "To Do" && "$USE_DEVCLAW_TASKS" == "true" ]]; then
|
|
423
|
+
echo "Dispatching via deterministic DevClaw task_start (Planning → To Do, level=$LEVEL)..." >&2
|
|
424
|
+
TASK_START_CMD=(start --project "$PROJECT_SLUG" --issue-id "$ISSUE_NUMBER" --level "$LEVEL")
|
|
425
|
+
if [[ -n "$PROJECT_CHANNEL_ID" ]]; then
|
|
426
|
+
TASK_START_CMD+=(--channel-id "$PROJECT_CHANNEL_ID")
|
|
427
|
+
fi
|
|
428
|
+
if [[ "$FACTORY_CHANGE" == "true" ]]; then
|
|
429
|
+
TASK_START_CMD+=(--factory-change)
|
|
430
|
+
fi
|
|
431
|
+
if ! genesis_devclaw_task_json "${TASK_START_CMD[@]}" >/dev/null 2>>"$GENESIS_LOG"; then
|
|
432
|
+
READY_FOR_DISPATCH=false
|
|
433
|
+
TRIAGE_ERRORS+=("task_start_failed")
|
|
434
|
+
else
|
|
435
|
+
TARGET_TRANSITIONED=true
|
|
436
|
+
LEVEL_LABEL_APPLIED=true
|
|
437
|
+
DISPATCH_STAGE_APPLIED=true
|
|
438
|
+
fi
|
|
439
|
+
elif [[ "$READY_FOR_DISPATCH" == "true" && "$USE_DEVCLAW_TASKS" == "true" ]]; then
|
|
440
|
+
echo "Routing deterministically via DevClaw task route (Planning → $TARGET_QUEUE_LABEL, level=$LEVEL)..." >&2
|
|
441
|
+
TASK_ROUTE_CMD=(route --project "$PROJECT_SLUG" --issue-id "$ISSUE_NUMBER" --to-label "$TARGET_QUEUE_LABEL")
|
|
442
|
+
if [[ -n "$PROJECT_CHANNEL_ID" ]]; then
|
|
443
|
+
TASK_ROUTE_CMD+=(--channel-id "$PROJECT_CHANNEL_ID")
|
|
444
|
+
fi
|
|
445
|
+
if [[ -n "$LEVEL" ]]; then
|
|
446
|
+
TASK_ROUTE_CMD+=(--level "$LEVEL")
|
|
447
|
+
fi
|
|
448
|
+
if [[ "$FACTORY_CHANGE" == "true" ]]; then
|
|
449
|
+
TASK_ROUTE_CMD+=(--factory-change)
|
|
450
|
+
fi
|
|
451
|
+
if ! genesis_devclaw_task_json "${TASK_ROUTE_CMD[@]}" >/dev/null 2>>"$GENESIS_LOG"; then
|
|
452
|
+
READY_FOR_DISPATCH=false
|
|
453
|
+
TRIAGE_ERRORS+=("task_route_failed")
|
|
454
|
+
else
|
|
455
|
+
TARGET_TRANSITIONED=true
|
|
456
|
+
LEVEL_LABEL_APPLIED=true
|
|
457
|
+
fi
|
|
458
|
+
else
|
|
459
|
+
# Add developer level label (legacy gh path or non-deterministic fallback)
|
|
460
|
+
if [[ "$READY_FOR_DISPATCH" == "true" && "$TARGET_QUEUE_LABEL" == "To Do" ]]; then
|
|
461
|
+
echo "Setting developer level: developer:$LEVEL" >&2
|
|
462
|
+
if ! gh issue edit "$ISSUE_NUMBER" \
|
|
463
|
+
--repo "$OWNER_REPO" \
|
|
464
|
+
--add-label "developer:$LEVEL" >/dev/null 2>>"$GENESIS_LOG"; then
|
|
465
|
+
READY_FOR_DISPATCH=false
|
|
466
|
+
TRIAGE_ERRORS+=("level_label_failed")
|
|
467
|
+
else
|
|
468
|
+
LEVEL_LABEL_APPLIED=true
|
|
469
|
+
fi
|
|
470
|
+
else
|
|
471
|
+
echo "Skipping developer level label (target=$TARGET_QUEUE_LABEL, ready=$READY_FOR_DISPATCH)" >&2
|
|
472
|
+
fi
|
|
473
|
+
|
|
474
|
+
# Swap Planning → target queue (heartbeat will auto-dispatch workers)
|
|
475
|
+
if [[ "$READY_FOR_DISPATCH" == "true" ]]; then
|
|
476
|
+
echo "Transitioning issue #$ISSUE_NUMBER: Planning → $TARGET_QUEUE_LABEL" >&2
|
|
477
|
+
|
|
478
|
+
TRANSITION_LABELS="$TARGET_QUEUE_LABEL"
|
|
479
|
+
if [[ "$TARGET_QUEUE_LABEL" == "To Do" ]]; then
|
|
480
|
+
TRANSITION_LABELS="$TRANSITION_LABELS,$DISPATCH_LABEL"
|
|
481
|
+
fi
|
|
482
|
+
|
|
483
|
+
if ! gh issue edit "$ISSUE_NUMBER" \
|
|
484
|
+
--repo "$OWNER_REPO" \
|
|
485
|
+
--remove-label "Planning" \
|
|
486
|
+
--add-label "$TRANSITION_LABELS" >/dev/null 2>>"$GENESIS_LOG"; then
|
|
487
|
+
CURRENT_LABELS="$(gh issue view "$ISSUE_NUMBER" --repo "$OWNER_REPO" --json labels --jq '.labels[].name' 2>/dev/null || true)"
|
|
488
|
+
if echo "$CURRENT_LABELS" | grep -qxF "$TARGET_QUEUE_LABEL"; then
|
|
489
|
+
TARGET_TRANSITIONED=true
|
|
490
|
+
[[ "$TARGET_QUEUE_LABEL" == "To Do" ]] && DISPATCH_STAGE_APPLIED=true
|
|
491
|
+
else
|
|
492
|
+
READY_FOR_DISPATCH=false
|
|
493
|
+
TRIAGE_ERRORS+=("planning_to_target_failed")
|
|
494
|
+
fi
|
|
495
|
+
else
|
|
496
|
+
TARGET_TRANSITIONED=true
|
|
497
|
+
[[ "$TARGET_QUEUE_LABEL" == "To Do" ]] && DISPATCH_STAGE_APPLIED=true
|
|
498
|
+
fi
|
|
499
|
+
else
|
|
500
|
+
echo "Skipping Planning → target transition due to previous triage failure" >&2
|
|
501
|
+
fi
|
|
502
|
+
fi
|
|
503
|
+
|
|
504
|
+
if [[ "$READY_FOR_DISPATCH" == "true" ]]; then
|
|
505
|
+
if [[ "$USE_DEVCLAW_TASKS" == "true" ]]; then
|
|
506
|
+
TASK_CLEAR_NEEDS_HUMAN_CMD=(labels --project "$PROJECT_SLUG" --issue-id "$ISSUE_NUMBER" --remove "needs-human")
|
|
507
|
+
if [[ -n "$PROJECT_CHANNEL_ID" ]]; then
|
|
508
|
+
TASK_CLEAR_NEEDS_HUMAN_CMD+=(--channel-id "$PROJECT_CHANNEL_ID")
|
|
509
|
+
fi
|
|
510
|
+
if [[ "$FACTORY_CHANGE" == "true" ]]; then
|
|
511
|
+
TASK_CLEAR_NEEDS_HUMAN_CMD+=(--factory-change)
|
|
512
|
+
fi
|
|
513
|
+
genesis_devclaw_task_json "${TASK_CLEAR_NEEDS_HUMAN_CMD[@]}" >/dev/null 2>>"$GENESIS_LOG" || true
|
|
514
|
+
else
|
|
515
|
+
gh issue edit "$ISSUE_NUMBER" --repo "$OWNER_REPO" --remove-label "needs-human" >/dev/null 2>&1 || true
|
|
516
|
+
fi
|
|
517
|
+
echo "Issue #$ISSUE_NUMBER dispatched to $TARGET_QUEUE_LABEL (level=$LEVEL)" >&2
|
|
518
|
+
else
|
|
519
|
+
echo "Issue #$ISSUE_NUMBER NOT dispatched; triage failed closed: ${TRIAGE_ERRORS[*]}" >&2
|
|
520
|
+
if [[ "$USE_DEVCLAW_TASKS" == "true" ]]; then
|
|
521
|
+
TASK_SET_NEEDS_HUMAN_CMD=(labels --project "$PROJECT_SLUG" --issue-id "$ISSUE_NUMBER" --add "needs-human")
|
|
522
|
+
if [[ -n "$PROJECT_CHANNEL_ID" ]]; then
|
|
523
|
+
TASK_SET_NEEDS_HUMAN_CMD+=(--channel-id "$PROJECT_CHANNEL_ID")
|
|
524
|
+
fi
|
|
525
|
+
if [[ "$FACTORY_CHANGE" == "true" ]]; then
|
|
526
|
+
TASK_SET_NEEDS_HUMAN_CMD+=(--factory-change)
|
|
527
|
+
fi
|
|
528
|
+
if ! genesis_devclaw_task_json "${TASK_SET_NEEDS_HUMAN_CMD[@]}" >/dev/null 2>>"$GENESIS_LOG"; then
|
|
529
|
+
TRIAGE_ERRORS+=("mark_needs_human_failed")
|
|
530
|
+
fi
|
|
531
|
+
elif ! gh issue edit "$ISSUE_NUMBER" --repo "$OWNER_REPO" --add-label "needs-human" >/dev/null 2>&1; then
|
|
532
|
+
TRIAGE_ERRORS+=("mark_needs_human_failed")
|
|
533
|
+
fi
|
|
534
|
+
|
|
535
|
+
if [[ "$TARGET_TRANSITIONED" == "true" ]]; then
|
|
536
|
+
if ! gh issue edit "$ISSUE_NUMBER" \
|
|
537
|
+
--repo "$OWNER_REPO" \
|
|
538
|
+
--remove-label "$TARGET_QUEUE_LABEL" \
|
|
539
|
+
--add-label "Planning" >/dev/null 2>>"$GENESIS_LOG"; then
|
|
540
|
+
TRIAGE_ERRORS+=("rollback_target_failed")
|
|
541
|
+
fi
|
|
542
|
+
fi
|
|
543
|
+
|
|
544
|
+
ROLLBACK_REMOVE_LABELS=()
|
|
545
|
+
if [[ "$LEVEL_LABEL_APPLIED" == "true" ]]; then
|
|
546
|
+
ROLLBACK_REMOVE_LABELS+=("developer:$LEVEL")
|
|
547
|
+
fi
|
|
548
|
+
if [[ "$DISPATCH_STAGE_APPLIED" == "true" ]]; then
|
|
549
|
+
ROLLBACK_REMOVE_LABELS+=("$DISPATCH_LABEL")
|
|
550
|
+
fi
|
|
551
|
+
|
|
552
|
+
if [[ "${#ROLLBACK_REMOVE_LABELS[@]}" -gt 0 ]]; then
|
|
553
|
+
ROLLBACK_REMOVE_CSV="$(IFS=,; echo "${ROLLBACK_REMOVE_LABELS[*]}")"
|
|
554
|
+
if ! gh issue edit "$ISSUE_NUMBER" \
|
|
555
|
+
--repo "$OWNER_REPO" \
|
|
556
|
+
--remove-label "$ROLLBACK_REMOVE_CSV" >/dev/null 2>>"$GENESIS_LOG"; then
|
|
557
|
+
TRIAGE_ERRORS+=("rollback_labels_failed")
|
|
558
|
+
fi
|
|
559
|
+
fi
|
|
560
|
+
fi
|
|
561
|
+
|
|
562
|
+
# Cleanup sideband files
|
|
563
|
+
genesis_sideband_cleanup "$SESSION_ID"
|
|
564
|
+
|
|
565
|
+
if [[ "${#TRIAGE_ERRORS[@]}" -gt 0 ]]; then
|
|
566
|
+
TRIAGE_ERRORS_JSON="$(printf '%s\n' "${TRIAGE_ERRORS[@]}" | jq -R . | jq -s .)"
|
|
567
|
+
else
|
|
568
|
+
TRIAGE_ERRORS_JSON="[]"
|
|
569
|
+
fi
|
|
570
|
+
echo "triage.sh completed for session $SESSION_ID (ready=$READY_FOR_DISPATCH, errors=${#TRIAGE_ERRORS[@]})" >&2
|
|
571
|
+
jq -n \
|
|
572
|
+
--arg sid "$SESSION_ID" \
|
|
573
|
+
--arg priority "$PRIORITY" \
|
|
574
|
+
--arg effort "$EFFORT" \
|
|
575
|
+
--arg target "$TARGET_QUEUE_LABEL" \
|
|
576
|
+
--arg project_slug "$PROJECT_SLUG" \
|
|
577
|
+
--arg project_channel_id "$PROJECT_CHANNEL_ID" \
|
|
578
|
+
--arg labels "$ALL_LABELS" \
|
|
579
|
+
--argjson ready "$([[ "$READY_FOR_DISPATCH" == "true" ]] && echo "true" || echo "false")" \
|
|
580
|
+
--argjson errors "$TRIAGE_ERRORS_JSON" \
|
|
581
|
+
--argjson num "$ISSUE_NUMBER" \
|
|
582
|
+
'{
|
|
583
|
+
session_id: $sid,
|
|
584
|
+
step: "triage",
|
|
585
|
+
triage: {
|
|
586
|
+
priority: $priority,
|
|
587
|
+
effort: $effort,
|
|
588
|
+
target_state: $target,
|
|
589
|
+
project_slug: (if $project_slug != "" then $project_slug else null end),
|
|
590
|
+
project_channel_id: (if $project_channel_id != "" then $project_channel_id else null end),
|
|
591
|
+
labels_applied: ($labels | split(",")),
|
|
592
|
+
issue_number: $num,
|
|
593
|
+
ready_for_dispatch: $ready,
|
|
594
|
+
errors: $errors
|
|
595
|
+
}
|
|
596
|
+
}'
|
|
597
|
+
|
|
598
|
+
genesis_metric_end "ok"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Validate critical Genesis step envelopes with deterministic jq checks.
|
|
5
|
+
# Usage: validate-step.sh <classify|spec|impact>
|
|
6
|
+
# Input: JSON on stdin
|
|
7
|
+
# Output: same JSON on stdout (if valid)
|
|
8
|
+
|
|
9
|
+
STEP="${1:-}"
|
|
10
|
+
if [[ -n "${1:-}" && -f "${1:-}" ]]; then
|
|
11
|
+
INPUT="$(cat "$1")"
|
|
12
|
+
else
|
|
13
|
+
INPUT="$(cat)"
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
fail() {
|
|
17
|
+
local reason="${1:-validation_failed}"
|
|
18
|
+
echo "ERROR: [$STEP] $reason" >&2
|
|
19
|
+
exit 1
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
require_json() {
|
|
23
|
+
echo "$INPUT" | jq -e . >/dev/null 2>&1 || fail "invalid_json"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
validate_classify() {
|
|
27
|
+
echo "$INPUT" | jq -e '
|
|
28
|
+
(.session_id | type == "string" and length > 0) and
|
|
29
|
+
(.step == "classify") and
|
|
30
|
+
(.raw_idea | type == "string" and length > 0) and
|
|
31
|
+
(.classification | type == "object") and
|
|
32
|
+
(.classification.type | IN("feature","bugfix","refactor","research","infra")) and
|
|
33
|
+
(.classification.confidence | type == "number" and . >= 0 and . <= 1) and
|
|
34
|
+
(.classification.reasoning | type == "string" and length > 0) and
|
|
35
|
+
((.classification.delivery_target // "unknown") | IN("web-ui","api","cli","hybrid","unknown"))
|
|
36
|
+
' >/dev/null 2>&1 || fail "schema_violation"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
validate_spec() {
|
|
40
|
+
echo "$INPUT" | jq -e '
|
|
41
|
+
(.session_id | type == "string" and length > 0) and
|
|
42
|
+
(.step == "spec") and
|
|
43
|
+
(.spec | type == "object") and
|
|
44
|
+
(.spec.title | type == "string" and length > 0) and
|
|
45
|
+
(.spec.type | IN("feature","bugfix","refactor","research","infra")) and
|
|
46
|
+
(.spec.objective | type == "string" and length >= 10) and
|
|
47
|
+
(.spec.scope_v1 | type == "array" and length >= 1) and
|
|
48
|
+
(.spec.out_of_scope | type == "array") and
|
|
49
|
+
(.spec.acceptance_criteria | type == "array" and length >= 1) and
|
|
50
|
+
(.spec.definition_of_done | type == "array" and length >= 1) and
|
|
51
|
+
(.spec.constraints | type == "string") and
|
|
52
|
+
((.spec.delivery_target // "unknown") | IN("web-ui","api","cli","hybrid","unknown"))
|
|
53
|
+
' >/dev/null 2>&1 || fail "schema_violation"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
validate_impact() {
|
|
57
|
+
echo "$INPUT" | jq -e '
|
|
58
|
+
(.session_id | type == "string" and length > 0) and
|
|
59
|
+
(.step == "impact") and
|
|
60
|
+
(.impact | type == "object") and
|
|
61
|
+
(.impact.affected_files | type == "array") and
|
|
62
|
+
(.impact.new_files_needed | type == "array") and
|
|
63
|
+
(.impact.affected_modules | type == "array") and
|
|
64
|
+
(.impact.risk_areas | type == "array") and
|
|
65
|
+
(.impact.estimated_files_changed | type == "number" and . >= 0 and (floor == .)) and
|
|
66
|
+
(.impact.is_greenfield | type == "boolean")
|
|
67
|
+
' >/dev/null 2>&1 || fail "schema_violation"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
require_json
|
|
71
|
+
|
|
72
|
+
case "$STEP" in
|
|
73
|
+
classify) validate_classify ;;
|
|
74
|
+
spec) validate_spec ;;
|
|
75
|
+
impact) validate_impact ;;
|
|
76
|
+
*)
|
|
77
|
+
fail "unknown_step"
|
|
78
|
+
;;
|
|
79
|
+
esac
|
|
80
|
+
|
|
81
|
+
echo "$INPUT"
|