@jiggai/recipes 0.3.10 → 0.4.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/package.json
CHANGED
|
@@ -9,14 +9,14 @@ cronJobs:
|
|
|
9
9
|
name: "Lead triage loop"
|
|
10
10
|
schedule: "*/30 7-23 * * 1-5"
|
|
11
11
|
timezone: "America/New_York"
|
|
12
|
-
message: "Automated lead triage loop: triage inbox/tickets, assign work, and update notes/status.md. Anti-stuck: if lowest in-progress is HARD BLOCKED, advance the next unblocked ticket (or pull from backlog). If in-progress is stale (>12h no dated update), comment or move it back. Guardrail: run ./scripts/ticket-hygiene.sh each loop; if it fails, fix assignment stubs
|
|
12
|
+
message: "Automated lead triage loop: triage inbox/tickets, assign work, and update notes/status.md. Anti-stuck: if lowest in-progress is HARD BLOCKED, advance the next unblocked ticket (or pull from backlog). If in-progress is stale (>12h no dated update), comment or move it back. Guardrail: run ./scripts/ticket-hygiene.sh each loop; if it fails, fix lane/status/owner mismatches before proceeding (assignment stubs are deprecated)."
|
|
13
13
|
enabledByDefault: true
|
|
14
14
|
|
|
15
15
|
- id: execution-loop
|
|
16
16
|
name: "Execution loop"
|
|
17
17
|
schedule: "*/30 7-23 * * 1-5"
|
|
18
18
|
timezone: "America/New_York"
|
|
19
|
-
message: "Automated execution loop: make progress on in-progress tickets, keep changes small/safe, and update notes/status.md. Guardrail: run ./scripts/ticket-hygiene-dev.sh each loop; if it fails, fix
|
|
19
|
+
message: "Automated execution loop: make progress on in-progress tickets, keep changes small/safe, and update notes/status.md. Guardrail: run ./scripts/ticket-hygiene-dev.sh each loop; if it fails, fix lane/status/owner mismatches before proceeding (assignment stubs are deprecated)."
|
|
20
20
|
enabledByDefault: false
|
|
21
21
|
|
|
22
22
|
- id: pr-watcher
|
|
@@ -70,78 +70,111 @@ agents:
|
|
|
70
70
|
deny: []
|
|
71
71
|
|
|
72
72
|
templates:
|
|
73
|
+
sharedContext.ticketFlow: |
|
|
74
|
+
{
|
|
75
|
+
"laneByOwner": {
|
|
76
|
+
"lead": "backlog",
|
|
77
|
+
"dev": "in-progress",
|
|
78
|
+
"devops": "in-progress",
|
|
79
|
+
"test": "testing",
|
|
80
|
+
"qa": "testing"
|
|
81
|
+
},
|
|
82
|
+
"defaultLane": "in-progress"
|
|
83
|
+
}
|
|
84
|
+
|
|
73
85
|
lead.ticketHygiene: |
|
|
74
86
|
#!/usr/bin/env bash
|
|
75
87
|
set -euo pipefail
|
|
76
88
|
|
|
77
89
|
# ticket-hygiene.sh
|
|
78
|
-
# Guardrail script used by lead triage
|
|
79
|
-
#
|
|
90
|
+
# Guardrail script used by lead triage + execution loops.
|
|
91
|
+
# Assignment stubs are deprecated.
|
|
92
|
+
#
|
|
93
|
+
# Checks (ACTIVE lanes only):
|
|
94
|
+
# - Ticket file location (lane) must match Status:
|
|
95
|
+
# - Ticket Owner should be in the expected lane per shared-context/ticket-flow.json (best-effort)
|
|
96
|
+
#
|
|
97
|
+
# Notes:
|
|
98
|
+
# - We intentionally do NOT enforce mapping for work/done/ because historical tickets may have old Owner/Status.
|
|
80
99
|
|
|
81
100
|
cd "$(dirname "$0")/.."
|
|
82
101
|
|
|
83
|
-
stages=(backlog in-progress testing done)
|
|
84
|
-
|
|
85
102
|
fail=0
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
103
|
+
flow="shared-context/ticket-flow.json"
|
|
104
|
+
|
|
105
|
+
lane_from_rel() {
|
|
106
|
+
# expects work/<lane>/<file>.md
|
|
107
|
+
echo "$1" | sed -E 's#^work/([^/]+)/.*$##'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
field_from_md() {
|
|
111
|
+
local file="$1"
|
|
112
|
+
local key="$2"
|
|
113
|
+
# Extract first matching header line like: Key: value
|
|
114
|
+
local line
|
|
115
|
+
line="$(grep -m1 -E "^${key}:[[:space:]]*" "$file" 2>/dev/null || true)"
|
|
116
|
+
echo "${line#${key}:}" | sed -E 's/^\s+//'
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
expected_lane_for_owner() {
|
|
120
|
+
local owner="$1"
|
|
121
|
+
local currentLane="$2"
|
|
122
|
+
|
|
123
|
+
# If jq or the mapping file isn't available, do not block progress.
|
|
124
|
+
if [[ ! -f "$flow" ]]; then
|
|
125
|
+
echo "$currentLane"
|
|
126
|
+
return 0
|
|
94
127
|
fi
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if [[ "$bn" == *"pr-body"* ]]; then
|
|
99
|
-
for stage in "${stages[@]}"; do
|
|
100
|
-
cand="work/$stage/${num}-pr-body.md"
|
|
101
|
-
if [[ -f "$cand" ]]; then
|
|
102
|
-
canonical="$cand"
|
|
103
|
-
break
|
|
104
|
-
fi
|
|
105
|
-
done
|
|
128
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
129
|
+
echo "$currentLane"
|
|
130
|
+
return 0
|
|
106
131
|
fi
|
|
107
132
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
fi
|
|
115
|
-
done
|
|
133
|
+
local out
|
|
134
|
+
out="$(jq -r --arg o "$owner" '.laneByOwner[$o] // .defaultLane // empty' "$flow" 2>/dev/null || true)"
|
|
135
|
+
if [[ -n "$out" ]]; then
|
|
136
|
+
echo "$out"
|
|
137
|
+
else
|
|
138
|
+
echo "$currentLane"
|
|
116
139
|
fi
|
|
140
|
+
}
|
|
117
141
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
fi
|
|
142
|
+
check_ticket() {
|
|
143
|
+
local file="$1"
|
|
144
|
+
local rel="$file"
|
|
145
|
+
rel="${rel#./}"
|
|
123
146
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
147
|
+
local lane
|
|
148
|
+
lane="$(lane_from_rel "$rel")"
|
|
149
|
+
|
|
150
|
+
# Ignore done lane for owner/status enforcement.
|
|
151
|
+
if [[ "$lane" == "done" ]]; then
|
|
152
|
+
return 0
|
|
129
153
|
fi
|
|
130
154
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
155
|
+
local owner status
|
|
156
|
+
owner="$(field_from_md "$file" "Owner")"
|
|
157
|
+
status="$(field_from_md "$file" "Status")"
|
|
158
|
+
|
|
159
|
+
if [[ -n "$status" && "$status" != "$lane" ]]; then
|
|
160
|
+
echo "[FAIL] $rel: Status mismatch (has: $status, lane: $lane)" >&2
|
|
135
161
|
fail=1
|
|
136
|
-
continue
|
|
137
162
|
fi
|
|
138
163
|
|
|
139
|
-
if [[
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
164
|
+
if [[ -n "$owner" ]]; then
|
|
165
|
+
local expected
|
|
166
|
+
expected="$(expected_lane_for_owner "$owner" "$lane")"
|
|
167
|
+
if [[ -n "$expected" && "$expected" != "$lane" ]]; then
|
|
168
|
+
echo "[FAIL] $rel: Owner '$owner' expects lane '$expected' per $flow (currently in '$lane')" >&2
|
|
169
|
+
fail=1
|
|
170
|
+
fi
|
|
143
171
|
fi
|
|
172
|
+
}
|
|
144
173
|
|
|
174
|
+
shopt -s nullglob
|
|
175
|
+
for file in work/backlog/*.md work/in-progress/*.md work/testing/*.md work/done/*.md; do
|
|
176
|
+
[[ -f "$file" ]] || continue
|
|
177
|
+
check_ticket "$file"
|
|
145
178
|
done
|
|
146
179
|
|
|
147
180
|
if [[ "$fail" -ne 0 ]]; then
|
|
@@ -186,73 +219,94 @@ templates:
|
|
|
186
219
|
set -euo pipefail
|
|
187
220
|
|
|
188
221
|
# ticket-hygiene.sh
|
|
189
|
-
# Guardrail script used by lead triage
|
|
190
|
-
#
|
|
222
|
+
# Guardrail script used by lead triage + execution loops.
|
|
223
|
+
# Assignment stubs are deprecated.
|
|
224
|
+
#
|
|
225
|
+
# Checks (ACTIVE lanes only):
|
|
226
|
+
# - Ticket file location (lane) must match Status:
|
|
227
|
+
# - Ticket Owner should be in the expected lane per shared-context/ticket-flow.json (best-effort)
|
|
228
|
+
#
|
|
229
|
+
# Notes:
|
|
230
|
+
# - We intentionally do NOT enforce mapping for work/done/ because historical tickets may have old Owner/Status.
|
|
191
231
|
|
|
192
232
|
cd "$(dirname "$0")/.."
|
|
193
233
|
|
|
194
|
-
stages=(backlog in-progress testing done)
|
|
195
|
-
|
|
196
234
|
fail=0
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
235
|
+
flow="shared-context/ticket-flow.json"
|
|
236
|
+
|
|
237
|
+
lane_from_rel() {
|
|
238
|
+
# expects work/<lane>/<file>.md
|
|
239
|
+
echo "$1" | sed -E 's#^work/([^/]+)/.*$##'
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
field_from_md() {
|
|
243
|
+
local file="$1"
|
|
244
|
+
local key="$2"
|
|
245
|
+
# Extract first matching header line like: Key: value
|
|
246
|
+
local line
|
|
247
|
+
line="$(grep -m1 -E "^${key}:[[:space:]]*" "$file" 2>/dev/null || true)"
|
|
248
|
+
echo "${line#${key}:}" | sed -E 's/^\s+//'
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
expected_lane_for_owner() {
|
|
252
|
+
local owner="$1"
|
|
253
|
+
local currentLane="$2"
|
|
254
|
+
|
|
255
|
+
# If jq or the mapping file isn't available, do not block progress.
|
|
256
|
+
if [[ ! -f "$flow" ]]; then
|
|
257
|
+
echo "$currentLane"
|
|
258
|
+
return 0
|
|
205
259
|
fi
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if [[ "$bn" == *"pr-body"* ]]; then
|
|
210
|
-
for stage in "${stages[@]}"; do
|
|
211
|
-
cand="work/$stage/${num}-pr-body.md"
|
|
212
|
-
if [[ -f "$cand" ]]; then
|
|
213
|
-
canonical="$cand"
|
|
214
|
-
break
|
|
215
|
-
fi
|
|
216
|
-
done
|
|
260
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
261
|
+
echo "$currentLane"
|
|
262
|
+
return 0
|
|
217
263
|
fi
|
|
218
264
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
fi
|
|
226
|
-
done
|
|
265
|
+
local out
|
|
266
|
+
out="$(jq -r --arg o "$owner" '.laneByOwner[$o] // .defaultLane // empty' "$flow" 2>/dev/null || true)"
|
|
267
|
+
if [[ -n "$out" ]]; then
|
|
268
|
+
echo "$out"
|
|
269
|
+
else
|
|
270
|
+
echo "$currentLane"
|
|
227
271
|
fi
|
|
272
|
+
}
|
|
228
273
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
fi
|
|
274
|
+
check_ticket() {
|
|
275
|
+
local file="$1"
|
|
276
|
+
local rel="$file"
|
|
277
|
+
rel="${rel#./}"
|
|
234
278
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
279
|
+
local lane
|
|
280
|
+
lane="$(lane_from_rel "$rel")"
|
|
281
|
+
|
|
282
|
+
# Ignore done lane for owner/status enforcement.
|
|
283
|
+
if [[ "$lane" == "done" ]]; then
|
|
284
|
+
return 0
|
|
240
285
|
fi
|
|
241
286
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
287
|
+
local owner status
|
|
288
|
+
owner="$(field_from_md "$file" "Owner")"
|
|
289
|
+
status="$(field_from_md "$file" "Status")"
|
|
290
|
+
|
|
291
|
+
if [[ -n "$status" && "$status" != "$lane" ]]; then
|
|
292
|
+
echo "[FAIL] $rel: Status mismatch (has: $status, lane: $lane)" >&2
|
|
246
293
|
fail=1
|
|
247
|
-
continue
|
|
248
294
|
fi
|
|
249
295
|
|
|
250
|
-
if [[
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
296
|
+
if [[ -n "$owner" ]]; then
|
|
297
|
+
local expected
|
|
298
|
+
expected="$(expected_lane_for_owner "$owner" "$lane")"
|
|
299
|
+
if [[ -n "$expected" && "$expected" != "$lane" ]]; then
|
|
300
|
+
echo "[FAIL] $rel: Owner '$owner' expects lane '$expected' per $flow (currently in '$lane')" >&2
|
|
301
|
+
fail=1
|
|
302
|
+
fi
|
|
254
303
|
fi
|
|
304
|
+
}
|
|
255
305
|
|
|
306
|
+
shopt -s nullglob
|
|
307
|
+
for file in work/backlog/*.md work/in-progress/*.md work/testing/*.md work/done/*.md; do
|
|
308
|
+
[[ -f "$file" ]] || continue
|
|
309
|
+
check_ticket "$file"
|
|
256
310
|
done
|
|
257
311
|
|
|
258
312
|
if [[ "$fail" -ne 0 ]]; then
|
|
@@ -294,73 +348,94 @@ templates:
|
|
|
294
348
|
set -euo pipefail
|
|
295
349
|
|
|
296
350
|
# ticket-hygiene.sh
|
|
297
|
-
# Guardrail script used by lead triage
|
|
298
|
-
#
|
|
351
|
+
# Guardrail script used by lead triage + execution loops.
|
|
352
|
+
# Assignment stubs are deprecated.
|
|
353
|
+
#
|
|
354
|
+
# Checks (ACTIVE lanes only):
|
|
355
|
+
# - Ticket file location (lane) must match Status:
|
|
356
|
+
# - Ticket Owner should be in the expected lane per shared-context/ticket-flow.json (best-effort)
|
|
357
|
+
#
|
|
358
|
+
# Notes:
|
|
359
|
+
# - We intentionally do NOT enforce mapping for work/done/ because historical tickets may have old Owner/Status.
|
|
299
360
|
|
|
300
361
|
cd "$(dirname "$0")/.."
|
|
301
362
|
|
|
302
|
-
stages=(backlog in-progress testing done)
|
|
303
|
-
|
|
304
363
|
fail=0
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
364
|
+
flow="shared-context/ticket-flow.json"
|
|
365
|
+
|
|
366
|
+
lane_from_rel() {
|
|
367
|
+
# expects work/<lane>/<file>.md
|
|
368
|
+
echo "$1" | sed -E 's#^work/([^/]+)/.*$##'
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
field_from_md() {
|
|
372
|
+
local file="$1"
|
|
373
|
+
local key="$2"
|
|
374
|
+
# Extract first matching header line like: Key: value
|
|
375
|
+
local line
|
|
376
|
+
line="$(grep -m1 -E "^${key}:[[:space:]]*" "$file" 2>/dev/null || true)"
|
|
377
|
+
echo "${line#${key}:}" | sed -E 's/^\s+//'
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
expected_lane_for_owner() {
|
|
381
|
+
local owner="$1"
|
|
382
|
+
local currentLane="$2"
|
|
383
|
+
|
|
384
|
+
# If jq or the mapping file isn't available, do not block progress.
|
|
385
|
+
if [[ ! -f "$flow" ]]; then
|
|
386
|
+
echo "$currentLane"
|
|
387
|
+
return 0
|
|
313
388
|
fi
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if [[ "$bn" == *"pr-body"* ]]; then
|
|
318
|
-
for stage in "${stages[@]}"; do
|
|
319
|
-
cand="work/$stage/${num}-pr-body.md"
|
|
320
|
-
if [[ -f "$cand" ]]; then
|
|
321
|
-
canonical="$cand"
|
|
322
|
-
break
|
|
323
|
-
fi
|
|
324
|
-
done
|
|
389
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
390
|
+
echo "$currentLane"
|
|
391
|
+
return 0
|
|
325
392
|
fi
|
|
326
393
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
fi
|
|
334
|
-
done
|
|
394
|
+
local out
|
|
395
|
+
out="$(jq -r --arg o "$owner" '.laneByOwner[$o] // .defaultLane // empty' "$flow" 2>/dev/null || true)"
|
|
396
|
+
if [[ -n "$out" ]]; then
|
|
397
|
+
echo "$out"
|
|
398
|
+
else
|
|
399
|
+
echo "$currentLane"
|
|
335
400
|
fi
|
|
401
|
+
}
|
|
336
402
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
fi
|
|
403
|
+
check_ticket() {
|
|
404
|
+
local file="$1"
|
|
405
|
+
local rel="$file"
|
|
406
|
+
rel="${rel#./}"
|
|
342
407
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
408
|
+
local lane
|
|
409
|
+
lane="$(lane_from_rel "$rel")"
|
|
410
|
+
|
|
411
|
+
# Ignore done lane for owner/status enforcement.
|
|
412
|
+
if [[ "$lane" == "done" ]]; then
|
|
413
|
+
return 0
|
|
348
414
|
fi
|
|
349
415
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
416
|
+
local owner status
|
|
417
|
+
owner="$(field_from_md "$file" "Owner")"
|
|
418
|
+
status="$(field_from_md "$file" "Status")"
|
|
419
|
+
|
|
420
|
+
if [[ -n "$status" && "$status" != "$lane" ]]; then
|
|
421
|
+
echo "[FAIL] $rel: Status mismatch (has: $status, lane: $lane)" >&2
|
|
354
422
|
fail=1
|
|
355
|
-
continue
|
|
356
423
|
fi
|
|
357
424
|
|
|
358
|
-
if [[
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
425
|
+
if [[ -n "$owner" ]]; then
|
|
426
|
+
local expected
|
|
427
|
+
expected="$(expected_lane_for_owner "$owner" "$lane")"
|
|
428
|
+
if [[ -n "$expected" && "$expected" != "$lane" ]]; then
|
|
429
|
+
echo "[FAIL] $rel: Owner '$owner' expects lane '$expected' per $flow (currently in '$lane')" >&2
|
|
430
|
+
fail=1
|
|
431
|
+
fi
|
|
362
432
|
fi
|
|
433
|
+
}
|
|
363
434
|
|
|
435
|
+
shopt -s nullglob
|
|
436
|
+
for file in work/backlog/*.md work/in-progress/*.md work/testing/*.md work/done/*.md; do
|
|
437
|
+
[[ -f "$file" ]] || continue
|
|
438
|
+
check_ticket "$file"
|
|
364
439
|
done
|
|
365
440
|
|
|
366
441
|
if [[ "$fail" -ne 0 ]]; then
|
|
@@ -402,73 +477,94 @@ templates:
|
|
|
402
477
|
set -euo pipefail
|
|
403
478
|
|
|
404
479
|
# ticket-hygiene.sh
|
|
405
|
-
# Guardrail script used by lead triage
|
|
406
|
-
#
|
|
480
|
+
# Guardrail script used by lead triage + execution loops.
|
|
481
|
+
# Assignment stubs are deprecated.
|
|
482
|
+
#
|
|
483
|
+
# Checks (ACTIVE lanes only):
|
|
484
|
+
# - Ticket file location (lane) must match Status:
|
|
485
|
+
# - Ticket Owner should be in the expected lane per shared-context/ticket-flow.json (best-effort)
|
|
486
|
+
#
|
|
487
|
+
# Notes:
|
|
488
|
+
# - We intentionally do NOT enforce mapping for work/done/ because historical tickets may have old Owner/Status.
|
|
407
489
|
|
|
408
490
|
cd "$(dirname "$0")/.."
|
|
409
491
|
|
|
410
|
-
stages=(backlog in-progress testing done)
|
|
411
|
-
|
|
412
492
|
fail=0
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
493
|
+
flow="shared-context/ticket-flow.json"
|
|
494
|
+
|
|
495
|
+
lane_from_rel() {
|
|
496
|
+
# expects work/<lane>/<file>.md
|
|
497
|
+
echo "$1" | sed -E 's#^work/([^/]+)/.*$##'
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
field_from_md() {
|
|
501
|
+
local file="$1"
|
|
502
|
+
local key="$2"
|
|
503
|
+
# Extract first matching header line like: Key: value
|
|
504
|
+
local line
|
|
505
|
+
line="$(grep -m1 -E "^${key}:[[:space:]]*" "$file" 2>/dev/null || true)"
|
|
506
|
+
echo "${line#${key}:}" | sed -E 's/^\s+//'
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
expected_lane_for_owner() {
|
|
510
|
+
local owner="$1"
|
|
511
|
+
local currentLane="$2"
|
|
512
|
+
|
|
513
|
+
# If jq or the mapping file isn't available, do not block progress.
|
|
514
|
+
if [[ ! -f "$flow" ]]; then
|
|
515
|
+
echo "$currentLane"
|
|
516
|
+
return 0
|
|
421
517
|
fi
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
if [[ "$bn" == *"pr-body"* ]]; then
|
|
426
|
-
for stage in "${stages[@]}"; do
|
|
427
|
-
cand="work/$stage/${num}-pr-body.md"
|
|
428
|
-
if [[ -f "$cand" ]]; then
|
|
429
|
-
canonical="$cand"
|
|
430
|
-
break
|
|
431
|
-
fi
|
|
432
|
-
done
|
|
518
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
519
|
+
echo "$currentLane"
|
|
520
|
+
return 0
|
|
433
521
|
fi
|
|
434
522
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
fi
|
|
442
|
-
done
|
|
523
|
+
local out
|
|
524
|
+
out="$(jq -r --arg o "$owner" '.laneByOwner[$o] // .defaultLane // empty' "$flow" 2>/dev/null || true)"
|
|
525
|
+
if [[ -n "$out" ]]; then
|
|
526
|
+
echo "$out"
|
|
527
|
+
else
|
|
528
|
+
echo "$currentLane"
|
|
443
529
|
fi
|
|
530
|
+
}
|
|
444
531
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
fi
|
|
532
|
+
check_ticket() {
|
|
533
|
+
local file="$1"
|
|
534
|
+
local rel="$file"
|
|
535
|
+
rel="${rel#./}"
|
|
450
536
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
537
|
+
local lane
|
|
538
|
+
lane="$(lane_from_rel "$rel")"
|
|
539
|
+
|
|
540
|
+
# Ignore done lane for owner/status enforcement.
|
|
541
|
+
if [[ "$lane" == "done" ]]; then
|
|
542
|
+
return 0
|
|
456
543
|
fi
|
|
457
544
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
545
|
+
local owner status
|
|
546
|
+
owner="$(field_from_md "$file" "Owner")"
|
|
547
|
+
status="$(field_from_md "$file" "Status")"
|
|
548
|
+
|
|
549
|
+
if [[ -n "$status" && "$status" != "$lane" ]]; then
|
|
550
|
+
echo "[FAIL] $rel: Status mismatch (has: $status, lane: $lane)" >&2
|
|
462
551
|
fail=1
|
|
463
|
-
continue
|
|
464
552
|
fi
|
|
465
553
|
|
|
466
|
-
if [[
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
554
|
+
if [[ -n "$owner" ]]; then
|
|
555
|
+
local expected
|
|
556
|
+
expected="$(expected_lane_for_owner "$owner" "$lane")"
|
|
557
|
+
if [[ -n "$expected" && "$expected" != "$lane" ]]; then
|
|
558
|
+
echo "[FAIL] $rel: Owner '$owner' expects lane '$expected' per $flow (currently in '$lane')" >&2
|
|
559
|
+
fail=1
|
|
560
|
+
fi
|
|
470
561
|
fi
|
|
562
|
+
}
|
|
471
563
|
|
|
564
|
+
shopt -s nullglob
|
|
565
|
+
for file in work/backlog/*.md work/in-progress/*.md work/testing/*.md work/done/*.md; do
|
|
566
|
+
[[ -f "$file" ]] || continue
|
|
567
|
+
check_ticket "$file"
|
|
472
568
|
done
|
|
473
569
|
|
|
474
570
|
if [[ "$fail" -ne 0 ]]; then
|
|
@@ -831,6 +927,10 @@ files:
|
|
|
831
927
|
- path: NOTES.md
|
|
832
928
|
template: notes
|
|
833
929
|
mode: createOnly
|
|
930
|
+
- path: shared-context/ticket-flow.json
|
|
931
|
+
template: sharedContext.ticketFlow
|
|
932
|
+
mode: createOnly
|
|
933
|
+
|
|
834
934
|
|
|
835
935
|
# Automation / hygiene scripts
|
|
836
936
|
# NOTE: portable policy: we do NOT chmod automatically. After scaffold:
|
package/src/handlers/tickets.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { findTicketFile, handoffTicket, takeTicket } from "../lib/ticket-workflo
|
|
|
6
6
|
import { ticketStageDir } from "../lib/lanes";
|
|
7
7
|
import { computeNextTicketNumber, TICKET_FILENAME_REGEX } from "../lib/ticket-finder";
|
|
8
8
|
import { resolveTeamContext } from "../lib/workspace";
|
|
9
|
-
import {
|
|
9
|
+
import { VALID_ROLES, VALID_STAGES } from "../lib/constants";
|
|
10
10
|
|
|
11
11
|
export function patchTicketField(md: string, key: string, value: string): string {
|
|
12
12
|
const lineRe = new RegExp(`^${key}:\\s.*$`, "m");
|
|
@@ -108,40 +108,20 @@ export async function handleMoveTicket(
|
|
|
108
108
|
await fs.writeFile(srcPath, patched, "utf8");
|
|
109
109
|
if (srcPath !== destPath) await fs.rename(srcPath, destPath);
|
|
110
110
|
|
|
111
|
-
//
|
|
112
|
-
if (dest === "done") {
|
|
113
|
-
const filename = path.basename(destPath);
|
|
114
|
-
const m = filename.match(TICKET_FILENAME_REGEX);
|
|
115
|
-
const ticketNumStr = m?.[1] ?? null;
|
|
116
|
-
if (ticketNumStr) {
|
|
117
|
-
const assignmentsDir = ticketStageDir(teamDir, "assignments");
|
|
118
|
-
if (await fileExists(assignmentsDir)) {
|
|
119
|
-
const files = (await fs.readdir(assignmentsDir)).filter((f) => f.startsWith(`${ticketNumStr}-assigned-`) && f.endsWith(".md"));
|
|
120
|
-
if (files.length) {
|
|
121
|
-
const archiveDir = path.join(assignmentsDir, "archive");
|
|
122
|
-
await ensureDir(archiveDir);
|
|
123
|
-
for (const f of files) {
|
|
124
|
-
const from = path.join(assignmentsDir, f);
|
|
125
|
-
const to = path.join(archiveDir, f);
|
|
126
|
-
await fs.rename(from, to);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
111
|
+
// Assignment stubs are deprecated; no archival behavior.
|
|
132
112
|
|
|
133
113
|
return { ok: true, from: srcPath, to: destPath };
|
|
134
114
|
}
|
|
135
115
|
|
|
136
116
|
/**
|
|
137
|
-
* Assign a ticket to an owner (
|
|
117
|
+
* Assign a ticket to an owner (updates Owner: only; assignment stubs are deprecated).
|
|
138
118
|
* @param api - OpenClaw plugin API
|
|
139
|
-
* @param options - teamId, ticket, owner, optional
|
|
119
|
+
* @param options - teamId, ticket, owner, optional dryRun
|
|
140
120
|
* @returns Plan (dryRun) or ok with plan
|
|
141
121
|
*/
|
|
142
122
|
export async function handleAssign(
|
|
143
123
|
api: OpenClawPluginApi,
|
|
144
|
-
options: { teamId: string; ticket: string; owner: string;
|
|
124
|
+
options: { teamId: string; ticket: string; owner: string; dryRun?: boolean },
|
|
145
125
|
) {
|
|
146
126
|
const teamId = String(options.teamId);
|
|
147
127
|
const { teamDir } = await resolveTeamContext(api, teamId);
|
|
@@ -151,14 +131,8 @@ export async function handleAssign(
|
|
|
151
131
|
}
|
|
152
132
|
const ticketPath = await findTicketFile(teamDir, options.ticket);
|
|
153
133
|
if (!ticketPath) throw new Error(`Ticket not found: ${options.ticket}`);
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
const ticketNumStr = m?.[1] ?? DEFAULT_TICKET_NUMBER;
|
|
157
|
-
const slug = m?.[2] ?? (options.ticket.replace(/^\d{4}-?/, "") || "ticket");
|
|
158
|
-
const assignmentsDir = ticketStageDir(teamDir, "assignments");
|
|
159
|
-
await ensureDir(assignmentsDir);
|
|
160
|
-
const assignmentPath = path.join(assignmentsDir, `${ticketNumStr}-assigned-${owner}.md`);
|
|
161
|
-
const plan = { ticketPath, assignmentPath, owner };
|
|
134
|
+
// Previously parsed for assignment-stub ids; assignment stubs are deprecated.
|
|
135
|
+
const plan = { ticketPath, owner };
|
|
162
136
|
if (options.dryRun) return { ok: true, plan };
|
|
163
137
|
const patchOwner = (md: string) => {
|
|
164
138
|
if (md.match(/^Owner:\s.*$/m)) return md.replace(/^Owner:\s.*$/m, `Owner: ${owner}`);
|
|
@@ -167,8 +141,7 @@ export async function handleAssign(
|
|
|
167
141
|
const md = await fs.readFile(ticketPath, "utf8");
|
|
168
142
|
const nextMd = patchOwner(md);
|
|
169
143
|
await fs.writeFile(ticketPath, nextMd, "utf8");
|
|
170
|
-
|
|
171
|
-
await writeFileSafely(assignmentPath, assignmentMd, options.overwrite ? "overwrite" : "createOnly");
|
|
144
|
+
// Assignment stubs are deprecated; do not create/update work/assignments/*.md.
|
|
172
145
|
return { ok: true, plan };
|
|
173
146
|
}
|
|
174
147
|
|
|
@@ -269,7 +242,8 @@ export async function handleDispatch(
|
|
|
269
242
|
if (!requestText) throw new Error("Request cannot be empty");
|
|
270
243
|
const inboxDir = path.join(teamDir, "inbox");
|
|
271
244
|
const backlogDir = ticketStageDir(teamDir, "backlog");
|
|
272
|
-
|
|
245
|
+
// Assignment stubs are deprecated; do not create work/assignments/*.
|
|
246
|
+
// Keep old stubs if they exist (migration will move them to work/assignments.__deprecated__/).
|
|
273
247
|
const slugify = (s: string) =>
|
|
274
248
|
s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "").slice(0, 60) || "request";
|
|
275
249
|
const nowKey = () => {
|
|
@@ -283,27 +257,22 @@ export async function handleDispatch(
|
|
|
283
257
|
const baseSlug = slugify(title);
|
|
284
258
|
const inboxPath = path.join(inboxDir, `${nowKey()}-${baseSlug}.md`);
|
|
285
259
|
const ticketPath = path.join(backlogDir, `${ticketNumStr}-${baseSlug}.md`);
|
|
286
|
-
const assignmentPath = path.join(assignmentsDir, `${ticketNumStr}-assigned-${owner}.md`);
|
|
287
260
|
const receivedIso = new Date().toISOString();
|
|
288
|
-
const inboxMd = `# Inbox — ${teamId}\n\nReceived: ${receivedIso}\n\n## Request\n${requestText}\n\n## Proposed work\n- Ticket: ${ticketNumStr}-${baseSlug}\n- Owner: ${owner}\n\n## Links\n- Ticket: ${path.relative(teamDir, ticketPath)}\n
|
|
289
|
-
const ticketMd = `# ${ticketNumStr}-${baseSlug}\n\nCreated: ${receivedIso}\nOwner: ${owner}\nStatus: queued\nInbox: ${path.relative(teamDir, inboxPath)}\
|
|
290
|
-
const assignmentMd = `# Assignment — ${ticketNumStr}-${baseSlug}\n\nCreated: ${receivedIso}\nAssigned: ${owner}\n\n## Goal\n${title}\n\n## Ticket\n${path.relative(teamDir, ticketPath)}\n\n## Notes\n- Created by: openclaw recipes dispatch\n`;
|
|
261
|
+
const inboxMd = `# Inbox — ${teamId}\n\nReceived: ${receivedIso}\n\n## Request\n${requestText}\n\n## Proposed work\n- Ticket: ${ticketNumStr}-${baseSlug}\n- Owner: ${owner}\n\n## Links\n- Ticket: ${path.relative(teamDir, ticketPath)}\n`;
|
|
262
|
+
const ticketMd = `# ${ticketNumStr}-${baseSlug}\n\nCreated: ${receivedIso}\nOwner: ${owner}\nStatus: queued\nInbox: ${path.relative(teamDir, inboxPath)}\n\n## Context\n${requestText}\n\n## Requirements\n- (fill in)\n\n## Acceptance criteria\n- (fill in)\n\n## Tasks\n- [ ] (fill in)\n\n## Comments\n- (use this section for @mentions, questions, decisions, and dated replies)\n`;
|
|
291
263
|
const plan = {
|
|
292
264
|
teamId,
|
|
293
265
|
request: requestText,
|
|
294
266
|
files: [
|
|
295
267
|
{ path: inboxPath, kind: "inbox", summary: title },
|
|
296
268
|
{ path: ticketPath, kind: "backlog-ticket", summary: title },
|
|
297
|
-
{ path: assignmentPath, kind: "assignment", summary: owner },
|
|
298
269
|
],
|
|
299
270
|
};
|
|
300
271
|
if (options.dryRun) return { ok: true as const, plan };
|
|
301
272
|
await ensureDir(inboxDir);
|
|
302
273
|
await ensureDir(backlogDir);
|
|
303
|
-
await ensureDir(assignmentsDir);
|
|
304
274
|
await writeFileSafely(inboxPath, inboxMd, "createOnly");
|
|
305
275
|
await writeFileSafely(ticketPath, ticketMd, "createOnly");
|
|
306
|
-
await writeFileSafely(assignmentPath, assignmentMd, "createOnly");
|
|
307
276
|
let nudgeQueued = false;
|
|
308
277
|
try {
|
|
309
278
|
const leadAgentId = `${teamId}-lead`;
|
|
@@ -312,7 +281,8 @@ export async function handleDispatch(
|
|
|
312
281
|
`Dispatch created new intake for team: ${teamId}`,
|
|
313
282
|
`- Inbox: ${path.relative(teamDir, inboxPath)}`,
|
|
314
283
|
`- Backlog: ${path.relative(teamDir, ticketPath)}`,
|
|
315
|
-
|
|
284
|
+
// Assignment stubs are deprecated; no assignment artifact is created.
|
|
285
|
+
|
|
316
286
|
`Action: please triage/normalize the ticket (fill Requirements/AC/tasks) and move it through the workflow.`,
|
|
317
287
|
].join("\n"),
|
|
318
288
|
{ sessionKey: `agent:${leadAgentId}:main` },
|
|
@@ -326,58 +296,6 @@ export async function handleDispatch(
|
|
|
326
296
|
return { ok: true as const, wrote: plan.files.map((f) => f.path), nudgeQueued };
|
|
327
297
|
}
|
|
328
298
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
* Why: some automation/board views treat assignment stubs as active work signals.
|
|
333
|
-
* If a ticket is manually moved to done (outside `openclaw recipes move-ticket`),
|
|
334
|
-
* its `work/assignments/<num>-assigned-*.md` stubs may linger and resurface the ticket.
|
|
335
|
-
*
|
|
336
|
-
* This command archives any matching assignment stubs into `work/assignments/archive/`.
|
|
337
|
-
*/
|
|
338
|
-
export async function handleCleanupClosedAssignments(
|
|
339
|
-
api: OpenClawPluginApi,
|
|
340
|
-
options: { teamId: string; ticketNums?: string[] }
|
|
341
|
-
): Promise<{ ok: true; teamId: string; archived: Array<{ from: string; to: string }> }> {
|
|
342
|
-
const teamId = String(options.teamId);
|
|
343
|
-
const { teamDir } = await resolveTeamContext(api, teamId);
|
|
344
|
-
|
|
345
|
-
const assignmentsDir = ticketStageDir(teamDir, "assignments");
|
|
346
|
-
const archiveDir = path.join(assignmentsDir, "archive");
|
|
347
|
-
const doneDir = ticketStageDir(teamDir, "done");
|
|
348
|
-
|
|
349
|
-
const archived: Array<{ from: string; to: string }> = [];
|
|
350
|
-
if (!(await fileExists(assignmentsDir))) return { ok: true, teamId, archived };
|
|
351
|
-
await ensureDir(archiveDir);
|
|
352
|
-
|
|
353
|
-
const ticketNumsFilter = Array.isArray(options.ticketNums) && options.ticketNums.length
|
|
354
|
-
? new Set(options.ticketNums.map((n) => String(n).padStart(4, "0")))
|
|
355
|
-
: null;
|
|
356
|
-
|
|
357
|
-
const doneFiles = (await fileExists(doneDir)) ? await fs.readdir(doneDir) : [];
|
|
358
|
-
const doneNums = new Set(
|
|
359
|
-
doneFiles
|
|
360
|
-
.map((f) => f.match(/^([0-9]{4})-/)?.[1])
|
|
361
|
-
.filter((x): x is string => !!x)
|
|
362
|
-
);
|
|
363
|
-
|
|
364
|
-
const files = (await fs.readdir(assignmentsDir)).filter((f) => f.endsWith(".md"));
|
|
365
|
-
for (const f of files) {
|
|
366
|
-
if (f === "archive") continue;
|
|
367
|
-
if (f.startsWith("archive" + path.sep)) continue;
|
|
368
|
-
const m = f.match(/^([0-9]{4})-assigned-.*\.md$/);
|
|
369
|
-
if (!m) continue;
|
|
370
|
-
const num = m[1];
|
|
371
|
-
if (ticketNumsFilter && !ticketNumsFilter.has(num)) continue;
|
|
372
|
-
|
|
373
|
-
// If the ticket number is present in done/, this assignment is considered closed.
|
|
374
|
-
if (!doneNums.has(num)) continue;
|
|
375
|
-
|
|
376
|
-
const from = path.join(assignmentsDir, f);
|
|
377
|
-
const to = path.join(archiveDir, f);
|
|
378
|
-
await fs.rename(from, to);
|
|
379
|
-
archived.push({ from, to });
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
return { ok: true, teamId, archived };
|
|
383
|
-
}
|
|
299
|
+
// Assignment stubs are deprecated and preserved only via migration to work/assignments.__deprecated__/.
|
|
300
|
+
// This handler is intentionally removed to avoid continuing stub semantics.
|
|
301
|
+
// (If you need to clean up legacy stubs, do it via a one-time migration script.)
|
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import { fileExists } from "./fs-utils";
|
|
5
4
|
import { ensureLaneDir } from "./lanes";
|
|
6
|
-
import { DEFAULT_TICKET_NUMBER } from "./constants";
|
|
7
5
|
import { findTicketFile as findTicketFileFromFinder } from "./ticket-finder";
|
|
8
|
-
import { parseTicketFilename } from "./ticket-finder";
|
|
9
|
-
|
|
10
|
-
async function ensureDir(p: string) {
|
|
11
|
-
await fs.mkdir(p, { recursive: true });
|
|
12
|
-
}
|
|
13
6
|
|
|
14
7
|
function patchTicketFields(
|
|
15
8
|
md: string,
|
|
16
|
-
opts: { ownerSafe: string; status: string
|
|
9
|
+
opts: { ownerSafe: string; status: string }
|
|
17
10
|
): string {
|
|
18
11
|
let out = md;
|
|
19
12
|
if (out.match(/^Owner:\s.*$/m)) out = out.replace(/^Owner:\s.*$/m, `Owner: ${opts.ownerSafe}`);
|
|
@@ -22,9 +15,7 @@ function patchTicketFields(
|
|
|
22
15
|
if (out.match(/^Status:\s.*$/m)) out = out.replace(/^Status:\s.*$/m, `Status: ${opts.status}`);
|
|
23
16
|
else out = out.replace(/^(# .+\n)/, `$1\nStatus: ${opts.status}\n`);
|
|
24
17
|
|
|
25
|
-
|
|
26
|
-
else out = out.replace(/^Owner:.*$/m, (line) => `${line}\nAssignment: ${opts.assignmentRel}`);
|
|
27
|
-
|
|
18
|
+
// Assignment stubs are deprecated; do not write/update Assignment: here.
|
|
28
19
|
return out;
|
|
29
20
|
}
|
|
30
21
|
|
|
@@ -47,34 +38,20 @@ export async function takeTicket(opts: { teamDir: string; ticket: string; owner?
|
|
|
47
38
|
const filename = path.basename(srcPath);
|
|
48
39
|
const destPath = path.join(inProgressDir, filename);
|
|
49
40
|
|
|
50
|
-
|
|
51
|
-
const { ticketNumStr, slug } = parsed;
|
|
52
|
-
|
|
53
|
-
const assignmentsDir = path.join(teamDir, 'work', 'assignments');
|
|
54
|
-
await ensureDir(assignmentsDir);
|
|
55
|
-
const assignmentPath = path.join(assignmentsDir, `${ticketNumStr}-assigned-${ownerSafe}.md`);
|
|
56
|
-
const assignmentRel = path.relative(teamDir, assignmentPath);
|
|
41
|
+
// Previously parsed filename for assignment-stub ids; no longer needed.
|
|
57
42
|
|
|
58
43
|
const alreadyInProgress = srcPath === destPath;
|
|
59
44
|
|
|
60
45
|
const md = await fs.readFile(srcPath, 'utf8');
|
|
61
|
-
const nextMd = patchTicketFields(md, { ownerSafe, status: 'in-progress'
|
|
46
|
+
const nextMd = patchTicketFields(md, { ownerSafe, status: 'in-progress' });
|
|
62
47
|
await fs.writeFile(srcPath, nextMd, 'utf8');
|
|
63
48
|
|
|
64
49
|
if (!alreadyInProgress) {
|
|
65
50
|
await fs.rename(srcPath, destPath);
|
|
66
51
|
}
|
|
67
52
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const assignmentExists = await fileExists(assignmentPath);
|
|
71
|
-
if (assignmentExists && !opts.overwriteAssignment) {
|
|
72
|
-
// createOnly
|
|
73
|
-
} else {
|
|
74
|
-
await fs.writeFile(assignmentPath, assignmentMd, 'utf8');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return { srcPath, destPath, moved: !alreadyInProgress, assignmentPath };
|
|
53
|
+
// Assignment stubs are deprecated; do not create/update work/assignments/*.md.
|
|
54
|
+
return { srcPath, destPath, moved: !alreadyInProgress };
|
|
78
55
|
}
|
|
79
56
|
|
|
80
57
|
export async function handoffTicket(opts: { teamDir: string; ticket: string; tester?: string; overwriteAssignment: boolean }) {
|
|
@@ -91,32 +68,18 @@ export async function handoffTicket(opts: { teamDir: string; ticket: string; tes
|
|
|
91
68
|
const filename = path.basename(srcPath);
|
|
92
69
|
const destPath = path.join(testingDir, filename);
|
|
93
70
|
|
|
94
|
-
|
|
95
|
-
const { ticketNumStr, slug } = parsed;
|
|
96
|
-
|
|
97
|
-
const assignmentsDir = path.join(teamDir, 'work', 'assignments');
|
|
98
|
-
await ensureDir(assignmentsDir);
|
|
99
|
-
const assignmentPath = path.join(assignmentsDir, `${ticketNumStr}-assigned-${testerSafe}.md`);
|
|
100
|
-
const assignmentRel = path.relative(teamDir, assignmentPath);
|
|
71
|
+
// Previously parsed filename for assignment-stub ids; no longer needed.
|
|
101
72
|
|
|
102
73
|
const alreadyInTesting = srcPath === destPath;
|
|
103
74
|
|
|
104
75
|
const md = await fs.readFile(srcPath, 'utf8');
|
|
105
|
-
const nextMd = patchTicketFields(md, { ownerSafe: testerSafe, status: 'testing'
|
|
76
|
+
const nextMd = patchTicketFields(md, { ownerSafe: testerSafe, status: 'testing' });
|
|
106
77
|
await fs.writeFile(srcPath, nextMd, 'utf8');
|
|
107
78
|
|
|
108
79
|
if (!alreadyInTesting) {
|
|
109
80
|
await fs.rename(srcPath, destPath);
|
|
110
81
|
}
|
|
111
82
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const assignmentExists = await fileExists(assignmentPath);
|
|
115
|
-
if (assignmentExists && !opts.overwriteAssignment) {
|
|
116
|
-
// createOnly: leave as-is
|
|
117
|
-
} else {
|
|
118
|
-
await fs.writeFile(assignmentPath, assignmentMd, 'utf8');
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return { srcPath, destPath, moved: !alreadyInTesting, assignmentPath };
|
|
83
|
+
// Assignment stubs are deprecated; do not create/update work/assignments/*.md.
|
|
84
|
+
return { srcPath, destPath, moved: !alreadyInTesting };
|
|
122
85
|
}
|