@jonit-dev/night-watch-cli 1.8.10-beta.5 β 1.8.10-beta.6
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/dist/cli.js +22 -1
- package/dist/scripts/night-watch-cron.sh +299 -49
- package/dist/scripts/night-watch-helpers.sh +83 -0
- package/dist/scripts/night-watch-merger-cron.sh +7 -1
- package/dist/scripts/night-watch-pr-reviewer-cron.sh +5 -0
- package/dist/scripts/night-watch-qa-cron.sh +5 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2991,7 +2991,7 @@ function sortByPriority(issues) {
|
|
|
2991
2991
|
return aOrder - bOrder;
|
|
2992
2992
|
});
|
|
2993
2993
|
}
|
|
2994
|
-
var PRIORITY_LABELS, PRIORITY_LABEL_INFO, CATEGORY_LABELS, CATEGORY_LABEL_INFO, HORIZON_LABELS, HORIZON_LABEL_INFO, PRIORITY_COLORS, NIGHT_WATCH_LABELS;
|
|
2994
|
+
var PRIORITY_LABELS, PRIORITY_LABEL_INFO, CATEGORY_LABELS, CATEGORY_LABEL_INFO, HORIZON_LABELS, HORIZON_LABEL_INFO, EXECUTOR_PARTIAL_LABEL, EXECUTOR_RESUMABLE_LABEL, EXECUTOR_READY_REVIEW_LABEL, PRIORITY_COLORS, NIGHT_WATCH_LABELS;
|
|
2995
2995
|
var init_labels = __esm({
|
|
2996
2996
|
"../core/dist/board/labels.js"() {
|
|
2997
2997
|
"use strict";
|
|
@@ -3065,6 +3065,9 @@ var init_labels = __esm({
|
|
|
3065
3065
|
"medium-term": { name: "medium-term", description: "6 weeks - 4 months delivery window" },
|
|
3066
3066
|
"long-term": { name: "long-term", description: "4-12 months delivery window" }
|
|
3067
3067
|
};
|
|
3068
|
+
EXECUTOR_PARTIAL_LABEL = "nw:partial";
|
|
3069
|
+
EXECUTOR_RESUMABLE_LABEL = "nw:resumable";
|
|
3070
|
+
EXECUTOR_READY_REVIEW_LABEL = "nw:ready-review";
|
|
3068
3071
|
PRIORITY_COLORS = {
|
|
3069
3072
|
P0: "b60205",
|
|
3070
3073
|
P1: "d93f0b",
|
|
@@ -3099,6 +3102,21 @@ var init_labels = __esm({
|
|
|
3099
3102
|
name: "e2e-validated",
|
|
3100
3103
|
description: "PR acceptance requirements validated by e2e/integration tests",
|
|
3101
3104
|
color: "0e8a16"
|
|
3105
|
+
},
|
|
3106
|
+
{
|
|
3107
|
+
name: EXECUTOR_PARTIAL_LABEL,
|
|
3108
|
+
description: "Executor started this PR and work is intentionally incomplete",
|
|
3109
|
+
color: "fbca04"
|
|
3110
|
+
},
|
|
3111
|
+
{
|
|
3112
|
+
name: EXECUTOR_RESUMABLE_LABEL,
|
|
3113
|
+
description: "Executor should resume this unfinished PR before starting new work",
|
|
3114
|
+
color: "d93f0b"
|
|
3115
|
+
},
|
|
3116
|
+
{
|
|
3117
|
+
name: EXECUTOR_READY_REVIEW_LABEL,
|
|
3118
|
+
description: "Executor finished implementation and the PR is ready for automated/human review",
|
|
3119
|
+
color: "0e8a16"
|
|
3102
3120
|
}
|
|
3103
3121
|
];
|
|
3104
3122
|
}
|
|
@@ -8155,6 +8173,9 @@ __export(dist_exports, {
|
|
|
8155
8173
|
DEFAULT_TEMPLATES_DIR: () => DEFAULT_TEMPLATES_DIR,
|
|
8156
8174
|
EXECUTOR_LOG_FILE: () => EXECUTOR_LOG_FILE,
|
|
8157
8175
|
EXECUTOR_LOG_NAME: () => EXECUTOR_LOG_NAME,
|
|
8176
|
+
EXECUTOR_PARTIAL_LABEL: () => EXECUTOR_PARTIAL_LABEL,
|
|
8177
|
+
EXECUTOR_READY_REVIEW_LABEL: () => EXECUTOR_READY_REVIEW_LABEL,
|
|
8178
|
+
EXECUTOR_RESUMABLE_LABEL: () => EXECUTOR_RESUMABLE_LABEL,
|
|
8158
8179
|
GLOBAL_CONFIG_DIR: () => GLOBAL_CONFIG_DIR,
|
|
8159
8180
|
GLOBAL_NOTIFICATIONS_FILE_NAME: () => GLOBAL_NOTIFICATIONS_FILE_NAME,
|
|
8160
8181
|
HISTORY_FILE_NAME: () => HISTORY_FILE_NAME,
|
|
@@ -140,6 +140,12 @@ ISSUE_NUMBER="" # board mode: GitHub issue number
|
|
|
140
140
|
ISSUE_BODY="" # board mode: issue body (PRD content)
|
|
141
141
|
ISSUE_TITLE_RAW="" # board mode: issue title
|
|
142
142
|
NW_CLI="" # board mode: resolved night-watch CLI binary
|
|
143
|
+
EXECUTOR_PR_JSON=""
|
|
144
|
+
EXECUTOR_PR_NUMBER=""
|
|
145
|
+
EXECUTOR_PR_URL=""
|
|
146
|
+
EXECUTOR_PR_DRAFT=""
|
|
147
|
+
RESUME_FROM_EXISTING_PR=0
|
|
148
|
+
RESUME_BRANCH_NAME=""
|
|
143
149
|
|
|
144
150
|
restore_issue_to_ready() {
|
|
145
151
|
local reason="${1:-Execution failed before implementation started.}"
|
|
@@ -149,6 +155,26 @@ restore_issue_to_ready() {
|
|
|
149
155
|
fi
|
|
150
156
|
}
|
|
151
157
|
|
|
158
|
+
if [ -z "${NW_TARGET_ISSUE:-}" ]; then
|
|
159
|
+
EXECUTOR_PR_JSON=$(find_executor_resume_pr "${BRANCH_PREFIX}" || true)
|
|
160
|
+
if [ -n "${EXECUTOR_PR_JSON}" ]; then
|
|
161
|
+
RESUME_FROM_EXISTING_PR=1
|
|
162
|
+
RESUME_BRANCH_NAME=$(printf '%s' "${EXECUTOR_PR_JSON}" | jq -r '.headRefName // empty' 2>/dev/null || true)
|
|
163
|
+
EXECUTOR_PR_NUMBER=$(printf '%s' "${EXECUTOR_PR_JSON}" | jq -r '.number // empty' 2>/dev/null || true)
|
|
164
|
+
EXECUTOR_PR_URL=$(printf '%s' "${EXECUTOR_PR_JSON}" | jq -r '.url // empty' 2>/dev/null || true)
|
|
165
|
+
EXECUTOR_PR_DRAFT=$(printf '%s' "${EXECUTOR_PR_JSON}" | jq -r '.isDraft // false' 2>/dev/null || true)
|
|
166
|
+
if [ -n "${RESUME_BRANCH_NAME}" ]; then
|
|
167
|
+
log "RESUME: Prioritizing resumable PR #${EXECUTOR_PR_NUMBER:-unknown} on ${RESUME_BRANCH_NAME}"
|
|
168
|
+
else
|
|
169
|
+
RESUME_FROM_EXISTING_PR=0
|
|
170
|
+
EXECUTOR_PR_JSON=""
|
|
171
|
+
EXECUTOR_PR_NUMBER=""
|
|
172
|
+
EXECUTOR_PR_URL=""
|
|
173
|
+
EXECUTOR_PR_DRAFT=""
|
|
174
|
+
fi
|
|
175
|
+
fi
|
|
176
|
+
fi
|
|
177
|
+
|
|
152
178
|
if [ "${NW_BOARD_ENABLED:-}" = "true" ]; then
|
|
153
179
|
# Board mode: discover next task from GitHub Projects board
|
|
154
180
|
NW_CLI=$(resolve_night_watch_cli 2>/dev/null || true)
|
|
@@ -169,35 +195,51 @@ if [ "${NW_BOARD_ENABLED:-}" = "true" ]; then
|
|
|
169
195
|
ISSUE_TITLE_RAW=$(printf '%s' "${ISSUE_JSON}" | jq -r '.title // empty' 2>/dev/null || true)
|
|
170
196
|
ISSUE_BODY=$(printf '%s' "${ISSUE_JSON}" | jq -r '.body // empty' 2>/dev/null || true)
|
|
171
197
|
else
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
198
|
+
if [ "${RESUME_FROM_EXISTING_PR}" = "1" ] && [ -n "${RESUME_BRANCH_NAME}" ]; then
|
|
199
|
+
ELIGIBLE_PRD="${RESUME_BRANCH_NAME#*/}"
|
|
200
|
+
ISSUE_NUMBER=$(printf '%s' "${ELIGIBLE_PRD}" | grep -oE '^[0-9]+' || true)
|
|
201
|
+
if [ -n "${ISSUE_NUMBER}" ]; then
|
|
202
|
+
ISSUE_JSON=$(gh issue view "${ISSUE_NUMBER}" --json number,title,body 2>/dev/null || true)
|
|
203
|
+
ISSUE_TITLE_RAW=$(printf '%s' "${ISSUE_JSON}" | jq -r '.title // empty' 2>/dev/null || true)
|
|
204
|
+
ISSUE_BODY=$(printf '%s' "${ISSUE_JSON}" | jq -r '.body // empty' 2>/dev/null || true)
|
|
205
|
+
"${NW_CLI}" board move-issue "${ISSUE_NUMBER}" --column "In Progress" 2>>"${LOG_FILE}" || \
|
|
206
|
+
log "WARN: Failed to move resumed issue #${ISSUE_NUMBER} to In Progress"
|
|
181
207
|
else
|
|
182
|
-
|
|
208
|
+
ISSUE_TITLE_RAW=$(printf '%s' "${EXECUTOR_PR_JSON}" | jq -r '.title // empty' 2>/dev/null || true)
|
|
183
209
|
fi
|
|
184
210
|
else
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
211
|
+
BOARD_DISCOVERY_STATUS=0
|
|
212
|
+
if ISSUE_JSON=$(find_eligible_board_issue "${PROJECT_DIR}" "${MAX_RUNTIME}"); then
|
|
213
|
+
BOARD_DISCOVERY_STATUS=0
|
|
214
|
+
else
|
|
215
|
+
BOARD_DISCOVERY_STATUS=$?
|
|
216
|
+
fi
|
|
217
|
+
if [ -z "${ISSUE_JSON}" ]; then
|
|
218
|
+
if [ "${BOARD_DISCOVERY_STATUS}" -eq 2 ]; then
|
|
219
|
+
log "INFO: Ready board issues were found, but all are in cooldown; skipping this run"
|
|
220
|
+
else
|
|
221
|
+
log "INFO: No Ready board issues found; skipping this run"
|
|
222
|
+
fi
|
|
223
|
+
else
|
|
224
|
+
ISSUE_NUMBER=$(printf '%s' "${ISSUE_JSON}" | jq -r '.number // empty' 2>/dev/null || true)
|
|
225
|
+
ISSUE_TITLE_RAW=$(printf '%s' "${ISSUE_JSON}" | jq -r '.title // empty' 2>/dev/null || true)
|
|
226
|
+
ISSUE_BODY=$(printf '%s' "${ISSUE_JSON}" | jq -r '.body // empty' 2>/dev/null || true)
|
|
227
|
+
if [ -z "${ISSUE_NUMBER}" ]; then
|
|
228
|
+
log "ERROR: Board mode: failed to parse issue number from JSON"
|
|
229
|
+
exit 1
|
|
230
|
+
fi
|
|
231
|
+
# Move issue to In Progress (claim it on the board)
|
|
232
|
+
"${NW_CLI}" board move-issue "${ISSUE_NUMBER}" --column "In Progress" 2>>"${LOG_FILE}" || \
|
|
233
|
+
log "WARN: Failed to move issue #${ISSUE_NUMBER} to In Progress"
|
|
191
234
|
fi
|
|
192
|
-
# Move issue to In Progress (claim it on the board)
|
|
193
|
-
"${NW_CLI}" board move-issue "${ISSUE_NUMBER}" --column "In Progress" 2>>"${LOG_FILE}" || \
|
|
194
|
-
log "WARN: Failed to move issue #${ISSUE_NUMBER} to In Progress"
|
|
195
235
|
fi
|
|
196
236
|
fi
|
|
197
237
|
|
|
198
238
|
if [ -n "${ISSUE_NUMBER}" ]; then
|
|
199
|
-
# Slugify title for branch naming
|
|
200
|
-
|
|
239
|
+
# Slugify title for branch naming unless we're resuming an existing PR branch.
|
|
240
|
+
if [ "${RESUME_FROM_EXISTING_PR}" != "1" ] || [ -z "${ELIGIBLE_PRD:-}" ]; then
|
|
241
|
+
ELIGIBLE_PRD="${ISSUE_NUMBER}-$(printf '%s' "${ISSUE_TITLE_RAW}" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-\|-$//g')"
|
|
242
|
+
fi
|
|
201
243
|
log "BOARD: Processing issue #${ISSUE_NUMBER}: ${ISSUE_TITLE_RAW}"
|
|
202
244
|
fi
|
|
203
245
|
fi
|
|
@@ -208,8 +250,25 @@ if [ -z "${ISSUE_NUMBER}" ]; then
|
|
|
208
250
|
emit_result "skip_no_eligible_prd"
|
|
209
251
|
exit 0
|
|
210
252
|
fi
|
|
253
|
+
if [ "${RESUME_FROM_EXISTING_PR}" = "1" ] && [ -n "${RESUME_BRANCH_NAME}" ]; then
|
|
254
|
+
RESUME_PRD_NAME="${RESUME_BRANCH_NAME#*/}"
|
|
255
|
+
if [ -f "${PRD_DIR}/${RESUME_PRD_NAME}.md" ]; then
|
|
256
|
+
ELIGIBLE_PRD="${RESUME_PRD_NAME}.md"
|
|
257
|
+
log "RESUME: Using resumable filesystem PRD ${ELIGIBLE_PRD}"
|
|
258
|
+
else
|
|
259
|
+
log "WARN: Resumable PR branch ${RESUME_BRANCH_NAME} has no matching PRD file in ${PRD_DIR}; falling back to normal selection"
|
|
260
|
+
RESUME_FROM_EXISTING_PR=0
|
|
261
|
+
EXECUTOR_PR_JSON=""
|
|
262
|
+
EXECUTOR_PR_NUMBER=""
|
|
263
|
+
EXECUTOR_PR_URL=""
|
|
264
|
+
EXECUTOR_PR_DRAFT=""
|
|
265
|
+
RESUME_BRANCH_NAME=""
|
|
266
|
+
fi
|
|
267
|
+
fi
|
|
211
268
|
# Filesystem mode: scan PRD directory
|
|
212
|
-
|
|
269
|
+
if [ -z "${ELIGIBLE_PRD:-}" ]; then
|
|
270
|
+
ELIGIBLE_PRD=$(find_eligible_prd "${PRD_DIR}" "${MAX_RUNTIME}" "${PROJECT_DIR}")
|
|
271
|
+
fi
|
|
213
272
|
if [ -z "${ELIGIBLE_PRD}" ]; then
|
|
214
273
|
log "SKIP: No eligible PRDs (all done, in-progress, or blocked)"
|
|
215
274
|
emit_result "skip_no_eligible_prd"
|
|
@@ -221,7 +280,11 @@ if [ -z "${ISSUE_NUMBER}" ]; then
|
|
|
221
280
|
fi
|
|
222
281
|
|
|
223
282
|
PRD_NAME="${ELIGIBLE_PRD%.md}"
|
|
224
|
-
|
|
283
|
+
if [ -n "${RESUME_BRANCH_NAME}" ]; then
|
|
284
|
+
BRANCH_NAME="${RESUME_BRANCH_NAME}"
|
|
285
|
+
else
|
|
286
|
+
BRANCH_NAME="${BRANCH_PREFIX}/${PRD_NAME}"
|
|
287
|
+
fi
|
|
225
288
|
WORKTREE_DIR="$(dirname "${PROJECT_DIR}")/${PROJECT_NAME}-nw-${PRD_NAME}"
|
|
226
289
|
BOOKKEEP_WORKTREE_DIR="$(dirname "${PROJECT_DIR}")/${PROJECT_NAME}-nw-bookkeeping"
|
|
227
290
|
if [ -n "${NW_DEFAULT_BRANCH:-}" ]; then
|
|
@@ -248,6 +311,177 @@ count_prs_for_branch() {
|
|
|
248
311
|
echo "${count:-0}"
|
|
249
312
|
}
|
|
250
313
|
|
|
314
|
+
extract_prd_title() {
|
|
315
|
+
local prd_path=""
|
|
316
|
+
local title=""
|
|
317
|
+
|
|
318
|
+
if [ -n "${ISSUE_TITLE_RAW}" ]; then
|
|
319
|
+
printf '%s' "${ISSUE_TITLE_RAW}"
|
|
320
|
+
return 0
|
|
321
|
+
fi
|
|
322
|
+
|
|
323
|
+
if [ -f "${PRD_DIR}/${ELIGIBLE_PRD}" ]; then
|
|
324
|
+
prd_path="${PRD_DIR}/${ELIGIBLE_PRD}"
|
|
325
|
+
elif [ -f "${PROJECT_DIR}/${PRD_DIR_REL}/${ELIGIBLE_PRD}" ]; then
|
|
326
|
+
prd_path="${PROJECT_DIR}/${PRD_DIR_REL}/${ELIGIBLE_PRD}"
|
|
327
|
+
fi
|
|
328
|
+
|
|
329
|
+
if [ -n "${prd_path}" ]; then
|
|
330
|
+
title=$(awk '/^[[:space:]]*#[[:space:]]+/ { sub(/^[[:space:]]*#[[:space:]]+/, "", $0); print; exit }' "${prd_path}" 2>/dev/null || true)
|
|
331
|
+
fi
|
|
332
|
+
|
|
333
|
+
if [ -n "${title}" ]; then
|
|
334
|
+
printf '%s' "${title}"
|
|
335
|
+
else
|
|
336
|
+
printf '%s' "${PRD_NAME}"
|
|
337
|
+
fi
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
build_executor_pr_title() {
|
|
341
|
+
local raw_title=""
|
|
342
|
+
|
|
343
|
+
raw_title=$(extract_prd_title)
|
|
344
|
+
raw_title=$(printf '%s' "${raw_title}" | tr '\r\n' ' ' | sed -E 's/[[:space:]]+/ /g; s/^[[:space:]]+//; s/[[:space:]]+$//')
|
|
345
|
+
if [ -z "${raw_title}" ]; then
|
|
346
|
+
raw_title="${PRD_NAME}"
|
|
347
|
+
fi
|
|
348
|
+
|
|
349
|
+
if printf '%s' "${raw_title}" | grep -Eqi '^feat:'; then
|
|
350
|
+
printf '%s' "${raw_title}"
|
|
351
|
+
else
|
|
352
|
+
printf 'feat: %s' "${raw_title}"
|
|
353
|
+
fi
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
build_executor_pr_body() {
|
|
357
|
+
local status_blurb=""
|
|
358
|
+
|
|
359
|
+
status_blurb="Status labels:
|
|
360
|
+
- ${NW_EXECUTOR_PARTIAL_LABEL}: implementation is in progress and intentionally incomplete
|
|
361
|
+
- ${NW_EXECUTOR_RESUMABLE_LABEL}: resume this PR before starting new work
|
|
362
|
+
- ${NW_EXECUTOR_READY_REVIEW_LABEL}: implementation is complete and ready for review"
|
|
363
|
+
|
|
364
|
+
if [ -n "${ISSUE_NUMBER}" ]; then
|
|
365
|
+
printf 'Closes #%s\n\nNight Watch opened this draft PR at executor start so progress is preserved across retries and timeouts.\n\n%s\n' \
|
|
366
|
+
"${ISSUE_NUMBER}" \
|
|
367
|
+
"${status_blurb}"
|
|
368
|
+
else
|
|
369
|
+
printf 'Source PRD: `%s/%s`\n\nNight Watch opened this draft PR at executor start so progress is preserved across retries and timeouts.\n\n%s\n' \
|
|
370
|
+
"${PRD_DIR_REL}" \
|
|
371
|
+
"${ELIGIBLE_PRD}" \
|
|
372
|
+
"${status_blurb}"
|
|
373
|
+
fi
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
refresh_executor_pr_metadata() {
|
|
377
|
+
EXECUTOR_PR_JSON=$(find_open_pr_for_branch "${BRANCH_NAME}" || true)
|
|
378
|
+
EXECUTOR_PR_NUMBER=$(printf '%s' "${EXECUTOR_PR_JSON}" | jq -r '.number // empty' 2>/dev/null || true)
|
|
379
|
+
EXECUTOR_PR_URL=$(printf '%s' "${EXECUTOR_PR_JSON}" | jq -r '.url // empty' 2>/dev/null || true)
|
|
380
|
+
EXECUTOR_PR_DRAFT=$(printf '%s' "${EXECUTOR_PR_JSON}" | jq -r '.isDraft // false' 2>/dev/null || true)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
sync_executor_pr_status() {
|
|
384
|
+
local add_labels="${1:-}"
|
|
385
|
+
local remove_labels="${2:-}"
|
|
386
|
+
local mark_ready="${3:-0}"
|
|
387
|
+
|
|
388
|
+
if [ -z "${EXECUTOR_PR_NUMBER}" ]; then
|
|
389
|
+
return 1
|
|
390
|
+
fi
|
|
391
|
+
|
|
392
|
+
ensure_executor_status_labels
|
|
393
|
+
|
|
394
|
+
if [ -n "${add_labels}" ]; then
|
|
395
|
+
gh pr edit "${EXECUTOR_PR_NUMBER}" --add-label "${add_labels}" >> "${LOG_FILE}" 2>&1 || true
|
|
396
|
+
fi
|
|
397
|
+
if [ -n "${remove_labels}" ]; then
|
|
398
|
+
gh pr edit "${EXECUTOR_PR_NUMBER}" --remove-label "${remove_labels}" >> "${LOG_FILE}" 2>&1 || true
|
|
399
|
+
fi
|
|
400
|
+
if [ "${mark_ready}" = "1" ]; then
|
|
401
|
+
gh pr ready "${EXECUTOR_PR_NUMBER}" >> "${LOG_FILE}" 2>&1 || true
|
|
402
|
+
fi
|
|
403
|
+
|
|
404
|
+
refresh_executor_pr_metadata
|
|
405
|
+
return 0
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
mark_executor_pr_incomplete() {
|
|
409
|
+
sync_executor_pr_status \
|
|
410
|
+
"${NW_EXECUTOR_PARTIAL_LABEL},${NW_EXECUTOR_RESUMABLE_LABEL}" \
|
|
411
|
+
"${NW_EXECUTOR_READY_REVIEW_LABEL}" \
|
|
412
|
+
"0"
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
mark_executor_pr_ready_for_review() {
|
|
416
|
+
sync_executor_pr_status \
|
|
417
|
+
"${NW_EXECUTOR_READY_REVIEW_LABEL}" \
|
|
418
|
+
"${NW_EXECUTOR_PARTIAL_LABEL},${NW_EXECUTOR_RESUMABLE_LABEL}" \
|
|
419
|
+
"1"
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
push_executor_branch() {
|
|
423
|
+
local push_mode="${1:-update}"
|
|
424
|
+
|
|
425
|
+
if [ "${push_mode}" = "initial" ]; then
|
|
426
|
+
if git -C "${WORKTREE_DIR}" push -u origin "${BRANCH_NAME}" >> "${LOG_FILE}" 2>&1; then
|
|
427
|
+
return 0
|
|
428
|
+
fi
|
|
429
|
+
fi
|
|
430
|
+
|
|
431
|
+
if git -C "${WORKTREE_DIR}" push origin "${BRANCH_NAME}" --force-with-lease >> "${LOG_FILE}" 2>&1; then
|
|
432
|
+
return 0
|
|
433
|
+
fi
|
|
434
|
+
|
|
435
|
+
git -C "${WORKTREE_DIR}" push -u origin "${BRANCH_NAME}" >> "${LOG_FILE}" 2>&1
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
ensure_executor_pr() {
|
|
439
|
+
local pr_title=""
|
|
440
|
+
local pr_body=""
|
|
441
|
+
local create_output=""
|
|
442
|
+
|
|
443
|
+
refresh_executor_pr_metadata
|
|
444
|
+
if [ -n "${EXECUTOR_PR_NUMBER}" ]; then
|
|
445
|
+
log "PR: Reusing existing open PR #${EXECUTOR_PR_NUMBER} for ${BRANCH_NAME}"
|
|
446
|
+
mark_executor_pr_incomplete
|
|
447
|
+
return 0
|
|
448
|
+
fi
|
|
449
|
+
|
|
450
|
+
pr_title=$(build_executor_pr_title)
|
|
451
|
+
pr_body=$(build_executor_pr_body)
|
|
452
|
+
|
|
453
|
+
log "PR: Creating draft PR for ${BRANCH_NAME} before provider execution"
|
|
454
|
+
if ! push_executor_branch "initial"; then
|
|
455
|
+
log "WARN: Initial push for ${BRANCH_NAME} failed before PR creation"
|
|
456
|
+
fi
|
|
457
|
+
|
|
458
|
+
ensure_executor_status_labels
|
|
459
|
+
if ! create_output=$(
|
|
460
|
+
gh pr create \
|
|
461
|
+
--draft \
|
|
462
|
+
--base "${DEFAULT_BRANCH}" \
|
|
463
|
+
--head "${BRANCH_NAME}" \
|
|
464
|
+
--title "${pr_title}" \
|
|
465
|
+
--body "${pr_body}" 2>> "${LOG_FILE}"
|
|
466
|
+
); then
|
|
467
|
+
log "FAIL: gh pr create failed for ${BRANCH_NAME}"
|
|
468
|
+
return 1
|
|
469
|
+
fi
|
|
470
|
+
|
|
471
|
+
refresh_executor_pr_metadata
|
|
472
|
+
if [ -z "${EXECUTOR_PR_URL}" ]; then
|
|
473
|
+
EXECUTOR_PR_URL=$(printf '%s' "${create_output}" | grep -Eo 'https://[^[:space:]]+/pull/[0-9]+' | tail -n 1 || true)
|
|
474
|
+
fi
|
|
475
|
+
if [ -n "${EXECUTOR_PR_NUMBER}" ]; then
|
|
476
|
+
mark_executor_pr_incomplete
|
|
477
|
+
log "PR: Draft PR ready at ${EXECUTOR_PR_URL:-unknown} for ${BRANCH_NAME}"
|
|
478
|
+
else
|
|
479
|
+
log "WARN: gh pr create succeeded for ${BRANCH_NAME}, but PR metadata lookup did not resolve a number yet"
|
|
480
|
+
fi
|
|
481
|
+
|
|
482
|
+
return 0
|
|
483
|
+
}
|
|
484
|
+
|
|
251
485
|
checkpoint_timeout_progress() {
|
|
252
486
|
local worktree_dir="${1:?worktree_dir required}"
|
|
253
487
|
local branch_name="${2:?branch_name required}"
|
|
@@ -416,13 +650,12 @@ Read ${EXECUTOR_PROMPT_REF} and follow the FULL execution pipeline:
|
|
|
416
650
|
5. Run the project's verify/test command between waves to catch issues early
|
|
417
651
|
Follow all CLAUDE.md conventions (if present).
|
|
418
652
|
|
|
419
|
-
##
|
|
420
|
-
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
- After the PR is open, run final verification (lint, typecheck, tests). If anything fails, fix it, commit, and push again.
|
|
653
|
+
## PR Lifecycle
|
|
654
|
+
- The controller already created the draft PR for this branch${EXECUTOR_PR_URL:+: ${EXECUTOR_PR_URL}}
|
|
655
|
+
- Do NOT create another PR and do NOT edit PR status labels; the controller owns PR lifecycle and labels
|
|
656
|
+
- After each completed phase/wave milestone, commit and push progress to the existing branch:
|
|
657
|
+
git push origin ${BRANCH_NAME}
|
|
658
|
+
- When all implementation is complete, run final verification (lint, typecheck, tests). If anything fails, fix it, commit, and push again.
|
|
426
659
|
- STOP immediately after the final push β do NOT do any additional work, visual checks, or exploration.
|
|
427
660
|
- Do NOT process any other issues β only issue #${ISSUE_NUMBER}"
|
|
428
661
|
else
|
|
@@ -445,11 +678,12 @@ Read ${EXECUTOR_PROMPT_REF} and follow the FULL execution pipeline:
|
|
|
445
678
|
5. Run the project's verify/test command between waves to catch issues early
|
|
446
679
|
Follow all CLAUDE.md conventions (if present).
|
|
447
680
|
|
|
448
|
-
##
|
|
449
|
-
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
681
|
+
## PR Lifecycle
|
|
682
|
+
- The controller already created the draft PR for this branch${EXECUTOR_PR_URL:+: ${EXECUTOR_PR_URL}}
|
|
683
|
+
- Do NOT create another PR and do NOT edit PR status labels; the controller owns PR lifecycle and labels
|
|
684
|
+
- After each completed phase/wave milestone, commit and push progress to the existing branch:
|
|
685
|
+
git push origin ${BRANCH_NAME}
|
|
686
|
+
- When all implementation is complete, run final verification (lint, typecheck, tests). If anything fails, fix it, commit, and push again.
|
|
453
687
|
- STOP immediately after the final push β do NOT do any additional work, visual checks, or exploration.
|
|
454
688
|
- Do NOT move the PRD to done/ β the cron script handles that
|
|
455
689
|
- Do NOT process any other PRDs β only ${ELIGIBLE_PRD}"
|
|
@@ -505,6 +739,19 @@ if ! assert_isolated_worktree "${PROJECT_DIR}" "${WORKTREE_DIR}" "executor"; the
|
|
|
505
739
|
exit 1
|
|
506
740
|
fi
|
|
507
741
|
|
|
742
|
+
if ! ensure_executor_pr; then
|
|
743
|
+
log "FAIL: Unable to create or reuse executor PR for ${BRANCH_NAME}"
|
|
744
|
+
restore_issue_to_ready "Failed to create or reuse the draft PR for branch ${BRANCH_NAME}. Moved back to Ready for retry."
|
|
745
|
+
night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" failure --exit-code 1 2>/dev/null || true
|
|
746
|
+
emit_result "failure" "prd=${ELIGIBLE_PRD}|branch=${BRANCH_NAME}|reason=pr_setup_failed|detail=$(latest_failure_detail "${LOG_FILE}")"
|
|
747
|
+
exit 1
|
|
748
|
+
fi
|
|
749
|
+
|
|
750
|
+
if [ -n "${ISSUE_NUMBER}" ] && [ -n "${NW_CLI}" ] && [ -n "${EXECUTOR_PR_URL}" ] && [ "${RESUME_FROM_EXISTING_PR}" != "1" ]; then
|
|
751
|
+
"${NW_CLI}" board comment "${ISSUE_NUMBER}" \
|
|
752
|
+
--body "Draft PR opened at executor start: ${EXECUTOR_PR_URL} (labels: \`${NW_EXECUTOR_PARTIAL_LABEL}\`, \`${NW_EXECUTOR_RESUMABLE_LABEL}\`)." 2>>"${LOG_FILE}" || true
|
|
753
|
+
fi
|
|
754
|
+
|
|
508
755
|
# Sandbox: prevent the agent from modifying crontab during execution
|
|
509
756
|
export NW_EXECUTION_CONTEXT=agent
|
|
510
757
|
|
|
@@ -583,7 +830,7 @@ while [ "${ATTEMPT}" -lt "${MAX_RETRIES}" ]; do
|
|
|
583
830
|
fi
|
|
584
831
|
log "CONTEXT-EXHAUSTED: Session ${ATTEMPT_NUM} hit context limit β checkpointing and resuming (${ATTEMPT}/${MAX_RETRIES})"
|
|
585
832
|
checkpoint_timeout_progress "${WORKTREE_DIR}" "${BRANCH_NAME}" "${ELIGIBLE_PRD}"
|
|
586
|
-
|
|
833
|
+
push_executor_branch "update" >> "${LOG_FILE}" 2>&1 || true
|
|
587
834
|
# Switch prompt to "continue" mode for the next attempt (fresh context)
|
|
588
835
|
if [ -n "${ISSUE_NUMBER}" ]; then
|
|
589
836
|
PROMPT="Continue implementing PRD (GitHub issue #${ISSUE_NUMBER}: ${ISSUE_TITLE_RAW}).
|
|
@@ -606,13 +853,12 @@ The previous session ran out of context window. Progress has been committed on b
|
|
|
606
853
|
Read ${EXECUTOR_PROMPT_REF} and follow the FULL execution pipeline for remaining phases only.
|
|
607
854
|
Follow all CLAUDE.md conventions (if present).
|
|
608
855
|
|
|
609
|
-
##
|
|
610
|
-
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
- After the PR is open, run final verification (lint, typecheck, tests). If anything fails, fix it, commit, and push again.
|
|
856
|
+
## PR Lifecycle
|
|
857
|
+
- The controller already created the draft PR for this branch${EXECUTOR_PR_URL:+: ${EXECUTOR_PR_URL}}
|
|
858
|
+
- Do NOT create another PR and do NOT edit PR status labels; the controller owns PR lifecycle and labels
|
|
859
|
+
- After each completed phase/wave milestone, commit and push progress to the existing branch:
|
|
860
|
+
git push origin ${BRANCH_NAME}
|
|
861
|
+
- When all implementation is complete, run final verification (lint, typecheck, tests). If anything fails, fix it, commit, and push again.
|
|
616
862
|
- STOP immediately after the final push β do NOT do any additional work, visual checks, or exploration.
|
|
617
863
|
- Do NOT process any other issues β only issue #${ISSUE_NUMBER}"
|
|
618
864
|
else
|
|
@@ -636,11 +882,12 @@ The previous session ran out of context window. Progress has been committed on b
|
|
|
636
882
|
Read ${EXECUTOR_PROMPT_REF} and follow the FULL execution pipeline for remaining phases only.
|
|
637
883
|
Follow all CLAUDE.md conventions (if present).
|
|
638
884
|
|
|
639
|
-
##
|
|
640
|
-
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
885
|
+
## PR Lifecycle
|
|
886
|
+
- The controller already created the draft PR for this branch${EXECUTOR_PR_URL:+: ${EXECUTOR_PR_URL}}
|
|
887
|
+
- Do NOT create another PR and do NOT edit PR status labels; the controller owns PR lifecycle and labels
|
|
888
|
+
- After each completed phase/wave milestone, commit and push progress to the existing branch:
|
|
889
|
+
git push origin ${BRANCH_NAME}
|
|
890
|
+
- When all implementation is complete, run final verification (lint, typecheck, tests). If anything fails, fix it, commit, and push again.
|
|
644
891
|
- STOP immediately after the final push β do NOT do any additional work, visual checks, or exploration.
|
|
645
892
|
- Do NOT move the PRD to done/ β the cron script handles that
|
|
646
893
|
- Do NOT process any other PRDs β only ${ELIGIBLE_PRD}"
|
|
@@ -803,6 +1050,8 @@ fi
|
|
|
803
1050
|
if [ ${EXIT_CODE} -eq 0 ]; then
|
|
804
1051
|
OPEN_PR_COUNT=$(count_prs_for_branch open "${BRANCH_NAME}")
|
|
805
1052
|
if [ "${OPEN_PR_COUNT}" -gt 0 ]; then
|
|
1053
|
+
refresh_executor_pr_metadata
|
|
1054
|
+
mark_executor_pr_ready_for_review || true
|
|
806
1055
|
if [ -n "${ISSUE_NUMBER}" ]; then
|
|
807
1056
|
# Board mode: comment with PR URL, then close issue and move to Done
|
|
808
1057
|
PR_URL=$(gh pr list --state open --json headRefName,url \
|
|
@@ -858,6 +1107,7 @@ if [ ${EXIT_CODE} -eq 0 ]; then
|
|
|
858
1107
|
elif [ ${EXIT_CODE} -eq 124 ]; then
|
|
859
1108
|
log "TIMEOUT: Session limit hit after ${SESSION_MAX_RUNTIME}s while processing ${ELIGIBLE_PRD}"
|
|
860
1109
|
checkpoint_timeout_progress "${WORKTREE_DIR}" "${BRANCH_NAME}" "${ELIGIBLE_PRD}"
|
|
1110
|
+
push_executor_branch "update" >> "${LOG_FILE}" 2>&1 || true
|
|
861
1111
|
if [ -n "${ISSUE_NUMBER}" ]; then
|
|
862
1112
|
"${NW_CLI}" board move-issue "${ISSUE_NUMBER}" --column "Ready" 2>>"${LOG_FILE}" || true
|
|
863
1113
|
if [ "${SESSION_MAX_RUNTIME}" != "${MAX_RUNTIME}" ]; then
|
|
@@ -888,7 +1138,7 @@ elif check_context_exhausted "${LOG_FILE}" "${LOG_LINE_BEFORE}"; then
|
|
|
888
1138
|
# All resume attempts for context exhaustion were used up
|
|
889
1139
|
log "FAIL: Context window exhausted after ${MAX_RETRIES} resume attempts for ${ELIGIBLE_PRD}"
|
|
890
1140
|
checkpoint_timeout_progress "${WORKTREE_DIR}" "${BRANCH_NAME}" "${ELIGIBLE_PRD}"
|
|
891
|
-
|
|
1141
|
+
push_executor_branch "update" >> "${LOG_FILE}" 2>&1 || true
|
|
892
1142
|
if [ -n "${ISSUE_NUMBER}" ]; then
|
|
893
1143
|
"${NW_CLI}" board move-issue "${ISSUE_NUMBER}" --column "Ready" 2>>"${LOG_FILE}" || true
|
|
894
1144
|
"${NW_CLI}" board comment "${ISSUE_NUMBER}" \
|
|
@@ -87,6 +87,89 @@ validate_provider() {
|
|
|
87
87
|
return 1
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
# ββ Executor PR status labels ββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
91
|
+
|
|
92
|
+
NW_EXECUTOR_PARTIAL_LABEL="${NW_EXECUTOR_PARTIAL_LABEL:-nw:partial}"
|
|
93
|
+
NW_EXECUTOR_RESUMABLE_LABEL="${NW_EXECUTOR_RESUMABLE_LABEL:-nw:resumable}"
|
|
94
|
+
NW_EXECUTOR_READY_REVIEW_LABEL="${NW_EXECUTOR_READY_REVIEW_LABEL:-nw:ready-review}"
|
|
95
|
+
|
|
96
|
+
csv_has_label() {
|
|
97
|
+
local csv="${1:-}"
|
|
98
|
+
local label="${2:?label required}"
|
|
99
|
+
|
|
100
|
+
printf '%s\n' "${csv}" \
|
|
101
|
+
| tr ',' '\n' \
|
|
102
|
+
| sed '/^[[:space:]]*$/d' \
|
|
103
|
+
| grep -Fxq "${label}"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
ensure_github_label() {
|
|
107
|
+
local label_name="${1:?label_name required}"
|
|
108
|
+
local description="${2:-}"
|
|
109
|
+
local color="${3:-1d76db}"
|
|
110
|
+
|
|
111
|
+
gh label create "${label_name}" \
|
|
112
|
+
--description "${description}" \
|
|
113
|
+
--color "${color}" \
|
|
114
|
+
--force 2>/dev/null || true
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
ensure_executor_status_labels() {
|
|
118
|
+
ensure_github_label \
|
|
119
|
+
"${NW_EXECUTOR_PARTIAL_LABEL}" \
|
|
120
|
+
"Executor started this PR and work is intentionally incomplete" \
|
|
121
|
+
"fbca04"
|
|
122
|
+
ensure_github_label \
|
|
123
|
+
"${NW_EXECUTOR_RESUMABLE_LABEL}" \
|
|
124
|
+
"Executor should resume this unfinished PR before starting new work" \
|
|
125
|
+
"d93f0b"
|
|
126
|
+
ensure_github_label \
|
|
127
|
+
"${NW_EXECUTOR_READY_REVIEW_LABEL}" \
|
|
128
|
+
"Executor finished implementation and the PR is ready for automated/human review" \
|
|
129
|
+
"0e8a16"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
find_open_pr_for_branch() {
|
|
133
|
+
local branch_name="${1:?branch_name required}"
|
|
134
|
+
local pr_list=""
|
|
135
|
+
|
|
136
|
+
pr_list=$(gh pr list --state open --limit 200 \
|
|
137
|
+
--json number,headRefName,url,title,isDraft,labels,createdAt 2>/dev/null || echo "[]")
|
|
138
|
+
|
|
139
|
+
printf '%s' "${pr_list}" \
|
|
140
|
+
| jq -c --arg branch_name "${branch_name}" '
|
|
141
|
+
.[]
|
|
142
|
+
| select((.headRefName // "") == $branch_name)
|
|
143
|
+
' 2>/dev/null \
|
|
144
|
+
| head -n 1 || true
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
find_executor_resume_pr() {
|
|
148
|
+
local branch_prefix="${1:-night-watch}"
|
|
149
|
+
local pr_list=""
|
|
150
|
+
|
|
151
|
+
pr_list=$(gh pr list --state open --limit 200 \
|
|
152
|
+
--json number,headRefName,url,title,isDraft,labels,createdAt 2>/dev/null || echo "[]")
|
|
153
|
+
|
|
154
|
+
printf '%s' "${pr_list}" \
|
|
155
|
+
| jq -c \
|
|
156
|
+
--arg primary_prefix "${branch_prefix}/" \
|
|
157
|
+
--arg resumable_label "${NW_EXECUTOR_RESUMABLE_LABEL}" '
|
|
158
|
+
[
|
|
159
|
+
.[]
|
|
160
|
+
| select(
|
|
161
|
+
(.headRefName // "" | startswith($primary_prefix))
|
|
162
|
+
or
|
|
163
|
+
(.headRefName // "" | startswith("feat/"))
|
|
164
|
+
)
|
|
165
|
+
| .labelNames = ((.labels // []) | map(.name))
|
|
166
|
+
| select((.labelNames | index($resumable_label)) != null)
|
|
167
|
+
]
|
|
168
|
+
| sort_by(.createdAt // "")
|
|
169
|
+
| .[0] // empty
|
|
170
|
+
' 2>/dev/null || true
|
|
171
|
+
}
|
|
172
|
+
|
|
90
173
|
# ββ Generic Provider Command Builder ββββββββββββββββββββββββββββββββββββββββββ
|
|
91
174
|
|
|
92
175
|
# Build a provider command from NW_PROVIDER_* environment variables.
|
|
@@ -192,7 +192,7 @@ append_exit_trap "kill ${WATCHDOG_PID} 2>/dev/null || true"
|
|
|
192
192
|
# Discover open PRs sorted by creation date (oldest first = FIFO)
|
|
193
193
|
log "INFO: Scanning open PRs..."
|
|
194
194
|
PR_LIST_JSON=$(gh pr list --state open \
|
|
195
|
-
--json number,headRefName,createdAt,isDraft \
|
|
195
|
+
--json number,headRefName,createdAt,isDraft,labels \
|
|
196
196
|
--jq 'sort_by(.createdAt)' \
|
|
197
197
|
2>/dev/null || echo "[]")
|
|
198
198
|
|
|
@@ -211,6 +211,7 @@ while IFS= read -r pr_json; do
|
|
|
211
211
|
pr_number=$(echo "${pr_json}" | jq -r '.number')
|
|
212
212
|
pr_branch=$(echo "${pr_json}" | jq -r '.headRefName')
|
|
213
213
|
is_draft=$(echo "${pr_json}" | jq -r '.isDraft')
|
|
214
|
+
pr_labels=$(echo "${pr_json}" | jq -r '[.labels[]?.name] | join(",")')
|
|
214
215
|
|
|
215
216
|
# Skip drafts
|
|
216
217
|
if [ "${is_draft}" = "true" ]; then
|
|
@@ -218,6 +219,11 @@ while IFS= read -r pr_json; do
|
|
|
218
219
|
continue
|
|
219
220
|
fi
|
|
220
221
|
|
|
222
|
+
if csv_has_label "${pr_labels:-}" "${NW_EXECUTOR_PARTIAL_LABEL}"; then
|
|
223
|
+
log "INFO: PR #${pr_number} (${pr_branch}): Skipping partial executor PR"
|
|
224
|
+
continue
|
|
225
|
+
fi
|
|
226
|
+
|
|
221
227
|
# Check branch pattern filter
|
|
222
228
|
if ! matches_branch_patterns "${pr_branch}"; then
|
|
223
229
|
log "DEBUG: PR #${pr_number} (${pr_branch}): Branch pattern mismatch, skipping"
|
|
@@ -631,6 +631,11 @@ while IFS=$'\t' read -r pr_number pr_branch pr_labels; do
|
|
|
631
631
|
continue
|
|
632
632
|
fi
|
|
633
633
|
|
|
634
|
+
if csv_has_label "${pr_labels:-}" "${NW_EXECUTOR_PARTIAL_LABEL}"; then
|
|
635
|
+
log "INFO: PR #${pr_number} (${pr_branch}) is labeled ${NW_EXECUTOR_PARTIAL_LABEL}; waiting for executor to finish"
|
|
636
|
+
continue
|
|
637
|
+
fi
|
|
638
|
+
|
|
634
639
|
# Merge-conflict signal: this PR needs action even if CI and score look fine.
|
|
635
640
|
MERGE_STATE=$(gh pr view "${pr_number}" --json mergeStateStatus --jq '.mergeStateStatus' 2>/dev/null || echo "")
|
|
636
641
|
if [ "${MERGE_STATE}" = "DIRTY" ] || [ "${MERGE_STATE}" = "CONFLICTING" ]; then
|
|
@@ -426,6 +426,11 @@ while IFS=$'\t' read -r pr_number pr_branch pr_title pr_labels; do
|
|
|
426
426
|
continue
|
|
427
427
|
fi
|
|
428
428
|
|
|
429
|
+
if csv_has_label "${pr_labels:-}" "${NW_EXECUTOR_PARTIAL_LABEL}"; then
|
|
430
|
+
log "SKIP-QA: PR #${pr_number} (${pr_branch}) is labeled ${NW_EXECUTOR_PARTIAL_LABEL}"
|
|
431
|
+
continue
|
|
432
|
+
fi
|
|
433
|
+
|
|
429
434
|
# Skip PRs with the skip label
|
|
430
435
|
if echo "${pr_labels}" | grep -q "${SKIP_LABEL}"; then
|
|
431
436
|
log "SKIP-QA: PR #${pr_number} (${pr_branch}) has '${SKIP_LABEL}' label"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jonit-dev/night-watch-cli",
|
|
3
|
-
"version": "1.8.10-beta.
|
|
3
|
+
"version": "1.8.10-beta.6",
|
|
4
4
|
"description": "AI agent that implements your specs, opens PRs, and reviews code overnight. Queue GitHub issues or PRDs, wake up to pull requests.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|