@plainconceptsplatform/workflows 0.19.2 → 0.20.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/dist/catalog-installation.js +12 -1
- package/dist/index.js +0 -0
- package/dist/stack-defaults.js +16 -16
- package/dist/worker-env.js +14 -0
- 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/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/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 +329 -29
- 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-merge-gate.md +367 -148
- package/loops/workflows/agent-refine.md +60 -16
- 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/package.json +12 -11
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/remove-issue-labels/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
-
name: Remove issue labels
|
|
3
|
-
description: Remove one or more labels from an issue or pull request.
|
|
4
|
-
inputs:
|
|
5
|
-
token:
|
|
6
|
-
description: GitHub token used by github-script.
|
|
7
|
-
required: true
|
|
8
|
-
issue-number:
|
|
9
|
-
description: Issue or pull request number.
|
|
10
|
-
required: true
|
|
11
|
-
labels:
|
|
12
|
-
description: Labels to remove, one per line. A comma-separated list is accepted as well.
|
|
13
|
-
required: true
|
|
14
|
-
runs:
|
|
15
|
-
using: composite
|
|
16
|
-
steps:
|
|
17
|
-
- name: Remove labels
|
|
18
|
-
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
19
|
-
env:
|
|
20
|
-
ISSUE_NUMBER: ${{ inputs.issue-number }}
|
|
21
|
-
LABELS: ${{ inputs.labels }}
|
|
22
|
-
with:
|
|
23
|
-
github-token: ${{ inputs.token }}
|
|
24
|
-
script: |
|
|
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)) {
|
|
28
|
-
try {
|
|
29
|
-
await github.rest.issues.removeLabel({
|
|
30
|
-
...context.repo,
|
|
31
|
-
issue_number: Number(process.env.ISSUE_NUMBER),
|
|
32
|
-
name: label,
|
|
33
|
-
});
|
|
34
|
-
} catch (error) {
|
|
35
|
-
if (error.status !== 404) throw error;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/remove-issue-labels/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: Remove issue labels
|
|
3
|
+
description: Remove one or more labels from an issue or pull request.
|
|
4
|
+
inputs:
|
|
5
|
+
token:
|
|
6
|
+
description: GitHub token used by github-script.
|
|
7
|
+
required: true
|
|
8
|
+
issue-number:
|
|
9
|
+
description: Issue or pull request number.
|
|
10
|
+
required: true
|
|
11
|
+
labels:
|
|
12
|
+
description: Labels to remove, one per line. A comma-separated list is accepted as well.
|
|
13
|
+
required: true
|
|
14
|
+
runs:
|
|
15
|
+
using: composite
|
|
16
|
+
steps:
|
|
17
|
+
- name: Remove labels
|
|
18
|
+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
19
|
+
env:
|
|
20
|
+
ISSUE_NUMBER: ${{ inputs.issue-number }}
|
|
21
|
+
LABELS: ${{ inputs.labels }}
|
|
22
|
+
with:
|
|
23
|
+
github-token: ${{ inputs.token }}
|
|
24
|
+
script: |
|
|
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)) {
|
|
28
|
+
try {
|
|
29
|
+
await github.rest.issues.removeLabel({
|
|
30
|
+
...context.repo,
|
|
31
|
+
issue_number: Number(process.env.ISSUE_NUMBER),
|
|
32
|
+
name: label,
|
|
33
|
+
});
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error.status !== 404) throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/update-agent-issues/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
-
name: Update agent issues
|
|
3
|
-
description: Apply every update_issue item from agent_output.json.
|
|
4
|
-
inputs:
|
|
5
|
-
output-file:
|
|
6
|
-
description: Path to agent_output.json.
|
|
7
|
-
required: true
|
|
8
|
-
token:
|
|
9
|
-
description: GitHub token with issues:write.
|
|
10
|
-
required: true
|
|
11
|
-
fallback-issue-number:
|
|
12
|
-
description: Issue an item targets when it names none.
|
|
13
|
-
required: false
|
|
14
|
-
default: ''
|
|
15
|
-
runs:
|
|
16
|
-
using: composite
|
|
17
|
-
steps:
|
|
18
|
-
- name: Update issues
|
|
19
|
-
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
20
|
-
env:
|
|
21
|
-
AGENT_OUTPUT_LIB: ${{ github.action_path }}/../agent-output.cjs
|
|
22
|
-
OUTPUT_FILE: ${{ inputs.output-file }}
|
|
23
|
-
FALLBACK_ISSUE_NUMBER: ${{ inputs.fallback-issue-number }}
|
|
24
|
-
with:
|
|
25
|
-
github-token: ${{ inputs.token }}
|
|
26
|
-
script: |
|
|
27
|
-
const { readAgentItems } = require(process.env.AGENT_OUTPUT_LIB);
|
|
28
|
-
const items = readAgentItems(process.env.OUTPUT_FILE, 'update_issue');
|
|
29
|
-
|
|
30
|
-
let updated = 0;
|
|
31
|
-
|
|
32
|
-
for (const item of items) {
|
|
33
|
-
const target = item.item_number ?? process.env.FALLBACK_ISSUE_NUMBER;
|
|
34
|
-
|
|
35
|
-
if (!target) {
|
|
36
|
-
core.warning(`update_issue item has no item_number and no fallback, skipping: ${JSON.stringify(item)}`);
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const changes = {};
|
|
41
|
-
if (item.title) changes.title = item.title;
|
|
42
|
-
if (item.body) changes.body = item.body;
|
|
43
|
-
|
|
44
|
-
if (Object.keys(changes).length === 0) {
|
|
45
|
-
core.warning(`update_issue item changes nothing, skipping: ${JSON.stringify(item)}`);
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
await github.rest.issues.update({
|
|
50
|
-
...context.repo,
|
|
51
|
-
issue_number: Number(target),
|
|
52
|
-
...changes,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
updated += 1;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
core.info(`Updated ${updated} issue(s).`);
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/update-agent-issues/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: Update agent issues
|
|
3
|
+
description: Apply every update_issue item from agent_output.json.
|
|
4
|
+
inputs:
|
|
5
|
+
output-file:
|
|
6
|
+
description: Path to agent_output.json.
|
|
7
|
+
required: true
|
|
8
|
+
token:
|
|
9
|
+
description: GitHub token with issues:write.
|
|
10
|
+
required: true
|
|
11
|
+
fallback-issue-number:
|
|
12
|
+
description: Issue an item targets when it names none.
|
|
13
|
+
required: false
|
|
14
|
+
default: ''
|
|
15
|
+
runs:
|
|
16
|
+
using: composite
|
|
17
|
+
steps:
|
|
18
|
+
- name: Update issues
|
|
19
|
+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
20
|
+
env:
|
|
21
|
+
AGENT_OUTPUT_LIB: ${{ github.action_path }}/../agent-output.cjs
|
|
22
|
+
OUTPUT_FILE: ${{ inputs.output-file }}
|
|
23
|
+
FALLBACK_ISSUE_NUMBER: ${{ inputs.fallback-issue-number }}
|
|
24
|
+
with:
|
|
25
|
+
github-token: ${{ inputs.token }}
|
|
26
|
+
script: |
|
|
27
|
+
const { readAgentItems } = require(process.env.AGENT_OUTPUT_LIB);
|
|
28
|
+
const items = readAgentItems(process.env.OUTPUT_FILE, 'update_issue');
|
|
29
|
+
|
|
30
|
+
let updated = 0;
|
|
31
|
+
|
|
32
|
+
for (const item of items) {
|
|
33
|
+
const target = item.item_number ?? process.env.FALLBACK_ISSUE_NUMBER;
|
|
34
|
+
|
|
35
|
+
if (!target) {
|
|
36
|
+
core.warning(`update_issue item has no item_number and no fallback, skipping: ${JSON.stringify(item)}`);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const changes = {};
|
|
41
|
+
if (item.title) changes.title = item.title;
|
|
42
|
+
if (item.body) changes.body = item.body;
|
|
43
|
+
|
|
44
|
+
if (Object.keys(changes).length === 0) {
|
|
45
|
+
core.warning(`update_issue item changes nothing, skipping: ${JSON.stringify(item)}`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await github.rest.issues.update({
|
|
50
|
+
...context.repo,
|
|
51
|
+
issue_number: Number(target),
|
|
52
|
+
...changes,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
updated += 1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
core.info(`Updated ${updated} issue(s).`);
|
|
@@ -1,40 +1,62 @@
|
|
|
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:
|
|
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
|
|
10
|
-
required: true
|
|
11
|
-
ci-conclusion:
|
|
12
|
-
description: CI conclusion supplied to the merge gate.
|
|
13
|
-
required: true
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
description:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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: Compute the merge-gate disposition from measured facts and the agent's structured report, 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 report must target.
|
|
10
|
+
required: true
|
|
11
|
+
ci-conclusion:
|
|
12
|
+
description: CI conclusion supplied to the merge gate.
|
|
13
|
+
required: true
|
|
14
|
+
blast-level:
|
|
15
|
+
description: "Measured blast radius: low, medium or high."
|
|
16
|
+
required: false
|
|
17
|
+
default: low
|
|
18
|
+
protected-hit:
|
|
19
|
+
description: Whether a protected path was touched.
|
|
20
|
+
required: false
|
|
21
|
+
default: 'false'
|
|
22
|
+
owner-hit:
|
|
23
|
+
description: Whether an owner path was touched.
|
|
24
|
+
required: false
|
|
25
|
+
default: 'false'
|
|
26
|
+
confidence-threshold:
|
|
27
|
+
description: Agent confidence below which the pull request goes to a human.
|
|
28
|
+
required: false
|
|
29
|
+
default: '0.8'
|
|
30
|
+
outputs:
|
|
31
|
+
valid:
|
|
32
|
+
description: Whether the output produced a usable disposition.
|
|
33
|
+
value: ${{ steps.validate.outputs.valid }}
|
|
34
|
+
outcome:
|
|
35
|
+
description: "Disposition: auto-merge, human-review, owner-review, blocked, remediated, or invalid."
|
|
36
|
+
value: ${{ steps.validate.outputs.outcome }}
|
|
37
|
+
runs:
|
|
38
|
+
using: composite
|
|
39
|
+
steps:
|
|
40
|
+
- name: Compute the merge-gate disposition
|
|
41
|
+
id: validate
|
|
42
|
+
shell: bash
|
|
43
|
+
env:
|
|
44
|
+
ISSUE_NUMBER: ${{ inputs.issue-number }}
|
|
45
|
+
CI_CONCLUSION: ${{ inputs.ci-conclusion }}
|
|
46
|
+
OUTPUT_FILE: ${{ inputs.output-file }}
|
|
47
|
+
BLAST_LEVEL: ${{ inputs.blast-level }}
|
|
48
|
+
PROTECTED_HIT: ${{ inputs.protected-hit }}
|
|
49
|
+
OWNER_HIT: ${{ inputs.owner-hit }}
|
|
50
|
+
CONFIDENCE_THRESHOLD: ${{ inputs.confidence-threshold }}
|
|
51
|
+
run: |
|
|
52
|
+
set -euo pipefail
|
|
53
|
+
|
|
54
|
+
outcome="$(bash "${{ github.action_path }}/validate-merge-gate-output.sh" \
|
|
55
|
+
"$OUTPUT_FILE" "$ISSUE_NUMBER" "$CI_CONCLUSION" \
|
|
56
|
+
"$BLAST_LEVEL" "$PROTECTED_HIT" "$OWNER_HIT" "$CONFIDENCE_THRESHOLD")"
|
|
57
|
+
echo "outcome=$outcome" >> "$GITHUB_OUTPUT"
|
|
58
|
+
echo "valid=$([ "$outcome" != 'invalid' ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
|
|
59
|
+
|
|
60
|
+
if [ "$outcome" = 'invalid' ]; then
|
|
61
|
+
echo "::warning::Agent output has no usable merge-gate report. It will not be applied."
|
|
62
|
+
fi
|
|
@@ -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
|