@plainconceptsplatform/workflows 1.6.23 → 1.6.41
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/index.js +12 -1
- package/loops/actions/decide-agent-retry/action.yml +10 -2
- package/loops/actions/housekeeping/action.yml +11 -1
- package/loops/actions/merge-train/action.yml +5 -0
- package/loops/actions/merge-train/merge-train.sh +16 -3
- package/loops/actions/rebase-stale-patch/action.yml +41 -0
- package/loops/actions/rebase-stale-patch/rebase-stale-patch.sh +94 -0
- package/loops/actions/validate-merge-gate-output/action.yml +5 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +121 -2
- package/loops/docs/diagrams.md +5 -4
- package/loops/scripts/compile-agent-workflows.mjs +13 -0
- package/loops/workflows/agent-apply-review.md +9 -6
- package/loops/workflows/agent-audit.md +7 -4
- package/loops/workflows/agent-implement.md +54 -23
- package/loops/workflows/agent-merge-gate.md +31 -8
- package/loops/workflows/agent-refine.md +10 -7
- package/loops/workflows/agent-release.md +7 -4
- package/loops/workflows/agent-triage.md +9 -6
- package/loops/workflows/shared/opencode-ci.md +63 -8
- package/loops/workflows/shared/platform-defaults.md +30 -17
- package/loops/workflows/work-router.yml +14 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { formatCatalog, listCatalog, searchCatalog } from "./catalog-listing.js"
|
|
|
5
5
|
import { packageVersion } from "./package-baseline.js";
|
|
6
6
|
import { routeNames, templateNames } from "./workflow-catalog.js";
|
|
7
7
|
import { runInteractive } from "./tui.js";
|
|
8
|
+
import { realpathSync } from "node:fs";
|
|
8
9
|
import { resolve } from "node:path";
|
|
9
10
|
import { fileURLToPath } from "node:url";
|
|
10
11
|
const HELP_TEXT = `Workflows CLI install and update Plain Concepts Platform agentic workflows.
|
|
@@ -269,7 +270,17 @@ function fail(message) {
|
|
|
269
270
|
console.error(message);
|
|
270
271
|
return 1;
|
|
271
272
|
}
|
|
272
|
-
|
|
273
|
+
// pnpm installs the package behind a symlink: argv[1] is the link and import.meta.url the real file,
|
|
274
|
+
// so a plain comparison never matched and the installed CLI exited silently, --version included.
|
|
275
|
+
function realPath(path) {
|
|
276
|
+
try {
|
|
277
|
+
return realpathSync(path);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return resolve(path);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (process.argv[1] !== undefined && realPath(process.argv[1]) === realPath(fileURLToPath(import.meta.url))) {
|
|
273
284
|
void run(process.argv.slice(2)).then((exitCode) => {
|
|
274
285
|
process.exitCode = exitCode;
|
|
275
286
|
});
|
|
@@ -61,9 +61,9 @@ runs:
|
|
|
61
61
|
run: |
|
|
62
62
|
set -euo pipefail
|
|
63
63
|
# The agent job belongs to this same run: a called workflow shares the caller's run id.
|
|
64
|
-
read -r job_id started finished <<<"$(gh api "repos/$REPO/actions/runs/$RUN_ID/jobs?per_page=100" \
|
|
64
|
+
read -r job_id started finished conclusion <<<"$(gh api "repos/$REPO/actions/runs/$RUN_ID/jobs?per_page=100" \
|
|
65
65
|
--jq '[.jobs[] | select(.name | endswith("agent"))] | last // empty
|
|
66
|
-
| "\(.id // "") \(.started_at // "") \(.completed_at // "")"')"
|
|
66
|
+
| "\(.id // "") \(.started_at // "") \(.completed_at // "") \(.conclusion // "")"')"
|
|
67
67
|
minutes=-1
|
|
68
68
|
if [ -n "${started:-}" ] && [ -n "${finished:-}" ]; then
|
|
69
69
|
minutes=$(( ( $(date -u -d "$finished" +%s) - $(date -u -d "$started" +%s) ) / 60 ))
|
|
@@ -91,6 +91,14 @@ runs:
|
|
|
91
91
|
fi
|
|
92
92
|
rm -rf "$work"
|
|
93
93
|
fi
|
|
94
|
+
# A cancelled agent job is a person or a newer run taking the work back, not an outage.
|
|
95
|
+
# Pliny-Bot #816: refine was cancelled by hand and its label removed, then this step read
|
|
96
|
+
# the eight-minute run as a provider failure, sent it back through the router and parked
|
|
97
|
+
# the issue as stalled.
|
|
98
|
+
if [ "${conclusion:-}" = cancelled ]; then
|
|
99
|
+
retry=false
|
|
100
|
+
cause=cancelled
|
|
101
|
+
fi
|
|
94
102
|
{
|
|
95
103
|
echo "retry=$retry"
|
|
96
104
|
echo "next=$((attempts + 1))"
|
|
@@ -311,7 +311,17 @@ runs:
|
|
|
311
311
|
});
|
|
312
312
|
const told = comments.find((c) => (c.body ?? '').includes('<!-- stack-complete -->'));
|
|
313
313
|
if (!told) {
|
|
314
|
-
|
|
314
|
+
// The router announces a finished stack when a child's pull request merges into it.
|
|
315
|
+
// A child closed any other way (by hand, or by this sweep) never raised that event,
|
|
316
|
+
// and Pliny-Bot #781 sat finished with no announcement, which is what the merge train
|
|
317
|
+
// waits for before it lets the root merge. Announce it here; the root's own gate and
|
|
318
|
+
// CI still decide whether it merges.
|
|
319
|
+
await act(`#${rootPr}: announcing the finished stack of #${issue.number}`, async () => {
|
|
320
|
+
await github.rest.issues.createComment({
|
|
321
|
+
...context.repo, issue_number: rootPr,
|
|
322
|
+
body: `Stack complete: every child of #${issue.number} has landed on the feature branch. This root pull request is ready to merge into ${context.payload.repository?.default_branch ?? 'main'}: the merge gate decides it next, and a person is asked only when its blast radius or a finding says so. <!-- stack-complete -->`,
|
|
323
|
+
});
|
|
324
|
+
});
|
|
315
325
|
} else if (age(told.created_at) > stalePrMs) {
|
|
316
326
|
needsHuman.push({ kind: 'stack-held', issue, pull: root, waited: age(told.created_at) });
|
|
317
327
|
}
|
|
@@ -28,6 +28,10 @@ inputs:
|
|
|
28
28
|
description: How many times a blocked verdict, or a merge that did not happen, is retried after the base branch moves.
|
|
29
29
|
required: false
|
|
30
30
|
default: '3'
|
|
31
|
+
stack-root-automerge:
|
|
32
|
+
description: A stack root whose children have all landed is merged by the gate like any other pull request, unless its blast radius or a finding sends it to a person. false keeps every root for a human.
|
|
33
|
+
required: false
|
|
34
|
+
default: 'true'
|
|
31
35
|
max-repairs:
|
|
32
36
|
description: How many pull requests may be in repair at once. Merges stay one at a time.
|
|
33
37
|
required: false
|
|
@@ -69,6 +73,7 @@ runs:
|
|
|
69
73
|
HOLD_LABEL: ${{ inputs.hold-label }}
|
|
70
74
|
MAX_ATTEMPTS: ${{ inputs.max-gate-attempts }}
|
|
71
75
|
MAX_RETRIES: ${{ inputs.max-retries }}
|
|
76
|
+
STACK_ROOT_AUTOMERGE: ${{ inputs.stack-root-automerge }}
|
|
72
77
|
STUCK_HOURS: ${{ inputs.stuck-hours }}
|
|
73
78
|
MAIN_RED_WAIT_HOURS: ${{ inputs.main-red-wait-hours }}
|
|
74
79
|
MAX_REPAIRS: ${{ inputs.max-repairs }}
|
|
@@ -114,14 +114,19 @@ train_move() {
|
|
|
114
114
|
# BASE_MOVED true when the base branch has a commit newer than the verdict
|
|
115
115
|
# RETRIES blocked or failed-merge verdicts already given on this head
|
|
116
116
|
# MAX_RETRIES how many of those the train retries before leaving it to a person
|
|
117
|
-
# STACK_ROOT true for a stack's root pull request, which a person
|
|
117
|
+
# STACK_ROOT true for a stack's root pull request, which a person merges unless it is ready
|
|
118
|
+
# STACK_ROOT_READY true for a root whose stack was announced complete after the last verdict, with
|
|
119
|
+
# STACK_ROOT_AUTOMERGE on: the train gives it one more gate run, which merges it
|
|
120
|
+
# unless the blast radius or a finding says a person must
|
|
118
121
|
train_park() {
|
|
119
122
|
local park=none
|
|
120
123
|
if [ "${VERDICT_NEWER:-false}" = true ]; then
|
|
121
124
|
case "${VERDICT:-}" in
|
|
122
125
|
remediated | "") park=none ;;
|
|
123
126
|
blocked | auto-merge)
|
|
124
|
-
if [ "${STACK_ROOT:-false}" = true ]; then
|
|
127
|
+
if [ "${STACK_ROOT:-false}" = true ] && [ "${STACK_ROOT_READY:-false}" = true ]; then
|
|
128
|
+
park=none
|
|
129
|
+
elif [ "${STACK_ROOT:-false}" = true ]; then
|
|
125
130
|
park=held
|
|
126
131
|
elif [ "${BASE_MOVED:-false}" = true ] && [ "${RETRIES:-0}" -lt "${MAX_RETRIES:-3}" ]; then
|
|
127
132
|
park=none
|
|
@@ -360,15 +365,23 @@ for line in "${candidates[@]}"; do
|
|
|
360
365
|
fi
|
|
361
366
|
|
|
362
367
|
stack_root=false
|
|
368
|
+
stack_root_ready=false
|
|
363
369
|
if [ "$verdict_newer" = true ] && [ -n "$linked_issue" ]; then
|
|
364
370
|
if gh issue view "$linked_issue" --repo "$REPO" --json body --jq '.body // ""' 2>/dev/null |
|
|
365
371
|
grep -q "<!-- stack-root-pr: $pr -->"; then
|
|
366
372
|
stack_root=true
|
|
373
|
+
if [ "${STACK_ROOT_AUTOMERGE:-false}" = true ]; then
|
|
374
|
+
announced_at=$(gh api --paginate "repos/$REPO/issues/$pr/comments?per_page=100" \
|
|
375
|
+
--jq '.[] | select(.body | contains("<!-- stack-complete -->")) | .created_at' 2>/dev/null | sort | tail -1)
|
|
376
|
+
if [ -n "$announced_at" ] && [ "$announced_at" \> "$verdict_at" ]; then
|
|
377
|
+
stack_root_ready=true
|
|
378
|
+
fi
|
|
379
|
+
fi
|
|
367
380
|
fi
|
|
368
381
|
fi
|
|
369
382
|
|
|
370
383
|
park=$(VERDICT="$verdict" VERDICT_NEWER="$verdict_newer" BASE_MOVED="$base_moved" \
|
|
371
|
-
RETRIES="$retries" MAX_RETRIES="$MAX_RETRIES" STACK_ROOT="$stack_root" train_park)
|
|
384
|
+
RETRIES="$retries" MAX_RETRIES="$MAX_RETRIES" STACK_ROOT="$stack_root" STACK_ROOT_READY="$stack_root_ready" train_park)
|
|
372
385
|
|
|
373
386
|
attempts=0
|
|
374
387
|
silent_gate=false
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/rebase-stale-patch/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: Rebase a stale agent patch
|
|
3
|
+
description: When an agent's patch no longer applies because its base branch moved, rebase the run's bundle onto the base and open the pull request the agent asked for. A real conflict is left for a person.
|
|
4
|
+
inputs:
|
|
5
|
+
token:
|
|
6
|
+
description: App token that can push a branch, open a pull request and close an issue.
|
|
7
|
+
required: true
|
|
8
|
+
run-id:
|
|
9
|
+
description: The workflow run whose agent artifact holds the bundle.
|
|
10
|
+
required: true
|
|
11
|
+
artifact:
|
|
12
|
+
description: Name of the agent artifact, with the run's artifact prefix.
|
|
13
|
+
required: true
|
|
14
|
+
issue-number:
|
|
15
|
+
description: The issue the agent worked on.
|
|
16
|
+
required: true
|
|
17
|
+
default-branch:
|
|
18
|
+
description: Base used when the issue is not part of a stack.
|
|
19
|
+
required: true
|
|
20
|
+
outputs:
|
|
21
|
+
rescued:
|
|
22
|
+
description: '"true" when a pull request was opened from the rebased bundle.'
|
|
23
|
+
value: ${{ steps.rebase.outputs.rescued }}
|
|
24
|
+
pr-number:
|
|
25
|
+
description: The pull request opened, or empty.
|
|
26
|
+
value: ${{ steps.rebase.outputs.pr_number }}
|
|
27
|
+
runs:
|
|
28
|
+
using: composite
|
|
29
|
+
steps:
|
|
30
|
+
- name: Rebase the bundle and open its pull request
|
|
31
|
+
id: rebase
|
|
32
|
+
shell: bash
|
|
33
|
+
env:
|
|
34
|
+
GH_TOKEN: ${{ inputs.token }}
|
|
35
|
+
DOWNLOAD_TOKEN: ${{ github.token }}
|
|
36
|
+
REPO: ${{ github.repository }}
|
|
37
|
+
RUN_ID: ${{ inputs.run-id }}
|
|
38
|
+
ARTIFACT: ${{ inputs.artifact }}
|
|
39
|
+
ISSUE: ${{ inputs.issue-number }}
|
|
40
|
+
DEFAULT_BRANCH: ${{ inputs.default-branch }}
|
|
41
|
+
run: bash "${{ github.action_path }}/rebase-stale-patch.sh"
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/rebase-stale-patch/rebase-stale-patch.sh. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
3
|
+
# gh-aw pushes an agent's commits onto the base as it is when the run ends. When the base moved
|
|
4
|
+
# under a long run and the commits no longer apply, gh-aw files the patch as an issue and the work
|
|
5
|
+
# waits for a person, although most of the time a plain rebase applies it: Pliny-Bot #787 and #794
|
|
6
|
+
# were both rebased by hand in minutes. This does that rebase here and opens the pull request the
|
|
7
|
+
# agent asked for. A conflict a rebase cannot settle stays with a person, exactly as before.
|
|
8
|
+
#
|
|
9
|
+
# Needs: GH_TOKEN (App), DOWNLOAD_TOKEN (the run's own token, for its artifacts), REPO, RUN_ID, ARTIFACT, ISSUE, DEFAULT_BRANCH, GITHUB_OUTPUT.
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
say() { echo "$1"; }
|
|
14
|
+
done_with() {
|
|
15
|
+
echo "rescued=$1" >> "$GITHUB_OUTPUT"
|
|
16
|
+
echo "pr_number=${2:-}" >> "$GITHUB_OUTPUT"
|
|
17
|
+
exit 0
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
work="$(mktemp -d)"
|
|
21
|
+
if ! GH_TOKEN="${DOWNLOAD_TOKEN:-$GH_TOKEN}" gh run download "$RUN_ID" --repo "$REPO" --name "$ARTIFACT" --dir "$work" >/dev/null 2>&1; then
|
|
22
|
+
say "No agent artifact to rebase from."
|
|
23
|
+
done_with false
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
bundle="$(find "$work" -maxdepth 1 -name '*.bundle' | head -1)"
|
|
27
|
+
output="$work/agent_output.json"
|
|
28
|
+
if [ -z "$bundle" ] || [ ! -f "$output" ]; then
|
|
29
|
+
say "The artifact holds no bundle or no agent output."
|
|
30
|
+
done_with false
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
request="$(jq -c '[.items[]? | select(.type == "create_pull_request")] | first // empty' "$output")"
|
|
34
|
+
if [ -z "$request" ]; then
|
|
35
|
+
say "The agent did not ask for a pull request."
|
|
36
|
+
done_with false
|
|
37
|
+
fi
|
|
38
|
+
branch="$(jq -r '.branch // empty' <<<"$request")"
|
|
39
|
+
title="$(jq -r '.title // empty' <<<"$request")"
|
|
40
|
+
jq -r '.body // ""' <<<"$request" > "$work/body.md"
|
|
41
|
+
if [ -z "$branch" ] || [ -z "$title" ]; then
|
|
42
|
+
say "The pull request request has no branch or title."
|
|
43
|
+
done_with false
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# A stack child targets its feature branch while that branch exists; otherwise the default branch.
|
|
47
|
+
base="$DEFAULT_BRANCH"
|
|
48
|
+
stack="$(gh issue view "$ISSUE" --repo "$REPO" --json body --jq '.body // ""' |
|
|
49
|
+
sed -nE 's/.*<!-- stack-root-branch: ([^ ]+) -->.*/\1/p' | head -1)"
|
|
50
|
+
if [ -n "$stack" ] && gh api "repos/$REPO/branches/$stack" >/dev/null 2>&1; then
|
|
51
|
+
base="$stack"
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
remote="https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git"
|
|
55
|
+
# Full history: the bundle's prerequisites are commits of the base, and a rebase needs their blobs.
|
|
56
|
+
if git rev-parse --is-shallow-repository | grep -q true; then
|
|
57
|
+
git fetch --quiet --unshallow "$remote" "+refs/heads/${base}:refs/remotes/rescue-base/${base}"
|
|
58
|
+
else
|
|
59
|
+
git fetch --quiet "$remote" "+refs/heads/${base}:refs/remotes/rescue-base/${base}"
|
|
60
|
+
fi
|
|
61
|
+
git fetch --quiet "$bundle" "+refs/heads/*:refs/remotes/rescue-bundle/*"
|
|
62
|
+
head="refs/remotes/rescue-bundle/${branch}"
|
|
63
|
+
if ! git rev-parse --verify --quiet "$head" >/dev/null; then
|
|
64
|
+
head="$(git for-each-ref --format='%(refname)' refs/remotes/rescue-bundle | head -1)"
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
rescued_branch="${branch}-rebased-${RUN_ID}"
|
|
68
|
+
git checkout --quiet -B "$rescued_branch" "$head"
|
|
69
|
+
if ! git -c user.name="github-actions[bot]" -c user.email="41898282+github-actions[bot]@users.noreply.github.com" \
|
|
70
|
+
rebase --quiet "refs/remotes/rescue-base/${base}"; then
|
|
71
|
+
git rebase --abort || true
|
|
72
|
+
say "The rebase onto ${base} conflicts: left for a person."
|
|
73
|
+
done_with false
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
git push --quiet "$remote" "HEAD:refs/heads/${rescued_branch}"
|
|
77
|
+
|
|
78
|
+
grep -qiE "(closes|fixes|resolves) #${ISSUE}\b" "$work/body.md" || printf '\n\nCloses #%s\n' "$ISSUE" >> "$work/body.md"
|
|
79
|
+
printf '\n\nRebased onto `%s` by the workflow: the agent'"'"'s commits no longer applied after `%s` moved during the run.\n' \
|
|
80
|
+
"$base" "$base" >> "$work/body.md"
|
|
81
|
+
[[ "$title" == "[bot] "* ]] || title="[bot] $title"
|
|
82
|
+
url="$(gh pr create --repo "$REPO" --base "$base" --head "$rescued_branch" --title "$title" --body-file "$work/body.md")"
|
|
83
|
+
number="${url##*/}"
|
|
84
|
+
|
|
85
|
+
# The issue gh-aw filed with the patch is superseded by the pull request.
|
|
86
|
+
stray="$(gh issue list --repo "$REPO" --state open --limit 30 --json number,title 2>/dev/null |
|
|
87
|
+
jq -r --arg t "$title" '[.[] | select(.title == $t)] | first | .number // empty' || true)"
|
|
88
|
+
if [ -n "$stray" ]; then
|
|
89
|
+
gh issue close "$stray" --repo "$REPO" --reason "not planned" \
|
|
90
|
+
--comment "Superseded by #${number}: the patch was rebased onto ${base} and opened as a pull request." >/dev/null
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
say "Rebased onto ${base} and opened #${number}."
|
|
94
|
+
done_with true "$number"
|
|
@@ -41,6 +41,9 @@ outputs:
|
|
|
41
41
|
pushed:
|
|
42
42
|
description: Whether the agent proposed a push to the pull request branch, applied or not.
|
|
43
43
|
value: ${{ steps.validate.outputs.pushed }}
|
|
44
|
+
empty:
|
|
45
|
+
description: Whether the agent produced no safe output at all, which is a run that quit, not a report that was refused.
|
|
46
|
+
value: ${{ steps.validate.outputs.empty }}
|
|
44
47
|
runs:
|
|
45
48
|
using: composite
|
|
46
49
|
steps:
|
|
@@ -66,6 +69,8 @@ runs:
|
|
|
66
69
|
echo "valid=$([ "$outcome" != 'invalid' ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
|
|
67
70
|
pushed=$(jq -r '[.items[]? | select(.type == "push_to_pull_request_branch")] | length > 0' "$OUTPUT_FILE" 2>/dev/null || echo false)
|
|
68
71
|
echo "pushed=$pushed" >> "$GITHUB_OUTPUT"
|
|
72
|
+
items=$(jq -r '[.items[]?] | length' "$OUTPUT_FILE" 2>/dev/null || echo 0)
|
|
73
|
+
echo "empty=$([ "${items:-0}" = 0 ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
|
|
69
74
|
|
|
70
75
|
if [ "$outcome" = 'invalid' ]; then
|
|
71
76
|
echo "::warning::Agent output has no usable merge-gate report. It will not be applied."
|
|
@@ -1792,11 +1792,11 @@ fi
|
|
|
1792
1792
|
# full verification at minute forty-one after six scoped runs, and #751 spent fifteen of twenty-two
|
|
1793
1793
|
# minutes in twelve tiny worker tasks. These sentences are what keep a run near twenty minutes.
|
|
1794
1794
|
if worker_installed implement; then
|
|
1795
|
-
if grep -qF 'Run them once, on the final tree.' "$IMPLEMENT_WORKER_MD" && grep -qF 'on both paths, before the commands above.' "$IMPLEMENT_WORKER_MD" && grep -qF '
|
|
1795
|
+
if grep -qF 'Run them once, on the final tree.' "$IMPLEMENT_WORKER_MD" && grep -qF 'on both paths, before the commands above.' "$IMPLEMENT_WORKER_MD" && grep -qF 'tasks that do not depend on each' "$IMPLEMENT_WORKER_MD" && grep -qF 'WORK IN FEW TURNS' "$IMPLEMENT_WORKER_MD"; then
|
|
1796
1796
|
PASS=$((PASS + 1))
|
|
1797
1797
|
else
|
|
1798
1798
|
FAIL=$((FAIL + 1))
|
|
1799
|
-
echo "FAIL: the implement prompt must verify once on the final tree,
|
|
1799
|
+
echo "FAIL: the implement prompt must verify once on the final tree, let independent tasks run in parallel and work in few turns" >&2
|
|
1800
1800
|
fi
|
|
1801
1801
|
fi
|
|
1802
1802
|
# gh-aw leaves OPENCODE_MODEL empty, so every lead ran on opencode.ci.json's one model. Each worker
|
|
@@ -1824,6 +1824,9 @@ fi
|
|
|
1824
1824
|
if ! grep -qF 'jq --arg model "$AGENT_LEAD_MODEL"' "${WORKFLOWS_DIR}/shared/opencode-ci.md"; then
|
|
1825
1825
|
LEAD_OK=0; echo "FAIL: the CI config merge must write AGENT_LEAD_MODEL onto ci-workflow-agent" >&2
|
|
1826
1826
|
fi
|
|
1827
|
+
if ! grep -qF 'contents/${FRAGMENT}?ref=${DEFAULT_BRANCH}' "${WORKFLOWS_DIR}/shared/opencode-ci.md"; then
|
|
1828
|
+
LEAD_OK=0; echo "FAIL: the CI config merge must read opencode.ci.json from the default branch" >&2
|
|
1829
|
+
fi
|
|
1827
1830
|
if worker_installed implement && ! grep -qF 'subagent_type: planner' "$IMPLEMENT_WORKER_MD"; then
|
|
1828
1831
|
LEAD_OK=0; echo "FAIL: the implement prompt must run propose and archive in the planner agent" >&2
|
|
1829
1832
|
fi
|
|
@@ -2041,6 +2044,8 @@ if worker_installed merge-gate; then
|
|
|
2041
2044
|
assert "blocked holds once its retries are spent" held "$(park VERDICT=blocked VERDICT_NEWER=true BASE_MOVED=true RETRIES=3 MAX_RETRIES=3)"
|
|
2042
2045
|
assert "a merge that did not happen is retried once the base moves" none "$(park VERDICT=auto-merge VERDICT_NEWER=true BASE_MOVED=true RETRIES=1)"
|
|
2043
2046
|
assert "a stack root is never retried into a merge" held "$(park VERDICT=auto-merge VERDICT_NEWER=true BASE_MOVED=true RETRIES=0 STACK_ROOT=true)"
|
|
2047
|
+
assert "a root whose stack was announced complete gets one more gate run" none "$(park VERDICT=auto-merge VERDICT_NEWER=true BASE_MOVED=false RETRIES=0 STACK_ROOT=true STACK_ROOT_READY=true)"
|
|
2048
|
+
assert "a ready root whose merge failed is held again, not looped" held "$(park VERDICT=owner-review VERDICT_NEWER=true STACK_ROOT=true STACK_ROOT_READY=true)"
|
|
2044
2049
|
|
|
2045
2050
|
assert "hold keeps a pull request off the train" skip "$(move HOLD=true CI_STATUS=completed CI_CONCLUSION=success)"
|
|
2046
2051
|
assert "requested changes keep it off the train" skip "$(move REVIEW_DECISION=CHANGES_REQUESTED CI_STATUS=completed CI_CONCLUSION=success)"
|
|
@@ -2738,6 +2743,120 @@ if worker_installed implement; then
|
|
|
2738
2743
|
echo "FAIL: the implement prompt does not forbid running commands in the background and ending the turn to wait for them" >&2
|
|
2739
2744
|
fi
|
|
2740
2745
|
fi
|
|
2746
|
+
# The bash tool moves any command past its timeout to the background and says so. Pliny-Bot #774 took
|
|
2747
|
+
# that for an end, wrote "I'll wait for the verify command" and stopped after sixty-seven minutes. The
|
|
2748
|
+
# prompt must say that the message is not the end and to poll with tool calls.
|
|
2749
|
+
if worker_installed implement; then
|
|
2750
|
+
if grep -qF 'answers "moved to the background"' "${WORKFLOWS_DIR}/agent-implement.md" && grep -qF "sleep 40; tail -n 15" "${WORKFLOWS_DIR}/agent-implement.md"; then
|
|
2751
|
+
PASS=$((PASS + 1))
|
|
2752
|
+
else
|
|
2753
|
+
FAIL=$((FAIL + 1))
|
|
2754
|
+
echo "FAIL: the implement prompt does not tell the agent to keep polling when bash moves a command to the background" >&2
|
|
2755
|
+
fi
|
|
2756
|
+
fi
|
|
2757
|
+
# Every worker is told that a turn ending in text ends the run. The Pliny-Bot merge gate said "I'll read
|
|
2758
|
+
# the required context files first." and exited cleanly after twenty-three seconds, twice on one head.
|
|
2759
|
+
PLATFORM_DEFAULTS_MD="${WORKFLOWS_DIR}/shared/platform-defaults.md"
|
|
2760
|
+
if [ -f "$PLATFORM_DEFAULTS_MD" ]; then
|
|
2761
|
+
if grep -qF "A turn that ends with text and no tool call ends the whole run" "$PLATFORM_DEFAULTS_MD" && grep -qF "every message you write carries a tool call" "$PLATFORM_DEFAULTS_MD"; then
|
|
2762
|
+
PASS=$((PASS + 1))
|
|
2763
|
+
else
|
|
2764
|
+
FAIL=$((FAIL + 1))
|
|
2765
|
+
echo "FAIL: the shared worker defaults do not say that a text-only turn ends the run" >&2
|
|
2766
|
+
fi
|
|
2767
|
+
fi
|
|
2768
|
+
# A gate agent that produced no output at all quit; it did not write a report the validator refused.
|
|
2769
|
+
# It gets the machine budget, not the two-strike human-review of an unusable report.
|
|
2770
|
+
if worker_installed merge-gate; then
|
|
2771
|
+
if grep -qF 'OUTPUT_EMPTY: ${{ needs.validate_output.outputs.empty }}' "${WORKFLOWS_DIR}/agent-merge-gate.md" && grep -qF '[ "${OUTPUT_EMPTY:-false}" != true ]' "${WORKFLOWS_DIR}/agent-merge-gate.md" && grep -qF 'echo "empty=' "${HERE}/../validate-merge-gate-output/action.yml"; then
|
|
2772
|
+
PASS=$((PASS + 1))
|
|
2773
|
+
else
|
|
2774
|
+
FAIL=$((FAIL + 1))
|
|
2775
|
+
echo "FAIL: a gate agent that produced nothing is sent to a person instead of being retried" >&2
|
|
2776
|
+
fi
|
|
2777
|
+
fi
|
|
2778
|
+
# A cancelled agent job is work taken back, not an outage: no retry, no stalled flag. Pliny-Bot #816 was
|
|
2779
|
+
# cancelled by hand, re-dispatched as "a provider outage" and parked as stalled.
|
|
2780
|
+
DECIDE_YML="${HERE}/../decide-agent-retry/action.yml"
|
|
2781
|
+
if [ -f "$DECIDE_YML" ] && grep -qF 'cause=cancelled' "$DECIDE_YML" && grep -qF "steps.decide.outputs.cause != 'cancelled'" "${WORKFLOWS_DIR}/agent-refine.md" && grep -qF "steps.decide.outputs.cause != 'cancelled'" "${WORKFLOWS_DIR}/agent-implement.md"; then
|
|
2782
|
+
PASS=$((PASS + 1))
|
|
2783
|
+
else
|
|
2784
|
+
FAIL=$((FAIL + 1))
|
|
2785
|
+
echo "FAIL: a cancelled agent run is retried or parked as stalled" >&2
|
|
2786
|
+
fi
|
|
2787
|
+
# No prompt rule stopped the model ending a turn in text; the compiled engine command continues the
|
|
2788
|
+
# session when no safe output exists. Pliny-Bot #771 passed verify and exited with nothing delivered.
|
|
2789
|
+
COMPILE_MJS="${HERE}/../../scripts/compile-agent-workflows.mjs"
|
|
2790
|
+
# In a consumer the action sits under .github/ and the script at the repository root.
|
|
2791
|
+
[ -f "$COMPILE_MJS" ] || COMPILE_MJS="${HERE}/../../../scripts/compile-agent-workflows.mjs"
|
|
2792
|
+
if [ -f "$COMPILE_MJS" ] && grep -qF 'opencode run --continue' "$COMPILE_MJS" && grep -qF 'gh-aw/safeoutputs/outputs.jsonl' "$COMPILE_MJS"; then
|
|
2793
|
+
PASS=$((PASS + 1))
|
|
2794
|
+
else
|
|
2795
|
+
FAIL=$((FAIL + 1))
|
|
2796
|
+
echo "FAIL: the compiled engine command does not continue a session that delivered nothing" >&2
|
|
2797
|
+
fi
|
|
2798
|
+
# A patch the base moved under is rebased and opened as a pull request before anyone is asked: Pliny-Bot
|
|
2799
|
+
# #787 and #794 sat as issues until rebased by hand. The rescue must run before the pull request steps,
|
|
2800
|
+
# and the push-failure flags must stay quiet when it worked.
|
|
2801
|
+
IMPL_MD="${WORKFLOWS_DIR}/agent-implement.md"
|
|
2802
|
+
if worker_installed implement; then
|
|
2803
|
+
if grep -qF 'uses: ./.github/actions/rebase-stale-patch' "$IMPL_MD" && grep -qF "steps.rescue.outputs.rescued == 'true'" "$IMPL_MD" && grep -qF "&& steps.rescue.outputs.rescued != 'true'" "$IMPL_MD" && [ -f "${HERE}/../rebase-stale-patch/rebase-stale-patch.sh" ]; then
|
|
2804
|
+
PASS=$((PASS + 1))
|
|
2805
|
+
else
|
|
2806
|
+
FAIL=$((FAIL + 1))
|
|
2807
|
+
echo "FAIL: implement does not rebase a patch its base moved under before parking it" >&2
|
|
2808
|
+
fi
|
|
2809
|
+
fi
|
|
2810
|
+
# The subagent monitor never sees a CI child finish and denies every spawn after the cap.
|
|
2811
|
+
if grep -qF 'for plugin in pc-system-reminders pc-subagent-monitor' "${WORKFLOWS_DIR}/shared/opencode-ci.md"; then
|
|
2812
|
+
PASS=$((PASS + 1))
|
|
2813
|
+
else
|
|
2814
|
+
FAIL=$((FAIL + 1))
|
|
2815
|
+
echo "FAIL: the CI setup must remove pc-subagent-monitor in CI" >&2
|
|
2816
|
+
fi
|
|
2817
|
+
# a published pull request is never flagged stalled, whatever the agent job's own result.
|
|
2818
|
+
if grep -A8 '^ incomplete:' "$IMPLEMENT_WORKER_MD" | grep -qF "needs.safe_outputs.outputs.created_pr_number == ''"; then
|
|
2819
|
+
PASS=$((PASS + 1))
|
|
2820
|
+
else
|
|
2821
|
+
FAIL=$((FAIL + 1))
|
|
2822
|
+
echo "FAIL: implement flags an issue stalled next to the pull request it just opened" >&2
|
|
2823
|
+
fi
|
|
2824
|
+
# An empty AGENT_TIER_MODEL must remove an override a reused runner kept from an earlier job.
|
|
2825
|
+
if grep -qF 'rm -f .opencode/harness.user.json' "${WORKFLOWS_DIR}/shared/opencode-ci.md"; then
|
|
2826
|
+
PASS=$((PASS + 1))
|
|
2827
|
+
else
|
|
2828
|
+
FAIL=$((FAIL + 1))
|
|
2829
|
+
echo "FAIL: a stale harness.user.json on a reused runner keeps every tier pinned" >&2
|
|
2830
|
+
fi
|
|
2831
|
+
# A stack whose last child closed outside the router is announced by housekeeping, or its root waits.
|
|
2832
|
+
if grep -qF "announcing the finished stack" "${HERE}/../housekeeping/action.yml"; then
|
|
2833
|
+
PASS=$((PASS + 1))
|
|
2834
|
+
else
|
|
2835
|
+
FAIL=$((FAIL + 1))
|
|
2836
|
+
echo "FAIL: housekeeping does not announce a stack whose children all closed outside the router" >&2
|
|
2837
|
+
fi
|
|
2838
|
+
# A stack child runs the feature branch's copy of the workflows, so the router brings the default
|
|
2839
|
+
# branch in before it dispatches one.
|
|
2840
|
+
if grep -qF 'bring $DEFAULT_BRANCH into $STACK_BRANCH before a child runs' "${WORKFLOWS_DIR}/work-router.yml"; then
|
|
2841
|
+
PASS=$((PASS + 1))
|
|
2842
|
+
else
|
|
2843
|
+
FAIL=$((FAIL + 1))
|
|
2844
|
+
echo "FAIL: stack children run the feature branch's stale workflows" >&2
|
|
2845
|
+
fi
|
|
2846
|
+
# A stack root merges itself once every child has landed, through the same gate as any pull request.
|
|
2847
|
+
# The people it still stops are the gate's own: a blast radius over the limit, a protected path, a
|
|
2848
|
+
# finding. The gate must read the router's completion announcement, and the train must give the root
|
|
2849
|
+
# one more run after it.
|
|
2850
|
+
GATE_MD="${WORKFLOWS_DIR}/agent-merge-gate.md"
|
|
2851
|
+
TRAIN_SH="${HERE}/../merge-train/merge-train.sh"
|
|
2852
|
+
if worker_installed merge-gate && [ -f "$TRAIN_SH" ]; then
|
|
2853
|
+
if grep -qF 'root_complete=$root_complete' "$GATE_MD" && grep -qF "needs.subject.outputs.root_complete == 'true'" "$GATE_MD" && grep -qF 'STACK_ROOT_AUTOMERGE:' "$GATE_MD" && grep -qF 'STACK_ROOT_READY' "$TRAIN_SH"; then
|
|
2854
|
+
PASS=$((PASS + 1))
|
|
2855
|
+
else
|
|
2856
|
+
FAIL=$((FAIL + 1))
|
|
2857
|
+
echo "FAIL: a completed stack root is not merged by the gate and re-run by the train" >&2
|
|
2858
|
+
fi
|
|
2859
|
+
fi
|
|
2741
2860
|
# The merge train wakes on a finished CI run only. Pliny-Bot had four green pull requests wait an hour
|
|
2742
2861
|
# because a repair gate dispatched for red CI kept them marked as having a gate, and nothing called the
|
|
2743
2862
|
# train again once it was cancelled. The hourly sweep is now a heartbeat, and a repair gate whose CI has
|
package/loops/docs/diagrams.md
CHANGED
|
@@ -184,9 +184,10 @@ flowchart TD
|
|
|
184
184
|
|
|
185
185
|
The stack exists only in markers on the issues — `stack-root-branch` and `stack-root-pr` on the
|
|
186
186
|
feature issue, `stack-root-branch` stamped on each child at hand-over — so a repository with none
|
|
187
|
-
of them runs exactly the pipeline above and nothing else. The root is
|
|
188
|
-
|
|
189
|
-
|
|
187
|
+
of them runs exactly the pipeline above and nothing else. The root is held while children land.
|
|
188
|
+
Once every child has landed and the feature branch is green, the gate decides it like any pull
|
|
189
|
+
request and merges it, unless the blast radius, a protected path or a finding sends it to a person
|
|
190
|
+
(`STACK_ROOT_AUTOMERGE: "false"` restores a human merge for every root).
|
|
190
191
|
|
|
191
192
|
---
|
|
192
193
|
|
|
@@ -385,7 +386,7 @@ flowchart TD
|
|
|
385
386
|
gateDisposition -.->|"fragile, unmet criteria<br/>or low confidence"| gateHuman
|
|
386
387
|
gateDisposition -.->|"CI red or a verified<br/>high finding"| gateBlocked
|
|
387
388
|
gateMerge(("AUTO-MERGE<br/>squash into the PR's base<br/>(main, or the feature branch:<br/>then the child issue closes,<br/>pr-pending removed)"))
|
|
388
|
-
gateRootCheck -->|"yes: the root"| gateHeld(("HELD<br/>stack root
|
|
389
|
+
gateRootCheck -->|"yes: the root"| gateHeld(("HELD<br/>stack root waits for its children,<br/>then merges like any pull request<br/>unless the gate sends it to a person"))
|
|
389
390
|
gateHuman(("HUMAN REVIEW<br/>review label,<br/>implement label kept"))
|
|
390
391
|
gateOwner(("OWNER REVIEW<br/>review + owner-review,<br/>CODEOWNERS asked when named"))
|
|
391
392
|
gateHuman --> gatePreview
|
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
const OPENCODE_VERSION = '2.0.22'
|
|
4
4
|
const OPENCODE_PACKAGE = '@opencode/cli'
|
|
5
5
|
|
|
6
|
+
const OPENCODE_NUDGE =
|
|
7
|
+
'; rc=$?; for nudge in 1 2; do [ -s "${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" ] && break; ' +
|
|
8
|
+
'echo "No safe output yet, continuing the session ($nudge of 2)"; ' +
|
|
9
|
+
'OPENCODE_DISABLE_MODELS_FETCH=1 opencode run --continue --print-logs --log-level info ' +
|
|
10
|
+
'"Your last message ended without a tool call, so nothing has been delivered and this run is about to end. ' +
|
|
11
|
+
'Do not explain or plan: continue the task now and finish with the safe-output call the task names."; ' +
|
|
12
|
+
'rc=$?; done; exit $rc'
|
|
13
|
+
|
|
6
14
|
// The install the compiled locks carry, shared so both rewrite sites emit exactly
|
|
7
15
|
// the same command.
|
|
8
16
|
//
|
|
@@ -153,6 +161,11 @@ for (const file of readdirSync(workflowDirectory)) {
|
|
|
153
161
|
// "Invalid value for flag --log-level", aborting before the agent runs.
|
|
154
162
|
// The old upper-case spelling was valid in v1 and is not in v2.
|
|
155
163
|
.replace(/opencode run(?: --print-logs)? --log-level (?:DEBUG|ERROR|INFO|debug|error|info)/g, "OPENCODE_DISABLE_MODELS_FETCH=1 opencode run --print-logs --log-level info")
|
|
164
|
+
// A turn that ends in text ends `opencode run`, whatever the text promised, and no prompt rule
|
|
165
|
+
// stopped glm-5-2 doing it: Pliny-Bot #771 passed verify, wrote "let me discover the exact
|
|
166
|
+
// safeoutputs command" and exited with nothing delivered. When no safe output exists yet, the
|
|
167
|
+
// same session is continued, twice at most, and told to finish.
|
|
168
|
+
.replace(/("\$\(cat \/tmp\/gh-aw\/aw-prompts\/prompt\.txt\)")'/g, (_, prompt) => prompt + OPENCODE_NUDGE + "'")
|
|
156
169
|
.replace(/GH_AW_INFO_MODEL: "[^"]*"/g, 'GH_AW_INFO_MODEL: "per-agent"')
|
|
157
170
|
.replace(/OPENCODE_MODEL: [^\n]+/g, "OPENCODE_MODEL: ''")
|
|
158
171
|
.replace(/GH_AW_INFO_MODEL_COSTS: '[^']*'/g, 'GH_AW_INFO_MODEL_COSTS: \'{"providers":{}}\'')
|
|
@@ -6,9 +6,12 @@ env:
|
|
|
6
6
|
# so it names its model here. `model:` below only satisfies the compiler. A repository may
|
|
7
7
|
# change it to any model its opencode.ci.json declares.
|
|
8
8
|
AGENT_LEAD_MODEL: "forge/glm-5-2"
|
|
9
|
-
#
|
|
10
|
-
#
|
|
9
|
+
# Every harness tier (plan, build, fast) on one model the CI's Forge account is granted.
|
|
10
|
+
# harness.json names quasar-438b and carina-60b, which that account is not granted: Pliny-Bot
|
|
11
|
+
# #823's build tasks failed on quasar. Empty runs the tiers harness.json names.
|
|
11
12
|
AGENT_TIER_MODEL: "plainconcepts/glm-5-2"
|
|
13
|
+
# The fast tier alone, while the CI's Forge account is not granted carina-60b.
|
|
14
|
+
AGENT_FAST_MODEL: "plainconcepts/glm-5-2"
|
|
12
15
|
# Printed by the prompt as the verification block. Consumers set their own commands.
|
|
13
16
|
VERIFY_COMMANDS: ""
|
|
14
17
|
REPO_RULES: "Apply only actionable outstanding reviewer feedback to the selected bot pull request. Make minimal changes that address each comment. Preserve architecture and do not weaken tests. Run full verification after changes."
|
|
@@ -381,7 +384,7 @@ jobs:
|
|
|
381
384
|
issue-number: ${{ needs.subject.outputs.issue }}
|
|
382
385
|
labels: ${{ env.WORKING_LABEL }}
|
|
383
386
|
- name: Flag for human review
|
|
384
|
-
if: steps.decide.outputs.retry != 'true'
|
|
387
|
+
if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled'
|
|
385
388
|
uses: ./.github/actions/add-issue-labels
|
|
386
389
|
with:
|
|
387
390
|
token: ${{ steps.app-token.outputs.token }}
|
|
@@ -390,7 +393,7 @@ jobs:
|
|
|
390
393
|
${{ env.REVIEW_LABEL }}
|
|
391
394
|
${{ env.STALLED_LABEL }}
|
|
392
395
|
- name: Report missing review feedback outcome
|
|
393
|
-
if: steps.decide.outputs.retry != 'true'
|
|
396
|
+
if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled'
|
|
394
397
|
uses: ./.github/actions/create-issue-comment
|
|
395
398
|
with:
|
|
396
399
|
token: ${{ steps.app-token.outputs.token }}
|
|
@@ -499,8 +502,8 @@ safe-outputs:
|
|
|
499
502
|
# 240 went on to every worker at once when the provider was slow, which fixed the deaths and
|
|
500
503
|
# made every worker equally expensive to hang. These numbers are per worker: enough headroom
|
|
501
504
|
# for a slow gateway on the work it actually does, and not four hours for a run that reads one
|
|
502
|
-
# issue. Turns remain the guard against a confused agent looping;
|
|
503
|
-
#
|
|
505
|
+
# issue. Turns remain the guard against a confused agent looping; the credit ceiling meters the
|
|
506
|
+
# Forge models at Multiverse's list prices, set in shared/opencode-ci.md.
|
|
504
507
|
#
|
|
505
508
|
# Applies review comments to an existing branch and verifies the result.
|
|
506
509
|
timeout-minutes: 90
|
|
@@ -6,9 +6,12 @@ env:
|
|
|
6
6
|
# so it names its model here. `model:` below only satisfies the compiler. A repository may
|
|
7
7
|
# change it to any model its opencode.ci.json declares.
|
|
8
8
|
AGENT_LEAD_MODEL: "forge/glm-5-2"
|
|
9
|
-
#
|
|
10
|
-
#
|
|
9
|
+
# Every harness tier (plan, build, fast) on one model the CI's Forge account is granted.
|
|
10
|
+
# harness.json names quasar-438b and carina-60b, which that account is not granted: Pliny-Bot
|
|
11
|
+
# #823's build tasks failed on quasar. Empty runs the tiers harness.json names.
|
|
11
12
|
AGENT_TIER_MODEL: "plainconcepts/glm-5-2"
|
|
13
|
+
# The fast tier alone, while the CI's Forge account is not granted carina-60b.
|
|
14
|
+
AGENT_FAST_MODEL: "plainconcepts/glm-5-2"
|
|
12
15
|
REPO_RULES: "Read-only repository audit. Report only reproducible, actionable defects with evidence. Do not modify files, commit, push, or run write operations."
|
|
13
16
|
# Split out of REPO_RULES, which carried both the read-only discipline above and the list
|
|
14
17
|
# below. All four consuming repositories had customised the list and none could touch it
|
|
@@ -175,8 +178,8 @@ safe-outputs:
|
|
|
175
178
|
# 240 went on to every worker at once when the provider was slow, which fixed the deaths and
|
|
176
179
|
# made every worker equally expensive to hang. These numbers are per worker: enough headroom
|
|
177
180
|
# for a slow gateway on the work it actually does, and not four hours for a run that reads one
|
|
178
|
-
# issue. Turns remain the guard against a confused agent looping;
|
|
179
|
-
#
|
|
181
|
+
# issue. Turns remain the guard against a confused agent looping; the credit ceiling meters the
|
|
182
|
+
# Forge models at Multiverse's list prices, set in shared/opencode-ci.md.
|
|
180
183
|
#
|
|
181
184
|
# Sweeps the repository read-only and writes one findings issue. Observed around 45 minutes.
|
|
182
185
|
timeout-minutes: 90
|