@plainconceptsplatform/workflows 0.17.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/catalog-installation.js +25 -13
- package/dist/stack-defaults.js +16 -16
- package/dist/worker-env.js +24 -21
- package/loops/actions/add-issue-labels/action.yml +50 -50
- package/loops/actions/agent-output.cjs +17 -17
- package/loops/actions/apply-agent-bundle/action.yml +24 -24
- package/loops/actions/apply-agent-comments/action.yml +42 -42
- package/loops/actions/apply-agent-labels/action.yml +55 -55
- package/loops/actions/apply-agent-output/action.yml +108 -108
- package/loops/actions/assess-blast-radius/action.yml +148 -0
- package/loops/actions/assess-blast-radius/assess-blast-radius.sh +138 -0
- package/loops/actions/classify-route/action.yml +100 -100
- package/loops/actions/cleanup-artifacts/action.yml +91 -91
- package/loops/actions/close-agent-issues/action.yml +43 -43
- package/loops/actions/create-agent-issues/action.yml +52 -52
- package/loops/actions/create-issue-comment/action.yml +29 -29
- package/loops/actions/download-agent-output/action.yml +53 -53
- package/loops/actions/housekeeping/action.yml +55 -2
- package/loops/actions/identify-gate-subject/action.yml +134 -122
- package/loops/actions/link-pr-to-issue/action.yml +40 -40
- package/loops/actions/list-open-issues/action.yml +33 -33
- package/loops/actions/load-issue-context/action.yml +45 -45
- package/loops/actions/merge-agent-pr/action.yml +49 -49
- package/loops/actions/push-agent-branch/action.yml +45 -45
- package/loops/actions/remove-issue-labels/action.yml +37 -37
- package/loops/actions/require-open-issue/action.yml +59 -0
- package/loops/actions/update-agent-issues/action.yml +58 -58
- package/loops/actions/validate-merge-gate-output/action.yml +62 -40
- package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +147 -31
- package/loops/actions/validate-refine-output/action.yml +48 -44
- package/loops/actions/validate-refine-output/validate-refine-output.sh +15 -4
- package/loops/actions/validate-review-output/action.yml +35 -35
- package/loops/actions/validate-triage-output/action.yml +36 -36
- package/loops/actions/verify-composite-actions/action.yml +9 -9
- package/loops/actions/verify-refine-output/action.yml +9 -9
- package/loops/actions/verify-refine-output/verify-refine-output.sh +6 -1
- package/loops/actions/verify-route-matrix/action.yml +9 -9
- package/loops/actions/verify-route-matrix/verify-gate-metrics.mjs +51 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +630 -8
- package/loops/scripts/compile-agent-workflows.mjs +331 -331
- package/loops/templates/agentics/agentics-maintenance.yml +121 -121
- package/loops/templates/ci/app-ci-dotnet-next.yml +330 -330
- package/loops/templates/ci/app-ci-node-monorepo.yml +260 -260
- 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 +55 -49
- package/loops/templates/opencode/opencode.ci.json.md +59 -49
- package/loops/templates/release/github-release.yml +30 -30
- package/loops/workflows/agent-apply-review.md +33 -3
- package/loops/workflows/agent-audit.md +36 -0
- package/loops/workflows/agent-implement.md +77 -5
- package/loops/workflows/agent-merge-gate.md +368 -148
- package/loops/workflows/agent-refine.md +93 -17
- package/loops/workflows/agent-release.md +4 -4
- package/loops/workflows/agent-triage.md +33 -1
- package/loops/workflows/authorize-bot-work.yml +105 -105
- package/loops/workflows/shared/opencode-ci.md +206 -206
- package/loops/workflows/shared/platform-defaults.md +19 -19
- package/loops/workflows/work-router.yml +25 -11
- package/package.json +2 -2
|
@@ -1,46 +1,162 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
3
|
-
# Print the
|
|
3
|
+
# Print the merge-gate disposition: auto-merge, human-review, owner-review, blocked,
|
|
4
|
+
# remediated, or invalid.
|
|
5
|
+
#
|
|
6
|
+
# This file used to read one word out of the agent's prose and call it the decision. It now
|
|
7
|
+
# computes the decision from two sources that cannot be confused with each other: facts the
|
|
8
|
+
# workflow measured before the agent ran (CI, protected and owner paths, blast radius), and
|
|
9
|
+
# structured evidence the agent produced (verified findings, recoverability, confidence). The
|
|
10
|
+
# agent no longer names the outcome. It reports what it found; the rules below decide.
|
|
11
|
+
#
|
|
12
|
+
# The shape rules are strict on purpose. Every permissive default in here is a way for a
|
|
13
|
+
# malformed report to merge code nobody assessed, and the shapes that matter are near misses
|
|
14
|
+
# rather than nonsense: `"verified": "true"` as a string, a severity spelled `blocker`, an
|
|
15
|
+
# `acceptanceCriteriaMet` of `"false"`. Each of those read as the permissive value once. A
|
|
16
|
+
# report that does not match the contract is `invalid`, which parks the pull request; only a
|
|
17
|
+
# report that does match gets to decide anything.
|
|
18
|
+
#
|
|
19
|
+
# Usage:
|
|
20
|
+
# validate-merge-gate-output.sh OUTPUT_FILE ISSUE CI_CONCLUSION \
|
|
21
|
+
# BLAST_LEVEL PROTECTED_HIT OWNER_HIT CONFIDENCE_THRESHOLD
|
|
4
22
|
|
|
5
23
|
set -euo pipefail
|
|
6
24
|
|
|
7
25
|
output_file="$1"
|
|
8
26
|
issue_number="$2"
|
|
9
27
|
ci_conclusion="$3"
|
|
28
|
+
# `${4-low}`, not `${4:-low}`: the colon form substitutes the default for an argument that was
|
|
29
|
+
# passed as an empty string, which is exactly the case that has to be caught. A skipped or
|
|
30
|
+
# failed `protected_changes` reaches this script as empty arguments, and reading those as
|
|
31
|
+
# "low, nothing protected" is how an unmeasured pull request would merge.
|
|
32
|
+
blast_level="${4-low}"
|
|
33
|
+
protected_hit="${5-false}"
|
|
34
|
+
owner_hit="${6-false}"
|
|
35
|
+
confidence_threshold="${7-0.8}"
|
|
36
|
+
|
|
37
|
+
# An empty measured level is not a low-risk pull request, it is a job that did not report. The
|
|
38
|
+
# defaults above exist for a caller that genuinely has nothing to say; an empty string arriving
|
|
39
|
+
# from a skipped or failed `protected_changes` must not read as the most permissive value.
|
|
40
|
+
[ -n "$blast_level" ] || blast_level=unmeasured
|
|
41
|
+
[ -n "$protected_hit" ] || protected_hit=unmeasured
|
|
42
|
+
[ -n "$owner_hit" ] || owner_hit=unmeasured
|
|
43
|
+
[ -n "$confidence_threshold" ] || confidence_threshold=0.8
|
|
10
44
|
|
|
11
45
|
if [ ! -f "$output_file" ] || ! jq -e '.items | arrays' "$output_file" >/dev/null 2>&1; then
|
|
12
46
|
echo invalid
|
|
13
47
|
exit 0
|
|
14
48
|
fi
|
|
15
49
|
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
# Every jq error becomes `invalid` rather than a non-zero exit. A report shaped so badly that it
|
|
51
|
+
# crashes the program used to fail the step, which skipped `conclude` and left the belt to spend
|
|
52
|
+
# up to six full agent runs on what was a formatting mistake the first time.
|
|
53
|
+
jq -r \
|
|
54
|
+
--arg issue "$issue_number" \
|
|
55
|
+
--arg conclusion "$ci_conclusion" \
|
|
56
|
+
--arg blast "$blast_level" \
|
|
57
|
+
--arg protected "$protected_hit" \
|
|
58
|
+
--arg owner "$owner_hit" \
|
|
59
|
+
--arg threshold "$confidence_threshold" '
|
|
60
|
+
|
|
61
|
+
def level_rank: {"low": 0, "medium": 1, "high": 2}[.];
|
|
62
|
+
def is_bool: type == "boolean";
|
|
63
|
+
def known_severity: type == "string" and (ascii_downcase | . == "critical" or . == "high" or . == "medium" or . == "low");
|
|
64
|
+
|
|
65
|
+
# A finding the rules can act on. Anything else means the report is not the contract, and the
|
|
66
|
+
# whole report is refused rather than the finding being quietly dropped to the safe side.
|
|
67
|
+
def well_formed_finding:
|
|
68
|
+
(has("verified") and (.verified | is_bool))
|
|
69
|
+
and (has("severity") and (.severity | known_severity));
|
|
70
|
+
|
|
71
|
+
def blocks: .verified == true and (.severity | ascii_downcase | . == "critical" or . == "high");
|
|
72
|
+
|
|
73
|
+
def decide:
|
|
74
|
+
.items as $items
|
|
75
|
+
|
|
76
|
+
| [$items[] | select(.type == "add_comment"
|
|
77
|
+
and (.item_number | tostring) == $issue
|
|
78
|
+
and (.body | type == "string"))] as $comments
|
|
79
|
+
|
|
80
|
+
# Exactly one comment may carry a verdict. Taking the first of several let a second comment
|
|
81
|
+
# reporting a verified critical finding be discarded, and let a comment with no verdict at
|
|
82
|
+
# all supply the report for a verdict written in another.
|
|
83
|
+
| [$comments[] | select(.body | test("\\*\\*Verdict:\\*\\*\\s*(assessed|remediated)"; "i"))] as $verdicts
|
|
84
|
+
| if ($verdicts | length) != 1 then "invalid" else
|
|
85
|
+
|
|
86
|
+
($verdicts[0].body) as $body
|
|
87
|
+
| ($body | capture("\\*\\*Verdict:\\*\\*\\s*(?<v>assessed|remediated)"; "i").v | ascii_downcase) as $verdict
|
|
88
|
+
| ([$items[] | select(.type == "push_to_pull_request_branch")] | length) as $pushes
|
|
89
|
+
|
|
90
|
+
# The LAST fenced json block in that comment, because the prompt puts the report last and
|
|
91
|
+
# the prose above it routinely quotes json from the diff under review. Reading the first
|
|
92
|
+
# fence handed the decision to whatever the agent happened to quote, and PROTECTED_PATHS
|
|
93
|
+
# names package.json and global.json, so the reviewed diff is often json.
|
|
94
|
+
| ([$body | scan("```json\\s*(.*?)```"; "m") | .[0]] | last) as $fence
|
|
95
|
+
|
|
96
|
+
# remediated used to require conclusion == "failure", which threw correct work away. The
|
|
97
|
+
# prompt tells the agent to merge main in, verify and push when CI is green but the pull
|
|
98
|
+
# request conflicts. That is a real and common state: a conflicting pull request has no
|
|
99
|
+
# merge ref, so GitHub can never run CI on that head, and the belt falls back to the last
|
|
100
|
+
# verdict on the branch, which is usually success. The agent did the job, the validator
|
|
101
|
+
# called it invalid, conclude was skipped, and because this worker stages its outputs the
|
|
102
|
+
# resolved merge commit was discarded. The belt then dispatched again on the same verdict,
|
|
103
|
+
# up to six times, each a full run on the single-slot merge belt.
|
|
104
|
+
#
|
|
105
|
+
# No apostrophes in here. This block sits inside the single-quoted jq program, and one
|
|
106
|
+
# apostrophe closes that string and breaks the script.
|
|
107
|
+
| if $verdict == "remediated" and $pushes == 1 then "remediated"
|
|
108
|
+
elif $verdict != "assessed" or $pushes != 0 then "invalid"
|
|
109
|
+
elif $fence == null then "invalid"
|
|
110
|
+
else
|
|
111
|
+
($fence | fromjson) as $report
|
|
112
|
+
| if ($report | type) != "object" then "invalid" else
|
|
113
|
+
|
|
114
|
+
($report.findings // []) as $findings
|
|
115
|
+
| (($report.blastRadiusRaise // {}) | if type == "object" then (.to // $blast) else null end) as $raise
|
|
116
|
+
|
|
117
|
+
# Every field the decision reads is checked before any of it is read. A near miss
|
|
118
|
+
# here is not a small problem: each one of these used to resolve to the permissive
|
|
119
|
+
# value and merge.
|
|
120
|
+
| if ($findings | type) != "array" then "invalid"
|
|
121
|
+
elif ([$findings[] | select(well_formed_finding | not)] | length) > 0 then "invalid"
|
|
122
|
+
elif ($report | has("recoverability")) and (($report.recoverability | type != "string") or (($report.recoverability | ascii_downcase) | level_rank) == null) then "invalid"
|
|
123
|
+
elif ($report | has("acceptanceCriteriaMet")) and (($report.acceptanceCriteriaMet | is_bool) | not) then "invalid"
|
|
124
|
+
elif ($report | has("confidence")) and (($report.confidence | type) != "number") then "invalid"
|
|
125
|
+
elif $raise == null or ($raise | type != "string") or (($raise | ascii_downcase) | level_rank) == null then "invalid"
|
|
126
|
+
elif ($blast | level_rank) == null then "invalid"
|
|
127
|
+
else
|
|
128
|
+
([$findings[] | select(blocks)] | length) as $blockers
|
|
129
|
+
|
|
130
|
+
# The agent may raise the measured blast radius when it finds something the path
|
|
131
|
+
# rules could not see. It may never lower it.
|
|
132
|
+
| ([($blast | level_rank), ($raise | ascii_downcase | level_rank)] | max) as $level
|
|
133
|
+
|
|
134
|
+
# The same rule the findings live by, applied to the one judgement field that
|
|
135
|
+
# can park a pull request on its own. An unevidenced "low" is the old category
|
|
136
|
+
# escalation wearing a new name: replayed against a consumer, a model that rated
|
|
137
|
+
# everything low dropped the auto-merge rate straight back to 27%, which is
|
|
138
|
+
# where it started. Saying a change cannot be undone means naming what cannot.
|
|
139
|
+
| (($report.recoverability // "medium") | ascii_downcase) as $claimed
|
|
140
|
+
| ((($report.recoverabilitySignals // []) | (type == "array") and (length > 0))) as $evidenced
|
|
141
|
+
| (if $claimed == "low" and ($evidenced | not) then "medium" else $claimed end) as $recoverability
|
|
142
|
+
| (($report.confidence // 0)) as $confidence
|
|
143
|
+
|
|
144
|
+
| if $conclusion != "success" then "blocked"
|
|
145
|
+
elif $blockers > 0 then "blocked"
|
|
146
|
+
elif $protected == "true" or $owner == "true" or $level == 2 then "owner-review"
|
|
147
|
+
# A fact the workflow could not measure is not a fact. Anything other than a
|
|
148
|
+
# clean true or false here means protected_changes did not report, and the
|
|
149
|
+
# pull request goes to a person rather than through on a default.
|
|
150
|
+
elif $protected != "false" or $owner != "false" then "human-review"
|
|
151
|
+
elif $level == 1 and $recoverability == "low" then "human-review"
|
|
152
|
+
elif ($report | has("acceptanceCriteriaMet")) and $report.acceptanceCriteriaMet == false then "human-review"
|
|
153
|
+
elif $confidence < ($threshold | tonumber) then "human-review"
|
|
154
|
+
else "auto-merge"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end;
|
|
160
|
+
|
|
161
|
+
try decide catch "invalid"
|
|
46
162
|
' "$output_file"
|
|
@@ -1,44 +1,48 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/validate-refine-output/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
-
name: Validate refine output
|
|
3
|
-
description: Verify agent_output.json contains a usable refinement outcome before any GitHub writes.
|
|
4
|
-
inputs:
|
|
5
|
-
output-file:
|
|
6
|
-
description: Path to agent_output.json.
|
|
7
|
-
required: true
|
|
8
|
-
marker:
|
|
9
|
-
description: Comment marker removed before evaluating clarification content.
|
|
10
|
-
required: true
|
|
11
|
-
|
|
12
|
-
description:
|
|
13
|
-
required: true
|
|
14
|
-
|
|
15
|
-
description:
|
|
16
|
-
required: true
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
description:
|
|
23
|
-
value: ${{ steps.validate.outputs.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/validate-refine-output/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: Validate refine output
|
|
3
|
+
description: Verify agent_output.json contains a usable refinement outcome before any GitHub writes.
|
|
4
|
+
inputs:
|
|
5
|
+
output-file:
|
|
6
|
+
description: Path to agent_output.json.
|
|
7
|
+
required: true
|
|
8
|
+
marker:
|
|
9
|
+
description: Comment marker removed before evaluating clarification content.
|
|
10
|
+
required: true
|
|
11
|
+
draft-marker:
|
|
12
|
+
description: Marker that identifies a temporal draft body written while questions remain.
|
|
13
|
+
required: true
|
|
14
|
+
comment-prefix:
|
|
15
|
+
description: Comment prefix removed before evaluating clarification content.
|
|
16
|
+
required: true
|
|
17
|
+
issue-number:
|
|
18
|
+
description: Issue number that every refinement outcome must target.
|
|
19
|
+
required: true
|
|
20
|
+
outputs:
|
|
21
|
+
valid:
|
|
22
|
+
description: Whether the output contains a usable replacement body or clarification comment.
|
|
23
|
+
value: ${{ steps.validate.outputs.valid }}
|
|
24
|
+
outcome:
|
|
25
|
+
description: "Deterministic refinement outcome: complete, split, questions, or invalid."
|
|
26
|
+
value: ${{ steps.validate.outputs.outcome }}
|
|
27
|
+
runs:
|
|
28
|
+
using: composite
|
|
29
|
+
steps:
|
|
30
|
+
- name: Validate refinement outcome
|
|
31
|
+
id: validate
|
|
32
|
+
shell: bash
|
|
33
|
+
env:
|
|
34
|
+
COMMENT_PREFIX: ${{ inputs.comment-prefix }}
|
|
35
|
+
DRAFT_MARKER: ${{ inputs.draft-marker }}
|
|
36
|
+
ISSUE_NUMBER: ${{ inputs.issue-number }}
|
|
37
|
+
MARKER: ${{ inputs.marker }}
|
|
38
|
+
OUTPUT_FILE: ${{ inputs.output-file }}
|
|
39
|
+
run: |
|
|
40
|
+
set -euo pipefail
|
|
41
|
+
|
|
42
|
+
outcome="$(bash "${{ github.action_path }}/validate-refine-output.sh" "$OUTPUT_FILE" "$MARKER" "$COMMENT_PREFIX" "$ISSUE_NUMBER" "$DRAFT_MARKER")"
|
|
43
|
+
echo "outcome=$outcome" >> "$GITHUB_OUTPUT"
|
|
44
|
+
echo "valid=$([ "$outcome" != 'invalid' ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
|
|
45
|
+
|
|
46
|
+
if [ "$outcome" = 'invalid' ]; then
|
|
47
|
+
echo "::warning::Agent output has no usable refinement outcome. It will not be applied."
|
|
48
|
+
fi
|
|
@@ -8,13 +8,14 @@ output_file="$1"
|
|
|
8
8
|
marker="$2"
|
|
9
9
|
comment_prefix="$3"
|
|
10
10
|
issue_number="$4"
|
|
11
|
+
draft_marker="$5"
|
|
11
12
|
|
|
12
13
|
if [ ! -f "$output_file" ] || ! jq -e '.items | arrays' "$output_file" >/dev/null 2>&1; then
|
|
13
14
|
echo invalid
|
|
14
15
|
exit 0
|
|
15
16
|
fi
|
|
16
17
|
|
|
17
|
-
jq -r --arg marker "$marker" --arg prefix "$comment_prefix" --arg issue "$issue_number" '
|
|
18
|
+
jq -r --arg marker "$marker" --arg prefix "$comment_prefix" --arg issue "$issue_number" --arg draft_marker "$draft_marker" '
|
|
18
19
|
def has_replacement_body:
|
|
19
20
|
any(.items[]; .type == "update_issue" and
|
|
20
21
|
(.item_number == null or (.item_number | tostring) == $issue) and
|
|
@@ -23,6 +24,14 @@ jq -r --arg marker "$marker" --arg prefix "$comment_prefix" --arg issue "$issue_
|
|
|
23
24
|
def has_update:
|
|
24
25
|
any(.items[]; .type == "update_issue");
|
|
25
26
|
|
|
27
|
+
# A temporal draft is a replacement body the worker still expects to grow: the draft
|
|
28
|
+
# marker distinguishes it from a finished body.
|
|
29
|
+
def has_draft_body:
|
|
30
|
+
any(.items[]; .type == "update_issue" and
|
|
31
|
+
(.item_number == null or (.item_number | tostring) == $issue) and
|
|
32
|
+
(.body | type == "string") and
|
|
33
|
+
(.body | contains($draft_marker)));
|
|
34
|
+
|
|
26
35
|
# A split writes children, which are the only items allowed to target something other than
|
|
27
36
|
# the source issue: they do not exist yet, so they carry no number at all.
|
|
28
37
|
def child_count:
|
|
@@ -65,11 +74,13 @@ jq -r --arg marker "$marker" --arg prefix "$comment_prefix" --arg issue "$issue_
|
|
|
65
74
|
|
|
66
75
|
# Order matters: a run that wrote children is a split even though it also replaced the
|
|
67
76
|
# parent body, and a lone child with no parent update is an incomplete split, not a
|
|
68
|
-
# complete refinement.
|
|
77
|
+
# complete refinement. A body carrying the draft marker is a temporal draft, not a
|
|
78
|
+
# finished refinement: it only counts as the questions outcome when the matching
|
|
79
|
+
# batched-questions comment is also there.
|
|
69
80
|
if child_count >= 2 and has_replacement_body and has_only_source_items then "split"
|
|
70
81
|
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
|
|
82
|
+
elif has_replacement_body and (has_draft_body | not) and has_only_source_items then "complete"
|
|
83
|
+
elif has_clarification and ((has_update | not) or has_draft_body) and has_only_source_items
|
|
73
84
|
and (reported_incomplete | not) then "questions"
|
|
74
85
|
else "invalid"
|
|
75
86
|
end
|
|
@@ -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, needs-maintainer, 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, needs-maintainer, 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,9 +1,9 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/verify-composite-actions/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
-
name: Verify composite actions
|
|
3
|
-
description: Check every local composite action manifest parses and uses only contexts a composite action actually has.
|
|
4
|
-
runs:
|
|
5
|
-
using: composite
|
|
6
|
-
steps:
|
|
7
|
-
- name: Validate composite action manifests
|
|
8
|
-
shell: bash
|
|
9
|
-
run: bash "${GITHUB_ACTION_PATH}/verify-composite-actions.sh"
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/verify-composite-actions/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: Verify composite actions
|
|
3
|
+
description: Check every local composite action manifest parses and uses only contexts a composite action actually has.
|
|
4
|
+
runs:
|
|
5
|
+
using: composite
|
|
6
|
+
steps:
|
|
7
|
+
- name: Validate composite action manifests
|
|
8
|
+
shell: bash
|
|
9
|
+
run: bash "${GITHUB_ACTION_PATH}/verify-composite-actions.sh"
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/verify-refine-output/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
-
name: Verify refine output validation
|
|
3
|
-
description: Exercise deterministic Refine output validation regressions.
|
|
4
|
-
runs:
|
|
5
|
-
using: composite
|
|
6
|
-
steps:
|
|
7
|
-
- name: Verify refinement outcomes
|
|
8
|
-
shell: bash
|
|
9
|
-
run: bash "${{ github.action_path }}/verify-refine-output.sh"
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/verify-refine-output/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: Verify refine output validation
|
|
3
|
+
description: Exercise deterministic Refine output validation regressions.
|
|
4
|
+
runs:
|
|
5
|
+
using: composite
|
|
6
|
+
steps:
|
|
7
|
+
- name: Verify refinement outcomes
|
|
8
|
+
shell: bash
|
|
9
|
+
run: bash "${{ github.action_path }}/verify-refine-output.sh"
|
|
@@ -7,6 +7,7 @@ set -euo pipefail
|
|
|
7
7
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
8
|
VALIDATOR="${HERE}/../validate-refine-output/validate-refine-output.sh"
|
|
9
9
|
MARKER='<!-- agent-refine -->'
|
|
10
|
+
DRAFT_MARK='<!-- agent-refine-draft -->'
|
|
10
11
|
PREFIX='Refinement update'
|
|
11
12
|
TEMP_DIR="$(mktemp -d)"
|
|
12
13
|
|
|
@@ -23,7 +24,7 @@ assert_output() {
|
|
|
23
24
|
local actual
|
|
24
25
|
|
|
25
26
|
printf '%s' "$payload" > "$output_file"
|
|
26
|
-
actual="$(bash "$VALIDATOR" "$output_file" "$MARKER" "$PREFIX" 42)"
|
|
27
|
+
actual="$(bash "$VALIDATOR" "$output_file" "$MARKER" "$PREFIX" 42 "$DRAFT_MARK")"
|
|
27
28
|
|
|
28
29
|
if [ "$actual" = "$expected" ]; then
|
|
29
30
|
PASS=$((PASS + 1))
|
|
@@ -55,6 +56,10 @@ assert_output 'wrong issue output is invalid' invalid \
|
|
|
55
56
|
'{"items":[{"type":"add_comment","item_number":7,"body":"Which users need this feature?"}]}'
|
|
56
57
|
assert_output 'complete output cannot update another issue' invalid \
|
|
57
58
|
'{"items":[{"type":"update_issue","item_number":42,"body":"# User story"},{"type":"update_issue","item_number":7,"body":"# Other story"}]}'
|
|
59
|
+
assert_output 'draft update with questions comment is questions' questions \
|
|
60
|
+
'{"items":[{"type":"update_issue","item_number":42,"body":"<!-- agent-refine-draft -->\n### Proposal\n_pending — see questions below_"},{"type":"add_comment","item_number":42,"body":"<!-- agent-refine -->\nRefinement update\nI have some questions about this issue. Please reply in one comment and I''ll process your answers.\nWhich area owns this behavior?"}]}'
|
|
61
|
+
assert_output 'draft update alone is invalid' invalid \
|
|
62
|
+
'{"items":[{"type":"update_issue","item_number":42,"body":"<!-- agent-refine-draft -->\n### Proposal\n_pending — see questions below_"}]}'
|
|
58
63
|
|
|
59
64
|
# From a real run. The agent's first update_issue went out malformed, the bridge counted it
|
|
60
65
|
# as spent, the retry carrying the body was refused, and the "Refinement complete" comment
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/verify-route-matrix/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
-
name: Verify route matrix
|
|
3
|
-
description: Exercise the router's classifier against every supported event, and check that each route and dispatch operation has a job in work-router.yml.
|
|
4
|
-
runs:
|
|
5
|
-
using: composite
|
|
6
|
-
steps:
|
|
7
|
-
- name: Run route matrix verification
|
|
8
|
-
shell: bash
|
|
9
|
-
run: bash "${GITHUB_ACTION_PATH}/verify-route-matrix.sh"
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/verify-route-matrix/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: Verify route matrix
|
|
3
|
+
description: Exercise the router's classifier against every supported event, and check that each route and dispatch operation has a job in work-router.yml.
|
|
4
|
+
runs:
|
|
5
|
+
using: composite
|
|
6
|
+
steps:
|
|
7
|
+
- name: Run route matrix verification
|
|
8
|
+
shell: bash
|
|
9
|
+
run: bash "${GITHUB_ACTION_PATH}/verify-route-matrix.sh"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Managed by @plainconceptsplatform/workflows. Source: loops/actions/verify-route-matrix/verify-gate-metrics.mjs. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
+
// Runs the housekeeping digest's gate-metrics renderer against fixtures. The auto-merge rate
|
|
3
|
+
// is the number the merge gate asks to be judged on, and the one Phase 7 would widen trust
|
|
4
|
+
// from, so "it cannot merge anything wrong" is not a reason to leave its arithmetic untested.
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
const yml = readFileSync(process.argv[2], "utf8").replace(/\r\n/g, "\n");
|
|
7
|
+
|
|
8
|
+
// Pull the gate metrics renderer out of the inline github-script block and run it. The counting
|
|
9
|
+
// loops above it need a GitHub API to exercise, but the arithmetic does not, and the arithmetic
|
|
10
|
+
// is the part that reports a number somebody will act on.
|
|
11
|
+
const start = yml.indexOf("const gateSection = () => {");
|
|
12
|
+
if (start === -1) { console.error("FAIL: housekeeping has no gateSection renderer"); process.exit(1); }
|
|
13
|
+
let depth = 0, end = start;
|
|
14
|
+
for (let i = yml.indexOf("{", start); i < yml.length; i++) {
|
|
15
|
+
if (yml[i] === "{") depth++;
|
|
16
|
+
else if (yml[i] === "}") { depth--; if (depth === 0) { end = i + 1; break; } }
|
|
17
|
+
}
|
|
18
|
+
const src = yml.slice(start, end).replace(/^\s+/gm, " ");
|
|
19
|
+
|
|
20
|
+
const run = (dispositions, autoMerged, reverts) =>
|
|
21
|
+
new Function("dispositions", "autoMerged", "reverts", "metricsWindowMs",
|
|
22
|
+
`${src}; return gateSection();`)(dispositions, autoMerged, reverts, 14 * 24 * 3600000);
|
|
23
|
+
|
|
24
|
+
let failed = 0;
|
|
25
|
+
const check = (name, got, want) => {
|
|
26
|
+
const ok = typeof want === "function" ? want(got) : got === want;
|
|
27
|
+
if (!ok) { failed = 1; console.error(`FAIL: gate metrics ${name}: got ${JSON.stringify(got)}`); }
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
check("is empty when the gate has posted nothing",
|
|
31
|
+
run({}, [], []), "");
|
|
32
|
+
check("reports the auto-merge share",
|
|
33
|
+
run({ "auto-merge": 12, "human-review": 2, "owner-review": 1 }, [], []),
|
|
34
|
+
(s) => s.includes("**80% auto-merged** (12 of 15 dispositions)"));
|
|
35
|
+
check("rounds rather than truncates",
|
|
36
|
+
run({ "auto-merge": 2, "human-review": 1 }, [], []),
|
|
37
|
+
(s) => s.includes("**67% auto-merged**"));
|
|
38
|
+
check("counts a revert only against the pull request it names",
|
|
39
|
+
run({ "auto-merge": 2 }, [11, 12], [12, 99]),
|
|
40
|
+
(s) => s.includes("later reverted: 1"));
|
|
41
|
+
check("counts no reverts when none match",
|
|
42
|
+
run({ "auto-merge": 2 }, [11, 12], [77]),
|
|
43
|
+
(s) => s.includes("later reverted: 0"));
|
|
44
|
+
check("a repository with only parked pull requests reads as zero, not as an error",
|
|
45
|
+
run({ "human-review": 3 }, [], []),
|
|
46
|
+
(s) => s.includes("**0% auto-merged** (0 of 3 dispositions)"));
|
|
47
|
+
check("lists every disposition it saw",
|
|
48
|
+
run({ "auto-merge": 1, blocked: 2 }, [], []),
|
|
49
|
+
(s) => s.includes("`blocked` · 2") && s.includes("`auto-merge` · 1"));
|
|
50
|
+
|
|
51
|
+
process.exit(failed);
|