@plainconceptsplatform/workflows 0.4.34 → 0.6.1
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/README.md +88 -88
- package/dist/action-validation.test.js +5 -2
- package/dist/catalog-installation.js +50 -7
- package/dist/catalog-installation.test.js +59 -22
- package/dist/catalog-listing.test.js +4 -4
- package/dist/index.js +48 -48
- package/dist/index.test.js +9 -9
- package/dist/repository-inspection.test.js +2 -2
- package/dist/route-processing.js +24 -25
- package/dist/route-processing.test.js +166 -233
- package/dist/stack-defaults.js +14 -14
- package/dist/stack-defaults.test.js +79 -79
- package/dist/tui.test.js +10 -7
- package/dist/workflow-catalog.d.ts +1 -1
- package/dist/workflow-catalog.js +1 -5
- package/loops/actions/add-issue-labels/action.yml +2 -2
- package/loops/actions/agent-output.cjs +17 -17
- package/loops/actions/apply-agent-bundle/apply-bundle.sh +14 -1
- package/loops/actions/audit-close/action.yml +24 -0
- package/loops/actions/classify-route/action.yml +8 -3
- package/loops/actions/classify-route/classify-route.sh +58 -38
- package/loops/actions/cleanup-artifacts/action.yml +38 -12
- package/loops/actions/identify-gate-subject/action.yml +3 -1
- package/loops/actions/remove-issue-labels/action.yml +4 -2
- package/loops/actions/update-changelog/action.yml +113 -0
- package/loops/actions/validate-merge-gate-output/action.yml +40 -40
- package/loops/actions/validate-refine-output/validate-refine-output.sh +34 -5
- package/loops/actions/validate-review-output/action.yml +35 -35
- package/loops/actions/validate-triage-output/action.yml +36 -36
- package/loops/actions/verify-refine-output/verify-refine-output.sh +20 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +375 -36
- package/loops/scripts/compile-agent-workflows.mjs +290 -6
- package/loops/scripts/merge-changelog.mjs +76 -0
- package/loops/templates/agentics/actionlint.yaml +13 -0
- package/loops/templates/agentics/agentics-checks.yml +125 -8
- package/loops/templates/agentics/agentics-maintenance.yml +121 -121
- package/loops/templates/ci/app-ci-dotnet-next.yml +330 -171
- package/loops/templates/ci/app-ci-node-monorepo.yml +260 -178
- package/loops/templates/issues/bug_report.yml +109 -109
- package/loops/templates/issues/feature_request.yml +75 -75
- package/loops/templates/opencode/opencode.ci.json +49 -47
- package/loops/templates/opencode/opencode.ci.json.md +49 -41
- package/loops/templates/release/github-release.yml +30 -30
- package/loops/workflows/agent-apply-review.md +469 -443
- package/loops/workflows/agent-audit.md +213 -197
- package/loops/workflows/agent-implement.md +640 -414
- package/loops/workflows/agent-merge-gate.md +844 -584
- package/loops/workflows/agent-refine.md +633 -418
- package/loops/workflows/agent-release.md +258 -0
- package/loops/workflows/agent-triage.md +447 -439
- package/loops/workflows/authorize-bot-work.yml +85 -82
- package/loops/workflows/shared/opencode-ci.md +206 -197
- package/loops/workflows/shared/platform-defaults.md +19 -16
- package/loops/workflows/work-router.yml +1038 -632
- package/package.json +7 -8
- package/dist/repository-state.d.ts +0 -18
- package/dist/repository-state.js +0 -77
- package/dist/repository-state.test.d.ts +0 -1
- package/dist/repository-state.test.js +0 -96
- package/loops/workflows/agent-direct.md +0 -375
- package/loops/workflows/agent-propose.md +0 -342
|
@@ -36,17 +36,32 @@ runs:
|
|
|
36
36
|
const failed = [];
|
|
37
37
|
|
|
38
38
|
for (const artifact of expired) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
// GitHub answers a delete with a 5xx now and then. Three tries, because the
|
|
40
|
+
// alternative is a red daily run over one artifact that tomorrow's run would
|
|
41
|
+
// have removed anyway.
|
|
42
|
+
let lastError;
|
|
43
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
44
|
+
try {
|
|
45
|
+
await github.rest.actions.deleteArtifact({
|
|
46
|
+
...context.repo,
|
|
47
|
+
artifact_id: artifact.id,
|
|
48
|
+
});
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
core.info(`Deleted ${artifact.name} (created ${artifact.created_at})`);
|
|
51
|
+
deleted += 1;
|
|
52
|
+
lastError = undefined;
|
|
53
|
+
break;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
lastError = error;
|
|
56
|
+
if (attempt < 3) await new Promise((wait) => setTimeout(wait, attempt * 2000));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (lastError) {
|
|
61
|
+
core.warning(
|
|
62
|
+
`Could not delete ${artifact.name} (${artifact.id}) after 3 tries: ${lastError.message}`,
|
|
63
|
+
);
|
|
64
|
+
failed.push({ name: artifact.name, status: lastError.status });
|
|
50
65
|
}
|
|
51
66
|
}
|
|
52
67
|
|
|
@@ -60,6 +75,17 @@ runs:
|
|
|
60
75
|
])
|
|
61
76
|
.write();
|
|
62
77
|
|
|
63
|
-
|
|
64
|
-
|
|
78
|
+
// Deleting nothing at all while expired artifacts exist is a permission or
|
|
79
|
+
// configuration problem, and has to be loud. One stubborn artifact among many is
|
|
80
|
+
// the API having a bad minute: tomorrow's run picks it up, and failing here only
|
|
81
|
+
// teaches people to ignore a red cleanup. A 4xx is never transient, so it fails
|
|
82
|
+
// whatever else succeeded.
|
|
83
|
+
const names = failed.map((entry) => entry.name).join(', ');
|
|
84
|
+
const structural = failed.some((entry) => entry.status && entry.status < 500);
|
|
85
|
+
if (failed.length > 0 && (deleted === 0 || structural)) {
|
|
86
|
+
core.setFailed(`${failed.length} artifact(s) could not be deleted: ${names}`);
|
|
87
|
+
} else if (failed.length > 0) {
|
|
88
|
+
core.warning(
|
|
89
|
+
`${failed.length} artifact(s) survived this run and will be retried tomorrow: ${names}`,
|
|
90
|
+
);
|
|
65
91
|
}
|
|
@@ -96,8 +96,10 @@ runs:
|
|
|
96
96
|
ci_run_id=""
|
|
97
97
|
if [ -z "$ci_conclusion" ]; then
|
|
98
98
|
head_sha="$(gh pr view "$PR_NUMBER" --repo "$REPO" --json headRefOid --jq '.headRefOid')"
|
|
99
|
+
# A run keeps the name it had when it ran, so a repository whose CI was renamed still
|
|
100
|
+
# carries runs under the old casing. Fold the name before comparing.
|
|
99
101
|
ci_run="$(gh api "repos/$REPO/actions/runs?head_sha=$head_sha&per_page=100" \
|
|
100
|
-
--jq '[.workflow_runs[] | select(.name == "
|
|
102
|
+
--jq '[.workflow_runs[] | select((.name | ascii_downcase) == "app: ci" or (.name | ascii_downcase) == "ci")] | sort_by(.created_at) | last')"
|
|
101
103
|
|
|
102
104
|
if [ -n "$ci_run" ] && [ "$ci_run" != "null" ]; then
|
|
103
105
|
ci_conclusion="$(jq -r '.conclusion // "pending"' <<<"$ci_run")"
|
|
@@ -9,7 +9,7 @@ inputs:
|
|
|
9
9
|
description: Issue or pull request number.
|
|
10
10
|
required: true
|
|
11
11
|
labels:
|
|
12
|
-
description:
|
|
12
|
+
description: Labels to remove, one per line. A comma-separated list is accepted as well.
|
|
13
13
|
required: true
|
|
14
14
|
runs:
|
|
15
15
|
using: composite
|
|
@@ -22,7 +22,9 @@ runs:
|
|
|
22
22
|
with:
|
|
23
23
|
github-token: ${{ inputs.token }}
|
|
24
24
|
script: |
|
|
25
|
-
|
|
25
|
+
// Callers wrote `labels: a,b` while this split on newlines, so one label named "a,b"
|
|
26
|
+
// was removed: a 404, swallowed below, and neither label ever came off.
|
|
27
|
+
for (const label of process.env.LABELS.split(/\r?\n|,/).map((label) => label.trim()).filter(Boolean)) {
|
|
26
28
|
try {
|
|
27
29
|
await github.rest.issues.removeLabel({
|
|
28
30
|
...context.repo,
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/update-changelog/action.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
+
name: Update changelog
|
|
3
|
+
description: Prepend a changelog entry for a landed issue and push it to the default branch.
|
|
4
|
+
|
|
5
|
+
inputs:
|
|
6
|
+
token:
|
|
7
|
+
description: Token with contents write on the repository.
|
|
8
|
+
required: true
|
|
9
|
+
issue-number:
|
|
10
|
+
description: Issue the landed commit implements.
|
|
11
|
+
required: true
|
|
12
|
+
commit-sha:
|
|
13
|
+
description: Commit the work landed as. Its subject becomes the summary.
|
|
14
|
+
required: true
|
|
15
|
+
changelog-path:
|
|
16
|
+
description: Path to changelog.json.
|
|
17
|
+
required: false
|
|
18
|
+
default: apps/web/src/shared/data/changelog.json
|
|
19
|
+
max-entries:
|
|
20
|
+
description: Newest entries to keep.
|
|
21
|
+
required: false
|
|
22
|
+
default: "20"
|
|
23
|
+
|
|
24
|
+
runs:
|
|
25
|
+
using: composite
|
|
26
|
+
steps:
|
|
27
|
+
- name: Prepend the entry and push
|
|
28
|
+
shell: bash
|
|
29
|
+
env:
|
|
30
|
+
GH_TOKEN: ${{ inputs.token }}
|
|
31
|
+
REPO: ${{ github.repository }}
|
|
32
|
+
ISSUE_NUMBER: ${{ inputs.issue-number }}
|
|
33
|
+
COMMIT_SHA: ${{ inputs.commit-sha }}
|
|
34
|
+
CHANGELOG_PATH: ${{ inputs.changelog-path }}
|
|
35
|
+
MAX_ENTRIES: ${{ inputs.max-entries }}
|
|
36
|
+
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
|
37
|
+
run: |
|
|
38
|
+
set -euo pipefail
|
|
39
|
+
|
|
40
|
+
# This used to be part of the implement prompt. It is deterministic work, and leaving it
|
|
41
|
+
# to the agent made every implement touch the same file: two runs whose branches were cut
|
|
42
|
+
# before the other merged conflicted here, produced correct code, and still failed to open
|
|
43
|
+
# a pull request. Doing it after the merge means exactly one writer, always against a
|
|
44
|
+
# current default branch.
|
|
45
|
+
|
|
46
|
+
host="${GITHUB_SERVER_URL#https://}"
|
|
47
|
+
git config user.name "github-actions[bot]"
|
|
48
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
49
|
+
git remote set-url origin "https://x-access-token:${GH_TOKEN}@${host}/${GITHUB_REPOSITORY}.git"
|
|
50
|
+
|
|
51
|
+
git fetch --quiet origin "$DEFAULT_BRANCH"
|
|
52
|
+
git checkout --quiet -B "$DEFAULT_BRANCH" "origin/$DEFAULT_BRANCH"
|
|
53
|
+
|
|
54
|
+
if [ ! -f "$CHANGELOG_PATH" ]; then
|
|
55
|
+
echo "::notice::$CHANGELOG_PATH does not exist; nothing to update."
|
|
56
|
+
exit 0
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Idempotent: a re-run, a retry, or a second call for the same commit must not add a
|
|
60
|
+
# duplicate row.
|
|
61
|
+
short_sha="${COMMIT_SHA:0:7}"
|
|
62
|
+
if jq -e --arg c "$short_sha" '.changes[]? | select(.commit == $c)' "$CHANGELOG_PATH" >/dev/null 2>&1; then
|
|
63
|
+
echo "::notice::$short_sha is already in the changelog."
|
|
64
|
+
exit 0
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
title=$(gh issue view "$ISSUE_NUMBER" --repo "$REPO" --json title --jq '.title')
|
|
68
|
+
|
|
69
|
+
# The commit subject carries what actually changed. Strip the conventional-commit prefix
|
|
70
|
+
# and the trailing issue reference so the summary reads as prose rather than as a commit.
|
|
71
|
+
subject=$(git log -1 --format=%s "$COMMIT_SHA")
|
|
72
|
+
summary=$(printf '%s' "$subject" \
|
|
73
|
+
| sed -E 's/^[a-z]+(\([^)]*\))?!?: *//' \
|
|
74
|
+
| sed -E 's/ *\(#[0-9]+\) *$//')
|
|
75
|
+
[ -n "$summary" ] || summary="$title"
|
|
76
|
+
# Capitalise, because a commit subject conventionally starts lowercase and this is read
|
|
77
|
+
# by people rather than by tooling.
|
|
78
|
+
summary="$(printf '%s' "${summary:0:1}" | tr '[:lower:]' '[:upper:]')${summary:1}"
|
|
79
|
+
|
|
80
|
+
timestamp=$(git log -1 --format=%cI "$COMMIT_SHA")
|
|
81
|
+
|
|
82
|
+
tmp=$(mktemp)
|
|
83
|
+
jq --argjson n "$MAX_ENTRIES" \
|
|
84
|
+
--arg ts "$timestamp" \
|
|
85
|
+
--argjson issue "$ISSUE_NUMBER" \
|
|
86
|
+
--arg title "$title" \
|
|
87
|
+
--arg summary "$summary" \
|
|
88
|
+
--arg commit "$short_sha" \
|
|
89
|
+
'.changes = ([{timestamp: $ts, issue: $issue, title: $title, summary: $summary, commit: $commit}] + (.changes // []))[:$n]' \
|
|
90
|
+
"$CHANGELOG_PATH" > "$tmp"
|
|
91
|
+
mv "$tmp" "$CHANGELOG_PATH"
|
|
92
|
+
|
|
93
|
+
if git diff --quiet -- "$CHANGELOG_PATH"; then
|
|
94
|
+
echo "::notice::changelog unchanged."
|
|
95
|
+
exit 0
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
git add -- "$CHANGELOG_PATH"
|
|
99
|
+
git commit --quiet -m "docs(changelog): record #${ISSUE_NUMBER}"
|
|
100
|
+
|
|
101
|
+
# The default branch may have moved while this ran. Rebase and retry rather than fail:
|
|
102
|
+
# only this action writes the file, so a rebase here cannot produce a content conflict.
|
|
103
|
+
for attempt in 1 2 3; do
|
|
104
|
+
if git push --quiet origin "$DEFAULT_BRANCH"; then
|
|
105
|
+
echo "Recorded #${ISSUE_NUMBER} at ${short_sha} in the changelog."
|
|
106
|
+
exit 0
|
|
107
|
+
fi
|
|
108
|
+
echo "::notice::push rejected, rebasing (attempt ${attempt})"
|
|
109
|
+
git fetch --quiet origin "$DEFAULT_BRANCH"
|
|
110
|
+
git rebase --quiet "origin/$DEFAULT_BRANCH" || { git rebase --abort || true; break; }
|
|
111
|
+
done
|
|
112
|
+
|
|
113
|
+
echo "::warning::Could not push the changelog entry for #${ISSUE_NUMBER}."
|
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/validate-merge-gate-output/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
-
name: Validate merge-gate output
|
|
3
|
-
description: Verify agent_output.json contains a usable merge-gate verdict before any state changes.
|
|
4
|
-
inputs:
|
|
5
|
-
output-file:
|
|
6
|
-
description: Path to agent_output.json.
|
|
7
|
-
required: true
|
|
8
|
-
issue-number:
|
|
9
|
-
description: Issue number that the merge-gate outcome must target.
|
|
10
|
-
required: true
|
|
11
|
-
ci-conclusion:
|
|
12
|
-
description: CI conclusion supplied to the merge gate.
|
|
13
|
-
required: true
|
|
14
|
-
outputs:
|
|
15
|
-
valid:
|
|
16
|
-
description: Whether the output contains a usable merge-gate comment with a verdict.
|
|
17
|
-
value: ${{ steps.validate.outputs.valid }}
|
|
18
|
-
outcome:
|
|
19
|
-
description: "Deterministic outcome: merge, review, remediated, or invalid."
|
|
20
|
-
value: ${{ steps.validate.outputs.outcome }}
|
|
21
|
-
runs:
|
|
22
|
-
using: composite
|
|
23
|
-
steps:
|
|
24
|
-
- name: Validate merge-gate outcome
|
|
25
|
-
id: validate
|
|
26
|
-
shell: bash
|
|
27
|
-
env:
|
|
28
|
-
ISSUE_NUMBER: ${{ inputs.issue-number }}
|
|
29
|
-
CI_CONCLUSION: ${{ inputs.ci-conclusion }}
|
|
30
|
-
OUTPUT_FILE: ${{ inputs.output-file }}
|
|
31
|
-
run: |
|
|
32
|
-
set -euo pipefail
|
|
33
|
-
|
|
34
|
-
outcome="$(bash "${{ github.action_path }}/validate-merge-gate-output.sh" "$OUTPUT_FILE" "$ISSUE_NUMBER" "$CI_CONCLUSION")"
|
|
35
|
-
echo "outcome=$outcome" >> "$GITHUB_OUTPUT"
|
|
36
|
-
echo "valid=$([ "$outcome" != 'invalid' ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
|
|
37
|
-
|
|
38
|
-
if [ "$outcome" = 'invalid' ]; then
|
|
39
|
-
echo "::warning::Agent output has no usable merge-gate outcome. It will not be applied."
|
|
40
|
-
fi
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/validate-merge-gate-output/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: Validate merge-gate output
|
|
3
|
+
description: Verify agent_output.json contains a usable merge-gate verdict before any state changes.
|
|
4
|
+
inputs:
|
|
5
|
+
output-file:
|
|
6
|
+
description: Path to agent_output.json.
|
|
7
|
+
required: true
|
|
8
|
+
issue-number:
|
|
9
|
+
description: Issue number that the merge-gate outcome must target.
|
|
10
|
+
required: true
|
|
11
|
+
ci-conclusion:
|
|
12
|
+
description: CI conclusion supplied to the merge gate.
|
|
13
|
+
required: true
|
|
14
|
+
outputs:
|
|
15
|
+
valid:
|
|
16
|
+
description: Whether the output contains a usable merge-gate comment with a verdict.
|
|
17
|
+
value: ${{ steps.validate.outputs.valid }}
|
|
18
|
+
outcome:
|
|
19
|
+
description: "Deterministic outcome: merge, review, remediated, or invalid."
|
|
20
|
+
value: ${{ steps.validate.outputs.outcome }}
|
|
21
|
+
runs:
|
|
22
|
+
using: composite
|
|
23
|
+
steps:
|
|
24
|
+
- name: Validate merge-gate outcome
|
|
25
|
+
id: validate
|
|
26
|
+
shell: bash
|
|
27
|
+
env:
|
|
28
|
+
ISSUE_NUMBER: ${{ inputs.issue-number }}
|
|
29
|
+
CI_CONCLUSION: ${{ inputs.ci-conclusion }}
|
|
30
|
+
OUTPUT_FILE: ${{ inputs.output-file }}
|
|
31
|
+
run: |
|
|
32
|
+
set -euo pipefail
|
|
33
|
+
|
|
34
|
+
outcome="$(bash "${{ github.action_path }}/validate-merge-gate-output.sh" "$OUTPUT_FILE" "$ISSUE_NUMBER" "$CI_CONCLUSION")"
|
|
35
|
+
echo "outcome=$outcome" >> "$GITHUB_OUTPUT"
|
|
36
|
+
echo "valid=$([ "$outcome" != 'invalid' ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
|
|
37
|
+
|
|
38
|
+
if [ "$outcome" = 'invalid' ]; then
|
|
39
|
+
echo "::warning::Agent output has no usable merge-gate outcome. It will not be applied."
|
|
40
|
+
fi
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/validate-refine-output/validate-refine-output.sh. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
3
|
-
# Print the deterministic Refine outcome: complete, questions, or invalid.
|
|
3
|
+
# Print the deterministic Refine outcome: complete, split, questions, or invalid.
|
|
4
4
|
|
|
5
5
|
set -euo pipefail
|
|
6
6
|
|
|
@@ -23,8 +23,24 @@ jq -r --arg marker "$marker" --arg prefix "$comment_prefix" --arg issue "$issue_
|
|
|
23
23
|
def has_update:
|
|
24
24
|
any(.items[]; .type == "update_issue");
|
|
25
25
|
|
|
26
|
+
# A split writes children, which are the only items allowed to target something other than
|
|
27
|
+
# the source issue: they do not exist yet, so they carry no number at all.
|
|
28
|
+
def child_count:
|
|
29
|
+
[.items[] | select(.type == "create_issue" and
|
|
30
|
+
(.body | type == "string") and (.body | test("[^[:space:]]")) and
|
|
31
|
+
(.title | type == "string") and (.title | test("[^[:space:]]")))] | length;
|
|
32
|
+
|
|
33
|
+
# Signals about the run rather than about the issue. A worker reports these alongside
|
|
34
|
+
# its real work, and they say nothing about where that work was aimed, so they are not
|
|
35
|
+
# counted when deciding whether a run stayed on its own issue. Counting them meant a run
|
|
36
|
+
# that refined the issue perfectly and also noted a difficulty was judged invalid and
|
|
37
|
+
# its work discarded.
|
|
38
|
+
def is_run_signal:
|
|
39
|
+
.type == "report_incomplete" or .type == "missing_tool"
|
|
40
|
+
or .type == "missing_data" or .type == "noop";
|
|
41
|
+
|
|
26
42
|
def has_only_source_items:
|
|
27
|
-
all(.items[];
|
|
43
|
+
all(.items[] | select(is_run_signal | not);
|
|
28
44
|
(
|
|
29
45
|
.type == "update_issue" and
|
|
30
46
|
(.item_number == null or (.item_number | tostring) == $issue)
|
|
@@ -32,7 +48,14 @@ jq -r --arg marker "$marker" --arg prefix "$comment_prefix" --arg issue "$issue_
|
|
|
32
48
|
(
|
|
33
49
|
.type == "add_comment" and
|
|
34
50
|
(.item_number | tostring) == $issue
|
|
35
|
-
)
|
|
51
|
+
) or
|
|
52
|
+
.type == "create_issue");
|
|
53
|
+
|
|
54
|
+
# The agent saying it did not finish. Survivable when it left real work behind, never
|
|
55
|
+
# when all it left was a comment: a failed run must not be read as a question, or the
|
|
56
|
+
# loop waits for an answer to something nobody asked.
|
|
57
|
+
def reported_incomplete:
|
|
58
|
+
any(.items[]; .type == "report_incomplete");
|
|
36
59
|
|
|
37
60
|
def has_clarification:
|
|
38
61
|
any(.items[]; .type == "add_comment" and (.item_number | tostring) == $issue and
|
|
@@ -40,8 +63,14 @@ jq -r --arg marker "$marker" --arg prefix "$comment_prefix" --arg issue "$issue_
|
|
|
40
63
|
(.body | split($marker) | join("") | split($prefix) | join("") |
|
|
41
64
|
gsub("<[^>]*>"; "") | test("[[:alnum:]]")));
|
|
42
65
|
|
|
43
|
-
|
|
44
|
-
|
|
66
|
+
# Order matters: a run that wrote children is a split even though it also replaced the
|
|
67
|
+
# parent body, and a lone child with no parent update is an incomplete split, not a
|
|
68
|
+
# complete refinement.
|
|
69
|
+
if child_count >= 2 and has_replacement_body and has_only_source_items then "split"
|
|
70
|
+
elif child_count > 0 then "invalid"
|
|
71
|
+
elif has_replacement_body and has_only_source_items then "complete"
|
|
72
|
+
elif has_clarification and (has_update | not) and has_only_source_items
|
|
73
|
+
and (reported_incomplete | not) then "questions"
|
|
45
74
|
else "invalid"
|
|
46
75
|
end
|
|
47
76
|
' "$output_file"
|
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/validate-review-output/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
-
name: Validate review output
|
|
3
|
-
description: Verify an apply-review outcome accounts for every unresolved review thread.
|
|
4
|
-
inputs:
|
|
5
|
-
output-file:
|
|
6
|
-
description: Path to agent_output.json.
|
|
7
|
-
required: true
|
|
8
|
-
pr-number:
|
|
9
|
-
description: Pull request receiving the review feedback.
|
|
10
|
-
required: true
|
|
11
|
-
review-threads-file:
|
|
12
|
-
description: JSON file containing unresolved review threads.
|
|
13
|
-
required: true
|
|
14
|
-
outputs:
|
|
15
|
-
valid:
|
|
16
|
-
description: Whether the agent output is a valid review outcome.
|
|
17
|
-
value: ${{ steps.validate.outputs.valid }}
|
|
18
|
-
outcome:
|
|
19
|
-
description: "Deterministic review outcome: implemented, already-satisfied, needs-human, or invalid."
|
|
20
|
-
value: ${{ steps.validate.outputs.outcome }}
|
|
21
|
-
runs:
|
|
22
|
-
using: composite
|
|
23
|
-
steps:
|
|
24
|
-
- name: Validate review outcome
|
|
25
|
-
id: validate
|
|
26
|
-
shell: bash
|
|
27
|
-
env:
|
|
28
|
-
OUTPUT_FILE: ${{ inputs.output-file }}
|
|
29
|
-
PR_NUMBER: ${{ inputs.pr-number }}
|
|
30
|
-
REVIEW_THREADS_FILE: ${{ inputs.review-threads-file }}
|
|
31
|
-
run: |
|
|
32
|
-
set -euo pipefail
|
|
33
|
-
outcome="$(bash "${{ github.action_path }}/validate-review-output.sh" "$OUTPUT_FILE" "$PR_NUMBER" "$REVIEW_THREADS_FILE")"
|
|
34
|
-
echo "outcome=$outcome" >> "$GITHUB_OUTPUT"
|
|
35
|
-
echo "valid=$([ "$outcome" != 'invalid' ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/validate-review-output/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: Validate review output
|
|
3
|
+
description: Verify an apply-review outcome accounts for every unresolved review thread.
|
|
4
|
+
inputs:
|
|
5
|
+
output-file:
|
|
6
|
+
description: Path to agent_output.json.
|
|
7
|
+
required: true
|
|
8
|
+
pr-number:
|
|
9
|
+
description: Pull request receiving the review feedback.
|
|
10
|
+
required: true
|
|
11
|
+
review-threads-file:
|
|
12
|
+
description: JSON file containing unresolved review threads.
|
|
13
|
+
required: true
|
|
14
|
+
outputs:
|
|
15
|
+
valid:
|
|
16
|
+
description: Whether the agent output is a valid review outcome.
|
|
17
|
+
value: ${{ steps.validate.outputs.valid }}
|
|
18
|
+
outcome:
|
|
19
|
+
description: "Deterministic review outcome: implemented, already-satisfied, needs-human, or invalid."
|
|
20
|
+
value: ${{ steps.validate.outputs.outcome }}
|
|
21
|
+
runs:
|
|
22
|
+
using: composite
|
|
23
|
+
steps:
|
|
24
|
+
- name: Validate review outcome
|
|
25
|
+
id: validate
|
|
26
|
+
shell: bash
|
|
27
|
+
env:
|
|
28
|
+
OUTPUT_FILE: ${{ inputs.output-file }}
|
|
29
|
+
PR_NUMBER: ${{ inputs.pr-number }}
|
|
30
|
+
REVIEW_THREADS_FILE: ${{ inputs.review-threads-file }}
|
|
31
|
+
run: |
|
|
32
|
+
set -euo pipefail
|
|
33
|
+
outcome="$(bash "${{ github.action_path }}/validate-review-output.sh" "$OUTPUT_FILE" "$PR_NUMBER" "$REVIEW_THREADS_FILE")"
|
|
34
|
+
echo "outcome=$outcome" >> "$GITHUB_OUTPUT"
|
|
35
|
+
echo "valid=$([ "$outcome" != 'invalid' ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
|
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/validate-triage-output/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
-
name: Validate triage output
|
|
3
|
-
description: Verify agent_output.json contains a usable triage verdict before any GitHub writes.
|
|
4
|
-
inputs:
|
|
5
|
-
output-file:
|
|
6
|
-
description: Path to agent_output.json.
|
|
7
|
-
required: true
|
|
8
|
-
issue-number:
|
|
9
|
-
description: Issue number that every triage outcome must target.
|
|
10
|
-
required: true
|
|
11
|
-
outputs:
|
|
12
|
-
valid:
|
|
13
|
-
description: Whether the output contains a usable triage comment with a verdict.
|
|
14
|
-
value: ${{ steps.validate.outputs.valid }}
|
|
15
|
-
outcome:
|
|
16
|
-
description: "Deterministic triage outcome: pass, needs-info, block, or invalid."
|
|
17
|
-
value: ${{ steps.validate.outputs.outcome }}
|
|
18
|
-
runs:
|
|
19
|
-
using: composite
|
|
20
|
-
steps:
|
|
21
|
-
- name: Validate triage outcome
|
|
22
|
-
id: validate
|
|
23
|
-
shell: bash
|
|
24
|
-
env:
|
|
25
|
-
ISSUE_NUMBER: ${{ inputs.issue-number }}
|
|
26
|
-
OUTPUT_FILE: ${{ inputs.output-file }}
|
|
27
|
-
run: |
|
|
28
|
-
set -euo pipefail
|
|
29
|
-
|
|
30
|
-
outcome="$(bash "${{ github.action_path }}/validate-triage-output.sh" "$OUTPUT_FILE" "$ISSUE_NUMBER")"
|
|
31
|
-
echo "outcome=$outcome" >> "$GITHUB_OUTPUT"
|
|
32
|
-
echo "valid=$([ "$outcome" != 'invalid' ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
|
|
33
|
-
|
|
34
|
-
if [ "$outcome" = 'invalid' ]; then
|
|
35
|
-
echo "::warning::Agent output has no usable triage outcome. It will not be applied."
|
|
36
|
-
fi
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/validate-triage-output/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: Validate triage output
|
|
3
|
+
description: Verify agent_output.json contains a usable triage verdict before any GitHub writes.
|
|
4
|
+
inputs:
|
|
5
|
+
output-file:
|
|
6
|
+
description: Path to agent_output.json.
|
|
7
|
+
required: true
|
|
8
|
+
issue-number:
|
|
9
|
+
description: Issue number that every triage outcome must target.
|
|
10
|
+
required: true
|
|
11
|
+
outputs:
|
|
12
|
+
valid:
|
|
13
|
+
description: Whether the output contains a usable triage comment with a verdict.
|
|
14
|
+
value: ${{ steps.validate.outputs.valid }}
|
|
15
|
+
outcome:
|
|
16
|
+
description: "Deterministic triage outcome: pass, needs-info, block, or invalid."
|
|
17
|
+
value: ${{ steps.validate.outputs.outcome }}
|
|
18
|
+
runs:
|
|
19
|
+
using: composite
|
|
20
|
+
steps:
|
|
21
|
+
- name: Validate triage outcome
|
|
22
|
+
id: validate
|
|
23
|
+
shell: bash
|
|
24
|
+
env:
|
|
25
|
+
ISSUE_NUMBER: ${{ inputs.issue-number }}
|
|
26
|
+
OUTPUT_FILE: ${{ inputs.output-file }}
|
|
27
|
+
run: |
|
|
28
|
+
set -euo pipefail
|
|
29
|
+
|
|
30
|
+
outcome="$(bash "${{ github.action_path }}/validate-triage-output.sh" "$OUTPUT_FILE" "$ISSUE_NUMBER")"
|
|
31
|
+
echo "outcome=$outcome" >> "$GITHUB_OUTPUT"
|
|
32
|
+
echo "valid=$([ "$outcome" != 'invalid' ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
|
|
33
|
+
|
|
34
|
+
if [ "$outcome" = 'invalid' ]; then
|
|
35
|
+
echo "::warning::Agent output has no usable triage outcome. It will not be applied."
|
|
36
|
+
fi
|
|
@@ -56,6 +56,26 @@ assert_output 'wrong issue output is invalid' invalid \
|
|
|
56
56
|
assert_output 'complete output cannot update another issue' invalid \
|
|
57
57
|
'{"items":[{"type":"update_issue","item_number":42,"body":"# User story"},{"type":"update_issue","item_number":7,"body":"# Other story"}]}'
|
|
58
58
|
|
|
59
|
+
# From a real run. The agent's first update_issue went out malformed, the bridge counted it
|
|
60
|
+
# as spent, the retry carrying the body was refused, and the "Refinement complete" comment
|
|
61
|
+
# went out regardless. Invalid is the only safe reading: the body was never replaced. Note
|
|
62
|
+
# what it must not be read as — a question. Nobody asked one, so "questions" would leave the
|
|
63
|
+
# issue waiting on an answer that is never coming.
|
|
64
|
+
assert_output 'a completion claim with no body is invalid' invalid \
|
|
65
|
+
'{"items":[{"type":"add_comment","item_number":42,"body":"<!-- agent-refine -->\nRefinement update\nRefinement complete. The implement label has been added and the implement workflow will start shortly."},{"type":"report_incomplete","reason":"update_issue limit reached"}]}'
|
|
66
|
+
|
|
67
|
+
# The other side of it: a run that did replace the body and also reported a difficulty has
|
|
68
|
+
# done the work, and its work is not thrown away for having said so.
|
|
69
|
+
assert_output 'a refined body survives a reported difficulty' complete \
|
|
70
|
+
'{"items":[{"type":"update_issue","item_number":42,"body":"# User story"},{"type":"report_incomplete","reason":"a tool was slow"}]}'
|
|
71
|
+
assert_output 'a split survives a note about a missing tool' split \
|
|
72
|
+
'{"items":[{"type":"update_issue","item_number":42,"body":"# Epic"},{"type":"create_issue","title":"One","body":"First"},{"type":"create_issue","title":"Two","body":"Second"},{"type":"missing_tool","reason":"no browser"}]}'
|
|
73
|
+
|
|
74
|
+
# And a run that left nothing but a signal has done nothing.
|
|
75
|
+
assert_output 'only a run signal is invalid' invalid \
|
|
76
|
+
'{"items":[{"type":"report_incomplete","reason":"gave up"}]}'
|
|
77
|
+
|
|
78
|
+
|
|
59
79
|
echo
|
|
60
80
|
if [ "$FAIL" -eq 0 ]; then
|
|
61
81
|
echo "Refine output validation: ${PASS} passed"
|