@plainconceptsplatform/workflows 0.4.34 → 0.5.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/loops/actions/agent-output.cjs +17 -17
- package/loops/actions/audit-close/action.yml +24 -0
- package/loops/actions/classify-route/action.yml +1 -3
- package/loops/actions/classify-route/classify-route.sh +40 -35
- 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 +16 -3
- package/loops/actions/validate-review-output/action.yml +35 -35
- package/loops/actions/validate-triage-output/action.yml +36 -36
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +91 -36
- package/loops/scripts/compile-agent-workflows.mjs +195 -6
- package/loops/templates/agentics/agentics-checks.yml +86 -86
- package/loops/templates/agentics/agentics-maintenance.yml +121 -121
- package/loops/templates/ci/app-ci-dotnet-next.yml +268 -171
- package/loops/templates/ci/app-ci-node-monorepo.yml +208 -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 +46 -41
- package/loops/templates/release/github-release.yml +30 -30
- package/loops/workflows/agent-apply-review.md +465 -443
- package/loops/workflows/agent-audit.md +213 -197
- package/loops/workflows/agent-implement.md +538 -414
- package/loops/workflows/agent-merge-gate.md +730 -584
- package/loops/workflows/agent-refine.md +609 -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 +862 -632
- package/package.json +7 -8
- package/dist/action-validation.test.d.ts +0 -1
- package/dist/action-validation.test.js +0 -84
- package/dist/catalog-installation.d.ts +0 -30
- package/dist/catalog-installation.js +0 -379
- package/dist/catalog-installation.test.d.ts +0 -1
- package/dist/catalog-installation.test.js +0 -448
- package/dist/catalog-listing.d.ts +0 -13
- package/dist/catalog-listing.js +0 -70
- package/dist/catalog-listing.test.d.ts +0 -1
- package/dist/catalog-listing.test.js +0 -150
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -218
- package/dist/index.test.d.ts +0 -1
- package/dist/index.test.js +0 -273
- package/dist/repository-inspection.d.ts +0 -23
- package/dist/repository-inspection.js +0 -113
- package/dist/repository-inspection.test.d.ts +0 -1
- package/dist/repository-inspection.test.js +0 -77
- 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/dist/route-processing.d.ts +0 -6
- package/dist/route-processing.js +0 -150
- package/dist/route-processing.test.d.ts +0 -1
- package/dist/route-processing.test.js +0 -350
- package/dist/stack-defaults.d.ts +0 -11
- package/dist/stack-defaults.js +0 -104
- package/dist/stack-defaults.test.d.ts +0 -1
- package/dist/stack-defaults.test.js +0 -266
- package/dist/tui.d.ts +0 -25
- package/dist/tui.js +0 -287
- package/dist/tui.test.d.ts +0 -1
- package/dist/tui.test.js +0 -246
- package/dist/workflow-catalog.d.ts +0 -25
- package/dist/workflow-catalog.js +0 -59
- package/dist/workflow-catalog.test.d.ts +0 -1
- package/dist/workflow-catalog.test.js +0 -29
- package/loops/workflows/agent-direct.md +0 -375
- package/loops/workflows/agent-propose.md +0 -342
|
@@ -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,6 +23,13 @@ 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
|
+
|
|
26
33
|
def has_only_source_items:
|
|
27
34
|
all(.items[];
|
|
28
35
|
(
|
|
@@ -32,7 +39,8 @@ jq -r --arg marker "$marker" --arg prefix "$comment_prefix" --arg issue "$issue_
|
|
|
32
39
|
(
|
|
33
40
|
.type == "add_comment" and
|
|
34
41
|
(.item_number | tostring) == $issue
|
|
35
|
-
)
|
|
42
|
+
) or
|
|
43
|
+
.type == "create_issue");
|
|
36
44
|
|
|
37
45
|
def has_clarification:
|
|
38
46
|
any(.items[]; .type == "add_comment" and (.item_number | tostring) == $issue and
|
|
@@ -40,7 +48,12 @@ jq -r --arg marker "$marker" --arg prefix "$comment_prefix" --arg issue "$issue_
|
|
|
40
48
|
(.body | split($marker) | join("") | split($prefix) | join("") |
|
|
41
49
|
gsub("<[^>]*>"; "") | test("[[:alnum:]]")));
|
|
42
50
|
|
|
43
|
-
|
|
51
|
+
# Order matters: a run that wrote children is a split even though it also replaced the
|
|
52
|
+
# parent body, and a lone child with no parent update is an incomplete split, not a
|
|
53
|
+
# complete refinement.
|
|
54
|
+
if child_count >= 2 and has_replacement_body and has_only_source_items then "split"
|
|
55
|
+
elif child_count > 0 then "invalid"
|
|
56
|
+
elif has_replacement_body and has_only_source_items then "complete"
|
|
44
57
|
elif has_clarification and (has_update | not) and has_only_source_items then "questions"
|
|
45
58
|
else "invalid"
|
|
46
59
|
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, 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
|
|
@@ -7,6 +7,7 @@ set -euo pipefail
|
|
|
7
7
|
|
|
8
8
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
9
|
ROUTER_YML="${HERE}/../../workflows/work-router.yml"
|
|
10
|
+
AUTHORIZE_YML="${HERE}/../../workflows/authorize-bot-work.yml"
|
|
10
11
|
IMPLEMENT_WORKER_MD="${HERE}/../../workflows/agent-implement.md"
|
|
11
12
|
MERGE_GATE_WORKER_MD="${HERE}/../../workflows/agent-merge-gate.md"
|
|
12
13
|
|
|
@@ -59,24 +60,32 @@ assert_route() {
|
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
echo "── Label events ──────────────────────────────────────────────────────────"
|
|
62
|
-
assert_route "refine label
|
|
63
|
-
EVENT=issues ACTION=labeled LABEL=refine EVENT_ISSUE_NUMBER=42
|
|
64
|
-
assert_route "
|
|
65
|
-
EVENT=issues ACTION=labeled LABEL=
|
|
63
|
+
assert_route "human refine label waits for authorization" none \
|
|
64
|
+
EVENT=issues ACTION=labeled LABEL=refine ACTOR=maintainer EVENT_ISSUE_NUMBER=42
|
|
65
|
+
assert_route "bot refine label routes to refine" refine \
|
|
66
|
+
EVENT=issues ACTION=labeled LABEL=refine ACTOR=platform-devbox[bot] EVENT_ISSUE_NUMBER=42
|
|
67
|
+
assert_route "human implement label waits for authorization" none \
|
|
68
|
+
EVENT=issues ACTION=labeled LABEL=implement ACTOR=maintainer EVENT_ISSUE_NUMBER=42
|
|
69
|
+
assert_route "bot implement label routes to implement" implement \
|
|
70
|
+
EVENT=issues ACTION=labeled LABEL=implement ACTOR=platform-devbox[bot] EVENT_ISSUE_NUMBER=42
|
|
71
|
+
assert_route "human feature label waits for authorization" none \
|
|
72
|
+
EVENT=issues ACTION=labeled LABEL=feature ACTOR=maintainer EVENT_ISSUE_NUMBER=350
|
|
66
73
|
assert_route "unrelated label routes nowhere" none \
|
|
67
74
|
EVENT=issues ACTION=labeled LABEL=documentation EVENT_ISSUE_NUMBER=42
|
|
68
|
-
assert_route "
|
|
75
|
+
assert_route "issue opened without labels routes to triage" triage \
|
|
69
76
|
EVENT=issues ACTION=opened EVENT_ISSUE_NUMBER=42
|
|
77
|
+
assert_route "issue opened with refine label skips triage" none \
|
|
78
|
+
EVENT=issues ACTION=opened 'ISSUE_LABELS=["refine"]' EVENT_ISSUE_NUMBER=42
|
|
79
|
+
assert_route "issue opened with implement label skips triage" none \
|
|
80
|
+
EVENT=issues ACTION=opened 'ISSUE_LABELS=["implement"]' EVENT_ISSUE_NUMBER=42
|
|
70
81
|
assert_route "a human triage label routes to triage" triage \
|
|
71
82
|
EVENT=issues ACTION=labeled LABEL=triage ACTOR=maintainer EVENT_ISSUE_NUMBER=42
|
|
72
83
|
assert_route "a bot triage label routes nowhere" none \
|
|
73
84
|
EVENT=issues ACTION=labeled LABEL=triage ACTOR=platform-devbox[bot] EVENT_ISSUE_NUMBER=42
|
|
85
|
+
assert_route "implement + bot-working without feature routes to implement" implement \
|
|
86
|
+
EVENT=issues ACTION=labeled LABEL=bot-working 'ISSUE_LABELS=["implement","bot-working"]' EVENT_ISSUE_NUMBER=300
|
|
74
87
|
assert "refine label starts a first pass" first \
|
|
75
|
-
"$(route_field refine-mode EVENT=issues ACTION=labeled LABEL=refine EVENT_ISSUE_NUMBER=42)"
|
|
76
|
-
assert_route "direct label routes to direct" direct \
|
|
77
|
-
EVENT=issues ACTION=labeled LABEL=direct EVENT_ISSUE_NUMBER=42
|
|
78
|
-
assert "direct label starts a first pass" first \
|
|
79
|
-
"$(route_field direct-mode EVENT=issues ACTION=labeled LABEL=direct EVENT_ISSUE_NUMBER=42)"
|
|
88
|
+
"$(route_field refine-mode EVENT=issues ACTION=labeled LABEL=refine ACTOR=platform-devbox[bot] EVENT_ISSUE_NUMBER=42)"
|
|
80
89
|
|
|
81
90
|
echo "── Comment events ────────────────────────────────────────────────────────"
|
|
82
91
|
assert_route "a comment on a pull request routes to apply-review" apply-review \
|
|
@@ -93,12 +102,6 @@ assert_route "the bot's own comment never re-enters refine" none \
|
|
|
93
102
|
assert_route "a comment on an issue without refine routes nowhere" none \
|
|
94
103
|
EVENT=issue_comment COMMENT_ON_PR=false COMMENT_SENDER_TYPE=User \
|
|
95
104
|
'ISSUE_LABELS=["bug"]' EVENT_ISSUE_NUMBER=42
|
|
96
|
-
assert_route "an author reply on a direct issue continues" direct \
|
|
97
|
-
EVENT=issue_comment COMMENT_ON_PR=false COMMENT_SENDER_TYPE=User \
|
|
98
|
-
'ISSUE_LABELS=["direct"]' EVENT_ISSUE_NUMBER=42
|
|
99
|
-
assert "a direct reply is a continue pass" continue \
|
|
100
|
-
"$(route_field direct-mode EVENT=issue_comment COMMENT_ON_PR=false \
|
|
101
|
-
COMMENT_SENDER_TYPE=User 'ISSUE_LABELS=["direct"]' EVENT_ISSUE_NUMBER=42)"
|
|
102
105
|
assert_route "the bot's own comment never re-enters direct" none \
|
|
103
106
|
EVENT=issue_comment COMMENT_ON_PR=false COMMENT_SENDER_TYPE=Bot \
|
|
104
107
|
'ISSUE_LABELS=["direct"]' EVENT_ISSUE_NUMBER=42
|
|
@@ -112,6 +115,14 @@ assert_route "the bot's own comment never re-enters triage" none \
|
|
|
112
115
|
EVENT=issue_comment COMMENT_ON_PR=false COMMENT_SENDER_TYPE=Bot \
|
|
113
116
|
'ISSUE_LABELS=["triage"]' EVENT_ISSUE_NUMBER=42
|
|
114
117
|
|
|
118
|
+
echo "── Closed issues ─────────────────────────────────────────────────────────"
|
|
119
|
+
assert_route "a closing comment on a refine issue does not re-refine" none EVENT=issue_comment COMMENT_ON_PR=false COMMENT_SENDER_TYPE=User ISSUE_STATE=closed 'ISSUE_LABELS=["refine"]' EVENT_ISSUE_NUMBER=42
|
|
120
|
+
assert_route "a comment on a closed issue never re-triages" none EVENT=issue_comment COMMENT_ON_PR=false COMMENT_SENDER_TYPE=User ISSUE_STATE=closed 'ISSUE_LABELS=["triage"]' EVENT_ISSUE_NUMBER=42
|
|
121
|
+
assert_route "a work label added to a closed issue routes nowhere" none EVENT=issues ACTION=labeled LABEL=bot-working ISSUE_STATE=closed 'ISSUE_LABELS=["implement"]' EVENT_ISSUE_NUMBER=42
|
|
122
|
+
assert_route "a closed issue reopened as opened still routes nowhere while closed" none EVENT=issues ACTION=opened ISSUE_STATE=closed 'ISSUE_LABELS=[]' EVENT_ISSUE_NUMBER=42
|
|
123
|
+
assert_route "a comment on a closed pull request still routes to apply-review" apply-review EVENT=issue_comment COMMENT_ON_PR=true ISSUE_STATE=closed EVENT_ISSUE_NUMBER=7
|
|
124
|
+
assert_route "an open refine issue is unaffected by the closed guard" refine EVENT=issue_comment COMMENT_ON_PR=false COMMENT_SENDER_TYPE=User ISSUE_STATE=open 'ISSUE_LABELS=["refine"]' EVENT_ISSUE_NUMBER=42
|
|
125
|
+
|
|
115
126
|
echo "── Review events ─────────────────────────────────────────────────────────"
|
|
116
127
|
assert_route "a review comment routes to apply-review" apply-review \
|
|
117
128
|
EVENT=pull_request_review_comment EVENT_PR_NUMBER=7
|
|
@@ -121,13 +132,20 @@ assert_route "a pull_request_target routes to bot-approve" bot-approve \
|
|
|
121
132
|
EVENT=pull_request_target ACTION=opened
|
|
122
133
|
|
|
123
134
|
echo "── CI completion ─────────────────────────────────────────────────────────"
|
|
124
|
-
assert_route "App CI
|
|
125
|
-
EVENT=workflow_run RUN_PR_NUMBER=7 RUN_CONCLUSION=
|
|
126
|
-
assert "merge-gate carries the CI conclusion" failure \
|
|
135
|
+
assert_route "a failed App CI run on a pull request routes to merge-gate" merge-gate \
|
|
136
|
+
EVENT=workflow_run RUN_PR_NUMBER=7 RUN_CONCLUSION=failure RUN_ID=99
|
|
137
|
+
assert "merge-gate carries the failing CI conclusion" failure \
|
|
127
138
|
"$(route_field ci-conclusion EVENT=workflow_run RUN_PR_NUMBER=7 \
|
|
128
139
|
RUN_CONCLUSION=failure RUN_ID=99)"
|
|
129
|
-
|
|
130
|
-
EVENT=workflow_run RUN_PR_NUMBER=
|
|
140
|
+
assert "merge-gate carries the failing CI run id" 99 \
|
|
141
|
+
"$(route_field ci-run-id EVENT=workflow_run RUN_PR_NUMBER=7 \
|
|
142
|
+
RUN_CONCLUSION=failure RUN_ID=99)"
|
|
143
|
+
assert_route "a green App CI run does not auto-trigger the gate" none \
|
|
144
|
+
EVENT=workflow_run RUN_PR_NUMBER=7 RUN_CONCLUSION=success RUN_ID=99
|
|
145
|
+
assert_route "a cancelled App CI run does not auto-trigger the gate" none \
|
|
146
|
+
EVENT=workflow_run RUN_PR_NUMBER=7 RUN_CONCLUSION=cancelled RUN_ID=99
|
|
147
|
+
assert_route "a failed CI run with no pull request routes nowhere" none \
|
|
148
|
+
EVENT=workflow_run RUN_PR_NUMBER= RUN_CONCLUSION=failure RUN_ID=99
|
|
131
149
|
|
|
132
150
|
echo "── Schedules ─────────────────────────────────────────────────────────────"
|
|
133
151
|
while read -r cron; do
|
|
@@ -151,8 +169,6 @@ assert_route "refine dispatch rejects a non-numeric issue" none \
|
|
|
151
169
|
EVENT=workflow_dispatch OPERATION=refine INPUT_ISSUE_NUMBER=abc
|
|
152
170
|
assert_route "refine dispatch accepts a positive issue" refine \
|
|
153
171
|
EVENT=workflow_dispatch OPERATION=refine INPUT_ISSUE_NUMBER=42
|
|
154
|
-
assert_route "direct dispatch accepts a positive issue" direct \
|
|
155
|
-
EVENT=workflow_dispatch OPERATION=direct INPUT_ISSUE_NUMBER=42
|
|
156
172
|
assert_route "direct dispatch needs an issue number" none \
|
|
157
173
|
EVENT=workflow_dispatch OPERATION=direct INPUT_ISSUE_NUMBER=
|
|
158
174
|
assert_route "triage dispatch accepts a positive issue" triage \
|
|
@@ -161,41 +177,80 @@ assert_route "triage dispatch needs an issue number" none \
|
|
|
161
177
|
EVENT=workflow_dispatch OPERATION=triage INPUT_ISSUE_NUMBER=
|
|
162
178
|
assert "triage dispatch defaults to first pass" first \
|
|
163
179
|
"$(route_field triage-mode EVENT=workflow_dispatch OPERATION=triage INPUT_ISSUE_NUMBER=42)"
|
|
180
|
+
assert_route "batch dispatch needs an issue number" none \
|
|
181
|
+
EVENT=workflow_dispatch OPERATION=batch INPUT_ISSUE_NUMBER=
|
|
164
182
|
assert_route "merge-gate dispatch needs a pull request number" none \
|
|
165
183
|
EVENT=workflow_dispatch OPERATION=merge-gate INPUT_PR_NUMBER=0
|
|
166
184
|
assert_route "merge-gate dispatch accepts a positive pull request" merge-gate \
|
|
167
185
|
EVENT=workflow_dispatch OPERATION=merge-gate INPUT_PR_NUMBER=7
|
|
168
|
-
assert_route "stale-recovery dispatch needs no numbers" stale-recovery \
|
|
169
|
-
EVENT=workflow_dispatch OPERATION=stale-recovery
|
|
170
186
|
assert_route "reconcile-bot-pr-runs dispatch needs no numbers" reconcile-bot-pr-runs \
|
|
171
187
|
EVENT=workflow_dispatch OPERATION=reconcile-bot-pr-runs
|
|
172
188
|
assert_route "an unknown operation routes nowhere" none \
|
|
173
189
|
EVENT=workflow_dispatch OPERATION=deploy-everything
|
|
174
190
|
assert "a scheduled audit reports its trigger kind" scheduled \
|
|
175
191
|
"$(route_field trigger-kind EVENT=schedule "SCHEDULE=17 1 * * 1")"
|
|
192
|
+
|
|
176
193
|
assert "a dispatched audit reports its trigger kind" manual \
|
|
177
194
|
"$(route_field trigger-kind EVENT=workflow_dispatch OPERATION=audit INPUT_TRIGGER_KIND=manual)"
|
|
178
|
-
assert_route "propose dispatch needs no numbers" propose \
|
|
179
|
-
EVENT=workflow_dispatch OPERATION=propose
|
|
180
|
-
assert "a scheduled propose reports its trigger kind" scheduled \
|
|
181
|
-
"$(route_field trigger-kind EVENT=schedule "SCHEDULE=29 7 * * *")"
|
|
182
|
-
assert "a dispatched propose reports its trigger kind" manual \
|
|
183
|
-
"$(route_field trigger-kind EVENT=workflow_dispatch OPERATION=propose INPUT_TRIGGER_KIND=manual)"
|
|
184
195
|
|
|
185
196
|
echo "── Router wiring ─────────────────────────────────────────────────────────"
|
|
197
|
+
|
|
198
|
+
# GitHub evaluates every Actions expression in a workflow file, including ones written inside
|
|
199
|
+
# shell comments. An empty pair is not a valid expression and fails the whole file to parse,
|
|
200
|
+
# with an error that points at a line number rather than saying what is wrong. Prose about
|
|
201
|
+
# expressions must not contain one.
|
|
202
|
+
empty_expr=$(grep -rl -e '${{[[:space:]]*}}' "${HERE}/../../workflows"/*.yml "${HERE}/../../workflows"/*.md 2>/dev/null || true)
|
|
203
|
+
if [ -z "$empty_expr" ]; then
|
|
204
|
+
PASS=$((PASS + 1))
|
|
205
|
+
else
|
|
206
|
+
FAIL=$((FAIL + 1))
|
|
207
|
+
echo "FAIL: workflow files contain an empty Actions expression:" >&2
|
|
208
|
+
printf ' %s
|
|
209
|
+
' $empty_expr >&2
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# A hyphen inside a ${{ }} property path is parsed as subtraction, so the reference silently
|
|
214
|
+
# resolves to nothing and the rendered prompt keeps the raw expression. Underscores only.
|
|
215
|
+
if ! grep -qE 'needs\.[a-z_]+\.outputs\.[a-zA-Z0-9_]*-' "$IMPLEMENT_WORKER_MD"; then
|
|
216
|
+
PASS=$((PASS + 1))
|
|
217
|
+
else
|
|
218
|
+
FAIL=$((FAIL + 1))
|
|
219
|
+
echo "FAIL: implement worker reads a hyphenated job output inside an expression" >&2
|
|
220
|
+
grep -nE 'needs\.[a-z_]+\.outputs\.[a-zA-Z0-9_]*-' "$IMPLEMENT_WORKER_MD" >&2
|
|
221
|
+
fi
|
|
222
|
+
|
|
223
|
+
# A worker that prints a ${VERIFY_COMMANDS_*} block without setting it renders an empty command
|
|
224
|
+
# block, and the model invents its own build line. That is how a child shipped `dotnet build
|
|
225
|
+
# --no-restore` against an unrestored workspace. The commands are split per area so a change
|
|
226
|
+
# that only touches apps/web does not pay for a cold Release build of the API.
|
|
227
|
+
VERIFY_OK=1
|
|
228
|
+
for VAR in VERIFY_COMMANDS_API VERIFY_COMMANDS_WEB; do
|
|
229
|
+
if grep -q "env\.$VAR" "$IMPLEMENT_WORKER_MD" && ! grep -q "^ $VAR:" "$IMPLEMENT_WORKER_MD"; then
|
|
230
|
+
VERIFY_OK=0
|
|
231
|
+
echo "FAIL: implement worker prints $VAR without defining it" >&2
|
|
232
|
+
fi
|
|
233
|
+
done
|
|
234
|
+
if grep -qE 'env\.VERIFY_COMMANDS[^_]' "$IMPLEMENT_WORKER_MD"; then
|
|
235
|
+
VERIFY_OK=0
|
|
236
|
+
echo "FAIL: implement worker still references the unscoped VERIFY_COMMANDS" >&2
|
|
237
|
+
fi
|
|
238
|
+
if [ "$VERIFY_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
239
|
+
|
|
186
240
|
if grep -Fq 'protected-files: allowed' "$IMPLEMENT_WORKER_MD" &&
|
|
187
|
-
grep -Fq '
|
|
241
|
+
grep -Fq 'protected-files: allowed' "$MERGE_GATE_WORKER_MD" &&
|
|
242
|
+
grep -Fq "needs.protected_changes.outputs.requires_review != 'true' || needs.subject.outputs.conclusion == 'failure'" "$MERGE_GATE_WORKER_MD"; then
|
|
188
243
|
PASS=$((PASS + 1))
|
|
189
244
|
else
|
|
190
245
|
FAIL=$((FAIL + 1))
|
|
191
|
-
echo "FAIL: protected changes
|
|
246
|
+
echo "FAIL: protected changes must allow failed-CI repair while remaining held from merge" >&2
|
|
192
247
|
fi
|
|
193
248
|
|
|
194
249
|
# This repository is public. Every route a human can start from a comment, a review or a
|
|
195
250
|
# label must pass the authorize gate, or anyone able to comment can start a model run that
|
|
196
251
|
# writes code. Asserted here because removing the gate would otherwise be a silent, one-line
|
|
197
252
|
# change that nothing fails on.
|
|
198
|
-
for route in refine implement
|
|
253
|
+
for route in refine implement apply-review; do
|
|
199
254
|
if grep -qE "route == '${route}'.*needs\.authorize\.outputs\.trusted == 'true'" "$ROUTER_YML"; then
|
|
200
255
|
PASS=$((PASS + 1))
|
|
201
256
|
else
|
|
@@ -215,8 +270,8 @@ else
|
|
|
215
270
|
echo "FAIL: route 'triage' does not dispatch outside collaborators and require a trusted worker actor" >&2
|
|
216
271
|
fi
|
|
217
272
|
|
|
218
|
-
for route in refine implement
|
|
219
|
-
audit-close cleanup-artifacts reconcile-bot-pr-runs
|
|
273
|
+
for route in refine implement triage apply-review merge-gate audit bot-approve \
|
|
274
|
+
audit-close cleanup-artifacts reconcile-bot-pr-runs validate release; do
|
|
220
275
|
if grep -q "route == '${route}'" "$ROUTER_YML"; then
|
|
221
276
|
PASS=$((PASS + 1))
|
|
222
277
|
else
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// Managed by @plainconceptsplatform/workflows. Source: loops/scripts/compile-agent-workflows.mjs. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
+
|
|
3
|
+
const OPENCODE_VERSION = '1.18.23'
|
|
2
4
|
import { spawnSync } from "node:child_process";
|
|
3
5
|
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
6
|
import { join } from "node:path";
|
|
@@ -35,13 +37,200 @@ for (const file of readdirSync(workflowDirectory)) {
|
|
|
35
37
|
const path = join(workflowDirectory, file);
|
|
36
38
|
const content = readFileSync(path, "utf8");
|
|
37
39
|
const patched = content
|
|
38
|
-
.replaceAll("opencode run --print-logs --log-level DEBUG", "opencode run --
|
|
39
|
-
.replaceAll("opencode run --print-logs --log-level ERROR", "opencode run --
|
|
40
|
-
.replaceAll("opencode run --log-level ERROR", "opencode run --
|
|
40
|
+
.replaceAll("opencode run --print-logs --log-level DEBUG", "opencode run --log-level ERROR")
|
|
41
|
+
.replaceAll("opencode run --print-logs --log-level ERROR", "opencode run --log-level ERROR")
|
|
42
|
+
.replaceAll("opencode run --log-level ERROR", "opencode run --log-level ERROR")
|
|
41
43
|
.replaceAll("--log-level DEBUG", "--log-level ERROR")
|
|
42
44
|
.replace(/GH_AW_INFO_MODEL: "[^"]*"/g, 'GH_AW_INFO_MODEL: "per-agent"')
|
|
43
45
|
.replace(/OPENCODE_MODEL: [^\n]+/g, "OPENCODE_MODEL: ''")
|
|
44
|
-
.replace(/GH_AW_INFO_MODEL_COSTS: '[^']*'/g, 'GH_AW_INFO_MODEL_COSTS: \'{"providers":{}}\'')
|
|
46
|
+
.replace(/GH_AW_INFO_MODEL_COSTS: '[^']*'/g, 'GH_AW_INFO_MODEL_COSTS: \'{"providers":{}}\'')
|
|
47
|
+
// The MCP gateway is the only thing the agent publishes on the host loopback, so it is the
|
|
48
|
+
// one thing two runners on the same machine cannot share. Honour an inherited port so each
|
|
49
|
+
// runner service can pick its own; everything else is namespaced by its Docker daemon.
|
|
50
|
+
// awf names its containers awf-agent, awf-squid and awf-api-proxy with no way to change
|
|
51
|
+
// them, so two agent jobs on one host recreate each other's containers and the first dies
|
|
52
|
+
// with exit 137. Giving each runner its own user and daemon isolated the containers but
|
|
53
|
+
// broke everything the runners share through the filesystem: awf and gh-aw both write
|
|
54
|
+
// fixed-name files into /tmp, and a file one user creates cannot be overwritten by the
|
|
55
|
+
// next. Six separate failures came from that. One host lock is the smaller trade: agent
|
|
56
|
+
// jobs run one at a time, every other job still uses all the runners, and every user is
|
|
57
|
+
// the same again. The chain is serial anyway, so in practice this only queues one consumer behind
|
|
58
|
+
// the other. Held for the whole agent run, which is deliberate. The lock lives outside the
|
|
59
|
+
// /tmp/gh-aw namespace on purpose: the keying rewrite below would otherwise make the lock
|
|
60
|
+
// file per job, which is silently no lock at all.
|
|
61
|
+
// Agent jobs run in parallel again. Each runner has its own Docker-in-Docker daemon, so
|
|
62
|
+
// awf's fixed container names (awf-agent, awf-squid, awf-api-proxy, awmg-mcpg) live in
|
|
63
|
+
// separate namespaces and cannot recreate each other. gh-aw detects the tcp DOCKER_HOST
|
|
64
|
+
// and switches to its ARC/DinD path by itself, so no flag is needed here. The host-wide
|
|
65
|
+
// flock this replaces made every agent job wait for the previous one.
|
|
66
|
+
// Three host-global resources were left, and concurrent agent jobs fought over all of them.
|
|
67
|
+
// The global npm install rewrote the opencode binary another job was executing and killed it
|
|
68
|
+
// with SIGKILL mid-run; the warm server and its data directory were shared by every runner.
|
|
69
|
+
//
|
|
70
|
+
// All of these sites are step-level run: or env:, where the runner context is available. It
|
|
71
|
+
// is not available at workflow level, which is what broke the staging path earlier.
|
|
72
|
+
//
|
|
73
|
+
// Install only when the pinned version is missing, and take a lock so two jobs starting at
|
|
74
|
+
// once cannot both write the global prefix.
|
|
75
|
+
// gh-aw pins opencode-ai 1.2.14 at every release checked, up to v0.87.5. That version is
|
|
76
|
+
// from 2026-02-25. The harness expects 1.18.9 and this is the current release, so a gh-aw
|
|
77
|
+
// upgrade does not move it and the pin is ours to choose.
|
|
78
|
+
// gh-aw defaults NPM_CONFIG_MIN_RELEASE_AGE to 3 days, which blocks any package
|
|
79
|
+
// published in the last three days with ETARGET "No matching version found ... with a
|
|
80
|
+
// date before". Lowered to 1 so a fix released yesterday is usable. This is a
|
|
81
|
+
// supply-chain cooldown: shortening it accepts a newer package sooner.
|
|
82
|
+
.replace(/NPM_CONFIG_MIN_RELEASE_AGE: ['\"]?3['\"]?/g, "NPM_CONFIG_MIN_RELEASE_AGE: '1'")
|
|
83
|
+
// The MCP gateway runs inside a per-runner Docker-in-Docker daemon. gh-aw publishes it on
|
|
84
|
+
// 127.0.0.1, which is the DinD container's own loopback, while the outer publish forwards to
|
|
85
|
+
// that container's eth0, so the two never meet and gh-aw's health check fails 120 times with
|
|
86
|
+
// ECONNREFUSED. Binding 0.0.0.0 inside the daemon connects them. The host still exposes the
|
|
87
|
+
// port on loopback only, because the DinD container is published as 127.0.0.1:<port>.
|
|
88
|
+
.replaceAll("-p 127.0.0.1:'\"${MCP_GATEWAY_PORT}\"':", "-p '\"${MCP_GATEWAY_PORT}\"':")
|
|
89
|
+
// Each runner needs its own gateway port: four DinD daemons publish through to one host
|
|
90
|
+
// loopback, so two jobs on 8080 would collide. The runner sets it in .env.
|
|
91
|
+
// awf chroots into the Docker daemon's filesystem and requires it to be the runner's own
|
|
92
|
+
// glibc host. A Docker-in-Docker daemon fails with "Detected Alpine/musl host filesystem
|
|
93
|
+
// under /host", and no dind image fixes it: the agent needs the runner's toolchain and
|
|
94
|
+
// workspace, which a separate daemon container never has. So every agent job shares one
|
|
95
|
+
// daemon, awf's container names are fixed, and the jobs must be serialised.
|
|
96
|
+
.replaceAll(' awf --config', ' flock /tmp/agentic-awf.lock awf --config')
|
|
97
|
+
.replaceAll('export MCP_GATEWAY_PORT="8080"', 'export MCP_GATEWAY_PORT="${MCP_GATEWAY_PORT:-8080}"')
|
|
98
|
+
// The agent's safe outputs land in safeoutputs.jsonl, but the collector reads
|
|
99
|
+
// agent_output.json, and nothing in the generated workflow converts one to the other.
|
|
100
|
+
// With the OpenCode engine the tool calls go through the MCP server rather than stdout,
|
|
101
|
+
// so the log parser never produces agent_output.json and the placeholder step writes
|
|
102
|
+
// {"items":[]} over a run that did real work: the job goes green and the pull request
|
|
103
|
+
// is silently dropped. Build the collector's input from what the agent actually emitted.
|
|
104
|
+
.replaceAll("echo '{\"items\":[]}' > /tmp/gh-aw-${{ github.run_id }}/agent_output.json", "if [ -s /tmp/gh-aw-${{ github.run_id }}/safeoutputs.jsonl ]; then jq -s '{items: .}' /tmp/gh-aw-${{ github.run_id }}/safeoutputs.jsonl > /tmp/gh-aw-${{ github.run_id }}/agent_output.json; else echo '{\"items\":[]}' > /tmp/gh-aw-${{ github.run_id }}/agent_output.json; fi")
|
|
105
|
+
// The "Copy Safe Outputs" step runs on the host after the awf container exits. At that
|
|
106
|
+
// point /tmp/gh-aw/ IS the host's /tmp/gh-aw/, where the safeoutputs MCP container wrote
|
|
107
|
+
// patch and bundle files (via the MCP gateway's -v /tmp:/tmp:rw mount). Extend the step
|
|
108
|
+
// to also copy those files to RUNNER_TEMP/gh-aw/ so the upload step can include them.
|
|
109
|
+
// Without this, the upload step's glob /tmp/gh-aw/aw-*.patch runs inside the awf chroot
|
|
110
|
+
// (where /tmp/gh-aw is a different path) and finds nothing. The safe_outputs and conclude
|
|
111
|
+
// jobs then receive no patches in the artifact and silently skip the push.
|
|
112
|
+
.replace(
|
|
113
|
+
' cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true\n',
|
|
114
|
+
' cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true\n' +
|
|
115
|
+
' # Copy patch/bundle files generated by the safeoutputs MCP container to\n' +
|
|
116
|
+
' # RUNNER_TEMP/gh-aw/ so the upload step includes them in the artifact.\n' +
|
|
117
|
+
' mkdir -p "${RUNNER_TEMP}/gh-aw"\n' +
|
|
118
|
+
' ls -la /tmp/gh-aw/ 2>/dev/null || true\n' +
|
|
119
|
+
' cp /tmp/gh-aw/aw-*.patch "${RUNNER_TEMP}/gh-aw/" 2>/dev/null || true\n' +
|
|
120
|
+
' cp /tmp/gh-aw/aw-*.bundle "${RUNNER_TEMP}/gh-aw/" 2>/dev/null || true\n' +
|
|
121
|
+
' ls -la "${RUNNER_TEMP}/gh-aw/"aw-* 2>/dev/null || true\n')
|
|
122
|
+
// Add patch/bundle paths to the upload glob so the agent artifact carries what
|
|
123
|
+
// push-agent-branch needs. The anchor is the two adjacent path lines of the
|
|
124
|
+
// "...agent" upload step; the "...agent-output-fallback" upload lists them in the
|
|
125
|
+
// opposite order, so it cannot match. Anchoring matters: the first bare
|
|
126
|
+
// '/tmp/gh-aw/agent_output.json\n' in the file is the placeholder step's echo line,
|
|
127
|
+
// and replacing there corrupted the placeholder into executing the globs as
|
|
128
|
+
// commands while the upload list stayed without bundles. The artifact then carried
|
|
129
|
+
// no bundle, push-agent-branch silently skipped the push, and the gate reported
|
|
130
|
+
// "remediated" on a pull request that was never touched.
|
|
131
|
+
// Only the /tmp/gh-aw globs may be added: a ${{ runner.temp }} path as well would
|
|
132
|
+
// move the upload's least-common-ancestor from /tmp/gh-aw to /, and every file in
|
|
133
|
+
// the artifact (agent_output.json included) would gain a prefix that
|
|
134
|
+
// download-agent-output does not look for.
|
|
135
|
+
.replace(
|
|
136
|
+
' /tmp/gh-aw/safeoutputs.jsonl\n /tmp/gh-aw/agent_output.json\n',
|
|
137
|
+
' /tmp/gh-aw/safeoutputs.jsonl\n' +
|
|
138
|
+
' /tmp/gh-aw/aw-*.patch\n' +
|
|
139
|
+
' /tmp/gh-aw/aw-*.bundle\n' +
|
|
140
|
+
' /tmp/gh-aw/agent_output.json\n')
|
|
141
|
+
|
|
142
|
+
// v0.87.5's arc-dind mode stages the engine CLI to a daemon-visible path but assumes the
|
|
143
|
+
// Copilot engine: command -v copilot is empty under engine: opencode and cp "" fails the
|
|
144
|
+
// job before the agent starts. OpenCode needs no staging, because npm -g installs it under
|
|
145
|
+
// the tool-cache prefix inside _work, which the dind daemon shares.
|
|
146
|
+
// Centralised AgentMemory: an always-on App Service; the shim proxies to it when the
|
|
147
|
+
// URL is set and authenticates with the HMAC secret. Local development leaves both
|
|
148
|
+
// unset and keeps the local per-run store.
|
|
149
|
+
.replace(/^env:\n/m, 'env:\n AGENTMEMORY_URL: https://agentmemory-pro-01.azurewebsites.net\n')
|
|
150
|
+
// the secret is scoped to the one step that runs the agent (semgrep: a secret in
|
|
151
|
+
// workflow-level env is visible to every job and step)
|
|
152
|
+
.replaceAll(' OPENAI_BASE_URL: https://forge.plainconcepts.com/v1',
|
|
153
|
+
' OPENAI_BASE_URL: https://forge.plainconcepts.com/v1' + '\n' +
|
|
154
|
+
' AGENTMEMORY_SECRET: ${{ secrets.AGENTMEMORY_SECRET }}')
|
|
155
|
+
// the router passes secrets explicitly (semgrep flags secrets: inherit as over-broad),
|
|
156
|
+
// so the callee must declare everything it accepts
|
|
157
|
+
.replace(/^ COPILOT_GITHUB_TOKEN:/m,
|
|
158
|
+
' AGENTMEMORY_SECRET:' + '\n' + ' required: false' + '\n' + ' COPILOT_GITHUB_TOKEN:')
|
|
159
|
+
.replaceAll(' COPILOT_SRC="$(command -v copilot)"',
|
|
160
|
+
' command -v copilot >/dev/null 2>&1 || { echo "no copilot binary (engine is opencode); skipping"; exit 0; }' + '\n' +
|
|
161
|
+
' COPILOT_SRC="$(command -v copilot)"')
|
|
162
|
+
// "A failed run is already a red run": report-failure-as-issue: false silences one
|
|
163
|
+
// reporter, but gh-aw hardcodes a second one (report_failed_jobs) that files an
|
|
164
|
+
// "[aw] Failed jobs" issue per red run. Same philosophy, same off switch.
|
|
165
|
+
.replaceAll('GH_AW_REPORT_FAILED_JOBS: "true"', 'GH_AW_REPORT_FAILED_JOBS: "false"')
|
|
166
|
+
// gh-aw sees a custom step calling dotnet and injects its own setup-dotnet, which carries
|
|
167
|
+
// no DOTNET_INSTALL_DIR and so tries to write /usr/share/dotnet. The runner user has no
|
|
168
|
+
// sudo, so the job dies with "Permission denied" before the agent starts. The shared
|
|
169
|
+
// baseline step already redirects to the tool cache; this gives every other one the same
|
|
170
|
+
// env, including ones a future gh-aw release injects.
|
|
171
|
+
.replace(
|
|
172
|
+
/^( +)- name: Setup \.NET\n\1 uses: actions\/setup-dotnet@[^\n]*\n\1 with:\n((?:\1 [^\n]*\n)+)(?!\1 env:)/gm,
|
|
173
|
+
(_m, indent, withBody) =>
|
|
174
|
+
indent + '- name: Setup .NET\n' + indent + ' uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0\n' +
|
|
175
|
+
indent + ' with:\n' + withBody +
|
|
176
|
+
indent + ' env:\n' + indent + ' DOTNET_INSTALL_DIR: ${{ runner.tool_cache }}/dotnet\n')
|
|
177
|
+
.replace(/opencode-ai@[0-9][0-9.]*/g, 'opencode-ai@' + OPENCODE_VERSION)
|
|
178
|
+
// gh-aw installs with --ignore-scripts, which blocks opencode's own postinstall. That
|
|
179
|
+
// script downloads the platform binary, so 1.18 fails at first use with "opencode-ai's
|
|
180
|
+
// postinstall script was not run". 1.2.14 did not need it. Run only opencode's postinstall
|
|
181
|
+
// explicitly, so every other package's scripts stay blocked.
|
|
182
|
+
.replace(
|
|
183
|
+
/npm install --ignore-scripts -g opencode-ai@([0-9][0-9.]*)/g,
|
|
184
|
+
(_m, v) =>
|
|
185
|
+
"opencode --version 2>/dev/null | grep -qF '" + v + "' || " +
|
|
186
|
+
"flock /tmp/opencode-install.lock sh -c 'npm install --ignore-scripts -g opencode-ai@" + v +
|
|
187
|
+
" && cd \"$(npm root -g)/opencode-ai\" && node postinstall.mjs' # opencode-ai@" + v)
|
|
188
|
+
// One warm server per runner, on its own port, with its own data directory. Keyed on the
|
|
189
|
+
// runner rather than the run so a runner keeps its server warm between its own jobs.
|
|
190
|
+
// No double quotes here: the enclosing run: is a double-quoted YAML scalar, so a quote
|
|
191
|
+
// would have to be escaped. The default has no spaces, so bare is safe shell.
|
|
192
|
+
.replace(/\/tmp\/opencode-data(?!-\$\{\{)/g, () => '/tmp/opencode-data-${{ runner.name }}');
|
|
193
|
+
|
|
194
|
+
let rewritten = patched;
|
|
195
|
+
// A queued implement executes hours after its run was created, but actions/checkout
|
|
196
|
+
// without a ref checks out github.sha, which GitHub pins at run creation. Five children
|
|
197
|
+
// queued together at 02:01 all carried the 02:01 base; once the first sibling merged,
|
|
198
|
+
// every later push became a rebase onto a moved parent, which gh-aw's signed-commit
|
|
199
|
+
// push cannot do (it dies with "cannot rebase: You have unstaged changes" and files the
|
|
200
|
+
// pull request as an issue instead of a pull request). Checking out the branch tip at
|
|
201
|
+
// execution time makes the commit parent current main; the implement-global queue
|
|
202
|
+
// already keeps sibling merges out of the window while the agent runs.
|
|
203
|
+
if (file === "agent-implement.lock.yml") {
|
|
204
|
+
const start = rewritten.indexOf("\n agent:\n");
|
|
205
|
+
const rest = start === -1 ? "" : rewritten.slice(start + 1);
|
|
206
|
+
const next = rest.search(/\n [a-z_][a-zA-Z_-]*:\n/);
|
|
207
|
+
if (start !== -1 && next !== -1) {
|
|
208
|
+
const end = start + 1 + next;
|
|
209
|
+
const span = rewritten.slice(start, end).replace(
|
|
210
|
+
/(uses: actions\/checkout@[^\n]*\n(\s+)with:\n(?:\2 [^\n]+\n)*)/,
|
|
211
|
+
(whole, _block, indent) =>
|
|
212
|
+
whole.includes("ref:") ? whole : whole + indent + " ref: ${{ github.event.repository.default_branch }}\n",
|
|
213
|
+
);
|
|
214
|
+
rewritten = rewritten.slice(0, start) + span + rewritten.slice(end);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Guard the two rewrites the compiled agent jobs cannot work without. The anchors are
|
|
219
|
+
// upstream template text: when a gh-aw upgrade reshapes either step, the silent
|
|
220
|
+
// failure mode returns (an artifact with no bundle, a skipped push, a green gate on
|
|
221
|
+
// an untouched pull request). Failing the compile is the only loud signal there is.
|
|
222
|
+
for (const check of [
|
|
223
|
+
{ name: "bundle upload glob", ok: /\n( +)- name: Upload agent artifacts\n\1 if: always\(\)\n\1 continue-on-error: true\n\1 uses: actions\/upload-artifact@[^\n]*\n\1 with:\n\1 name: \$\{\{ needs\.activation\.outputs\.artifact_prefix \}\}agent\n\1 path: \|\n(?:\1 [^\n]*\n)*\1 \/tmp\/gh-aw\/aw-\*\.bundle\n/ },
|
|
224
|
+
{ name: "placeholder step intact", ok: /\n( +)- name: Write agent output placeholder if missing\n\1 if: always\(\)\n\1 run: \|\n\1 if \[ ! -f \/tmp\/gh-aw\/agent_output\.json \]; then\n\1 echo '\{"items":\[\]\}' > \/tmp\/gh-aw\/agent_output\.json\n\1 fi\n/ },
|
|
225
|
+
]) {
|
|
226
|
+
if (!check.ok.test(rewritten)) {
|
|
227
|
+
process.stderr.write(
|
|
228
|
+
`compile patch "${check.name}" no longer matches in ${file}: the gh-aw template changed. ` +
|
|
229
|
+
`Re-anchor the rewrite in loops/scripts/compile-agent-workflows.mjs, or agent pushes are silently dropped.\n`,
|
|
230
|
+
);
|
|
231
|
+
process.exitCode = 1;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
45
234
|
|
|
46
|
-
if (
|
|
47
|
-
}
|
|
235
|
+
if (rewritten !== content) writeFileSync(path, rewritten);
|
|
236
|
+
}
|