@plainconceptsplatform/workflows 1.9.0 → 1.10.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/README.md +1 -1
- package/dist/catalog-installation.js +2 -2
- package/dist/package-baseline.js +4 -1
- package/dist/route-processing.js +4 -1
- package/dist/worker-env.d.ts +1 -1
- package/dist/worker-env.js +6 -2
- package/dist/workflow-catalog.d.ts +3 -1
- package/dist/workflow-catalog.js +6 -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/audit-close/action.yml +13 -6
- package/loops/actions/classify-route/classify-route.sh +3 -1
- package/loops/actions/cleanup-artifacts/action.yml +91 -91
- package/loops/actions/close-agent-issues/action.yml +43 -43
- package/loops/actions/collect-doc-drift/action.yml +29 -0
- package/loops/actions/collect-doc-drift/collect-doc-drift.mjs +133 -0
- package/loops/actions/create-issue-comment/action.yml +29 -29
- package/loops/actions/download-agent-output/action.yml +53 -53
- package/loops/actions/identify-gate-subject/action.yml +4 -0
- package/loops/actions/land-pull-request/action.yml +10 -0
- package/loops/actions/land-pull-request/land-pull-request.sh +14 -0
- 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/remove-issue-labels/action.yml +37 -37
- package/loops/actions/report-workflow-errors/action.yml +1 -1
- package/loops/actions/update-agent-issues/action.yml +58 -58
- package/loops/actions/validate-refine-output/action.yml +6 -1
- package/loops/actions/validate-refine-output/validate-refine-output.sh +20 -2
- 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-route-matrix/action.yml +9 -9
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +38 -5
- package/loops/docs/diagrams.md +45 -2
- package/loops/templates/agentics/agentics-maintenance.yml +121 -121
- package/loops/templates/issues/bug_report.yml +109 -109
- package/loops/templates/issues/feature_request.yml +75 -75
- package/loops/workflows/agent-docs-audit.md +266 -0
- package/loops/workflows/agent-merge-gate.md +11 -7
- package/loops/workflows/agent-refine.md +61 -11
- package/loops/workflows/shared/platform-defaults.md +53 -53
- package/loops/workflows/work-router.yml +120 -3
- package/package.json +1 -1
|
@@ -25,7 +25,9 @@ env:
|
|
|
25
25
|
RISK_INDICATORS: "Any diff touching authentication, authorization or session handling. Any change to a calculation or pricing engine, or to code handling money. Any database migration, or a change to an entity or schema. Any change to an audit or event log, or anything that could break its continuity. Any change to a public API contract or a shared library other repositories consume."
|
|
26
26
|
# Paths a bot may change but never merge on its own: an extended regular expression matched
|
|
27
27
|
# against every changed path in the pull request. A match holds the merge for a human; it does
|
|
28
|
-
# not stop the agent repairing failed CI on the same files.
|
|
28
|
+
# not stop the agent repairing failed CI on the same files. A pull request a person with write
|
|
29
|
+
# access opened is not held for them: that person could push the same change to the branch, and
|
|
30
|
+
# GitHub does not let them approve their own pull request, so the hold only stranded it.
|
|
29
31
|
#
|
|
30
32
|
# Only the automation itself. This list used to hold every dependency manifest, lockfile and
|
|
31
33
|
# dotted directory as well, and with a size threshold and an owner list as wide it parked most
|
|
@@ -188,6 +190,7 @@ jobs:
|
|
|
188
190
|
closes: ${{ steps.subject.outputs.closes }}
|
|
189
191
|
conclusion: ${{ steps.subject.outputs.conclusion }}
|
|
190
192
|
run-id: ${{ steps.subject.outputs.run-id }}
|
|
193
|
+
human_author: ${{ steps.subject.outputs.human-author }}
|
|
191
194
|
review_blocked: ${{ steps.review.outputs.review_blocked }}
|
|
192
195
|
pr_base: ${{ steps.stack.outputs.pr_base }}
|
|
193
196
|
stack_root: ${{ steps.stack.outputs.stack_root }}
|
|
@@ -311,7 +314,7 @@ jobs:
|
|
|
311
314
|
with:
|
|
312
315
|
token: ${{ github.token }}
|
|
313
316
|
pr-number: ${{ needs.subject.outputs.pr }}
|
|
314
|
-
protected-paths: ${{ env.PROTECTED_PATHS }}
|
|
317
|
+
protected-paths: ${{ needs.subject.outputs.human_author != 'true' && env.PROTECTED_PATHS || '' }}
|
|
315
318
|
owner-paths: ${{ env.OWNER_PATHS }}
|
|
316
319
|
sensitive-paths: ${{ env.SENSITIVE_PATHS }}
|
|
317
320
|
high-files: ${{ env.BLAST_HIGH_FILES }}
|
|
@@ -426,13 +429,13 @@ jobs:
|
|
|
426
429
|
body: |
|
|
427
430
|
${{ env.GATE_MARKER }}
|
|
428
431
|
PR #${{ needs.subject.outputs.pr }} changes protected files and cannot be auto-merged.
|
|
429
|
-
The `review` and `owner-review` labels are set
|
|
430
|
-
|
|
432
|
+
The `review` and `owner-review` labels are set. Anyone with write access can review it:
|
|
433
|
+
approving the pull request merges it.
|
|
431
434
|
|
|
432
435
|
Protected files:
|
|
433
436
|
${{ needs.protected_changes.outputs.files }}
|
|
434
437
|
|
|
435
|
-
|
|
438
|
+
Asked to review: `${{ needs.protected_changes.outputs.required_owners || 'nobody named in CODEOWNERS' }}`
|
|
436
439
|
|
|
437
440
|
**Verdict:** owner-review
|
|
438
441
|
- name: Replay the verification plan for the owner
|
|
@@ -613,7 +616,7 @@ jobs:
|
|
|
613
616
|
&& format('| Accepted unattended | {0} |', needs.validate_output.outputs.flags)
|
|
614
617
|
|| '' }}
|
|
615
618
|
${{ needs.validate_output.outputs.outcome == 'owner-review'
|
|
616
|
-
&& format('|
|
|
619
|
+
&& format('| Asked to review | `{0}` |', needs.protected_changes.outputs.required_owners != '' && needs.protected_changes.outputs.required_owners || 'nobody named in CODEOWNERS, see the matched paths above')
|
|
617
620
|
|| '' }}
|
|
618
621
|
|
|
619
622
|
Why this blast radius:
|
|
@@ -622,7 +625,8 @@ jobs:
|
|
|
622
625
|
```
|
|
623
626
|
|
|
624
627
|
${{ needs.subject.outputs.issue != '' && format('Findings and verification on the linked issue: #{0}.', needs.subject.outputs.issue) || 'Findings and verification are in the review comment above.' }} [View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
625
|
-
${{ env.EVIDENCE_BEFORE_REVIEW == 'true' && contains(fromJson('["human-review","owner-review"]'), needs.validate_output.outputs.outcome) && 'Screenshots of this head follow in their own comment if it carries a verification plan.
|
|
628
|
+
${{ env.EVIDENCE_BEFORE_REVIEW == 'true' && contains(fromJson('["human-review","owner-review"]'), needs.validate_output.outputs.outcome) && 'Screenshots of this head follow in their own comment if it carries a verification plan.' || '' }}
|
|
629
|
+
${{ contains(fromJson('["human-review","owner-review"]'), needs.validate_output.outputs.outcome) && 'Anyone with write access can review it: approving the pull request merges it.' || '' }}
|
|
626
630
|
- name: Read the head to land
|
|
627
631
|
id: landing_head
|
|
628
632
|
if: needs.validate_output.outputs.outcome == 'auto-merge' && (needs.subject.outputs.stack_root != 'true' || needs.subject.outputs.root_complete == 'true')
|
|
@@ -58,6 +58,17 @@ env:
|
|
|
58
58
|
DRAFT_MARKER: "<!-- agent-refine-draft -->"
|
|
59
59
|
INITIAL_MODE: first
|
|
60
60
|
RESPONSE_MODE: rerefine
|
|
61
|
+
# The pass the router starts when nobody answered the questions in time, or the author accepted
|
|
62
|
+
# them with a thumbs-up: every proposed answer is taken as given. See REFINE_ANSWER_TIMEOUT_HOURS
|
|
63
|
+
# in work-router.yml.
|
|
64
|
+
ASSUME_MODE: assume
|
|
65
|
+
ASSUMED_LABEL: assumed
|
|
66
|
+
# The sentence that opens the batched questions, and the two ways a question ends. The validator
|
|
67
|
+
# reads the first to decide the outcome; the router reads the other two to know whether the
|
|
68
|
+
# questions can be taken as answered.
|
|
69
|
+
QUESTIONS_LINE: "I have some questions about this issue"
|
|
70
|
+
PROPOSED_TAG: "**Proposed:**"
|
|
71
|
+
DECISION_TAG: "**Your decision needed:**"
|
|
61
72
|
MAX_SELF_QUESTIONS: "5"
|
|
62
73
|
TRIVIAL_MARKER: "<!-- complexity: trivial -->"
|
|
63
74
|
ESTIMATE_MARKER_PREFIX: "<!-- estimate: "
|
|
@@ -76,8 +87,9 @@ env:
|
|
|
76
87
|
GIT_COMMITTER_NAME: "github-actions[bot]"
|
|
77
88
|
GIT_COMMITTER_EMAIL: "github-actions[bot]@users.noreply.github.com"
|
|
78
89
|
description: |
|
|
79
|
-
Refines an issue into a user story, on a first pass
|
|
80
|
-
bot's questions
|
|
90
|
+
Refines an issue into a user story, on a first pass, after the author has answered the
|
|
91
|
+
bot's questions, or once the bot's proposed answers stand because nobody answered in time.
|
|
92
|
+
Replaces .loops/recipes/refine-loop.yaml.
|
|
81
93
|
|
|
82
94
|
The refined story is wrapped in the repository's own issue template when one matches.
|
|
83
95
|
When questions remain, the worker leaves that template-wrapped temporal draft on the
|
|
@@ -92,7 +104,7 @@ description: |
|
|
|
92
104
|
still parks an issue for a person.
|
|
93
105
|
|
|
94
106
|
Router-only worker: triggered exclusively via workflow_call from work-router.yml.
|
|
95
|
-
Contract inputs: issue-number, mode(first|rerefine).
|
|
107
|
+
Contract inputs: issue-number, mode(first|rerefine|assume).
|
|
96
108
|
|
|
97
109
|
name: "Agent: Refine Issue"
|
|
98
110
|
|
|
@@ -114,7 +126,7 @@ on:
|
|
|
114
126
|
required: true
|
|
115
127
|
type: string
|
|
116
128
|
mode:
|
|
117
|
-
description: Refinement pass mode (first or
|
|
129
|
+
description: Refinement pass mode (first, rerefine or assume).
|
|
118
130
|
required: false
|
|
119
131
|
type: string
|
|
120
132
|
default: first
|
|
@@ -177,9 +189,10 @@ jobs:
|
|
|
177
189
|
labels=$(gh issue view "$ISSUE_NUMBER" --repo "$GITHUB_REPOSITORY" --json labels \
|
|
178
190
|
--jq '[.labels[].name]')
|
|
179
191
|
if jq -e 'index("review")' >/dev/null <<<"$labels"; then
|
|
180
|
-
if [ "$MODE" = "rerefine" ]; then
|
|
192
|
+
if [ "$MODE" = "rerefine" ] || [ "$MODE" = "assume" ]; then
|
|
181
193
|
# A rerefine is triggered by a human comment on an issue the bot parked for
|
|
182
|
-
# review. The comment is the review: clear the label and proceed.
|
|
194
|
+
# review. The comment is the review: clear the label and proceed. An assume pass
|
|
195
|
+
# is the router's answer to the same park once the deadline passed.
|
|
183
196
|
gh issue edit "$ISSUE_NUMBER" --remove-label "review" 2>/dev/null ||
|
|
184
197
|
echo "::notice::review label already gone on #$ISSUE_NUMBER"
|
|
185
198
|
echo "eligible=true" >> "$GITHUB_OUTPUT"
|
|
@@ -323,6 +336,8 @@ jobs:
|
|
|
323
336
|
token: ${{ github.token }}
|
|
324
337
|
issue-number: ${{ inputs.issue-number }}
|
|
325
338
|
labels: ${{ env.WORKING_LABEL }}
|
|
339
|
+
# `assumed` says the router already spent its assume pass on this issue. A refinement from
|
|
340
|
+
# scratch earns a new one.
|
|
326
341
|
- name: Clear the human-needed flag
|
|
327
342
|
uses: ./.github/actions/remove-issue-labels
|
|
328
343
|
with:
|
|
@@ -331,6 +346,7 @@ jobs:
|
|
|
331
346
|
labels: |-
|
|
332
347
|
${{ env.REVIEW_LABEL }}
|
|
333
348
|
${{ env.STALLED_LABEL }}
|
|
349
|
+
${{ inputs.mode == env.INITIAL_MODE && env.ASSUMED_LABEL || '' }}
|
|
334
350
|
validate_output:
|
|
335
351
|
needs: [activation, agent, safe_outputs]
|
|
336
352
|
if: >
|
|
@@ -362,6 +378,7 @@ jobs:
|
|
|
362
378
|
draft-marker: ${{ env.DRAFT_MARKER }}
|
|
363
379
|
comment-prefix: ${{ env.SAFE_OUTPUT_COMMENT_PREFIX }}
|
|
364
380
|
issue-number: ${{ inputs.issue-number }}
|
|
381
|
+
questions-line: ${{ env.QUESTIONS_LINE }}
|
|
365
382
|
conclude:
|
|
366
383
|
needs: [activation, agent, safe_outputs, validate_output]
|
|
367
384
|
# `always()` is load-bearing: without it GitHub prepends `success()`, which returns false
|
|
@@ -418,6 +435,7 @@ jobs:
|
|
|
418
435
|
labels: |-
|
|
419
436
|
${{ env.REFINED_LABEL }}
|
|
420
437
|
${{ env.IMPLEMENT_LABEL }}
|
|
438
|
+
${{ inputs.mode == env.ASSUME_MODE && env.ASSUMED_LABEL || '' }}
|
|
421
439
|
- name: Clear complete refinement labels
|
|
422
440
|
if: needs.validate_output.outputs.outcome == 'complete'
|
|
423
441
|
uses: ./.github/actions/remove-issue-labels
|
|
@@ -645,13 +663,18 @@ jobs:
|
|
|
645
663
|
gh issue edit "$child" --repo "$REPO" --add-label "$IMPLEMENT_LABEL" >/dev/null
|
|
646
664
|
echo "#$child handed to implement"
|
|
647
665
|
done
|
|
666
|
+
# An assume pass that still asks leaves `assumed` beside `review`, which the router reads as
|
|
667
|
+
# "already assumed once": the questions then wait for a person instead of being assumed
|
|
668
|
+
# again every working day.
|
|
648
669
|
- name: Flag questions for review
|
|
649
670
|
if: needs.validate_output.outputs.outcome == 'questions'
|
|
650
671
|
uses: ./.github/actions/add-issue-labels
|
|
651
672
|
with:
|
|
652
673
|
token: ${{ steps.app-token.outputs.token }}
|
|
653
674
|
issue-number: ${{ inputs.issue-number }}
|
|
654
|
-
labels:
|
|
675
|
+
labels: |-
|
|
676
|
+
${{ env.REVIEW_LABEL }}
|
|
677
|
+
${{ inputs.mode == env.ASSUME_MODE && env.ASSUMED_LABEL || '' }}
|
|
655
678
|
- name: Release questions for the author
|
|
656
679
|
if: needs.validate_output.outputs.outcome == 'questions'
|
|
657
680
|
uses: ./.github/actions/remove-issue-labels
|
|
@@ -903,7 +926,15 @@ timeout-minutes: 90
|
|
|
903
926
|
- On a `${{ env.RESPONSE_MODE }}` pass, incorporate only the supplied answers from the issue author or an
|
|
904
927
|
assignee. Do not use answers from other commenters. The body may already hold the
|
|
905
928
|
temporal draft from the earlier pass: reuse what still holds, and resolve its pending
|
|
906
|
-
marks with the author's answers.
|
|
929
|
+
marks with the author's answers. A question the author left unanswered keeps the answer
|
|
930
|
+
you proposed for it. A reply that accepts your proposals as a whole ("ok", "yes", "go
|
|
931
|
+
ahead", in any language) answers every question with its proposal.
|
|
932
|
+
- On a `${{ env.ASSUME_MODE }}` pass, nobody answered your last batch of questions in time, or the
|
|
933
|
+
author accepted it with a thumbs-up. Take each question's `${{ env.PROPOSED_TAG }}` answer as
|
|
934
|
+
given, except where the author or an assignee did answer in a comment: their answer wins.
|
|
935
|
+
Do not ask again: write the finished story. Add a section titled `Assumed without a reply`
|
|
936
|
+
at the end of the body, before any related links, listing each question in a short line
|
|
937
|
+
with the answer you took, so whoever reviews the pull request sees what nobody confirmed.
|
|
907
938
|
|
|
908
939
|
3. **Explore and write the story.** Split the issue into work units first. If the issue body is
|
|
909
940
|
a bullet list of distinct tasks, treat each bullet as its own work unit. Otherwise treat the
|
|
@@ -915,6 +946,14 @@ timeout-minutes: 90
|
|
|
915
946
|
issue. At most ${{ env.MAX_SELF_QUESTIONS }} self-asked questions per work unit, and stop
|
|
916
947
|
sooner once more exploring stops changing your understanding.
|
|
917
948
|
|
|
949
|
+
Answer your own questions from the codebase and the docs. Set one aside for the author when
|
|
950
|
+
it is a business or product decision the code cannot settle: which of two reasonable
|
|
951
|
+
behaviours the users want, who sees or may do something, a threshold or a limit, which figure
|
|
952
|
+
a calculation uses, what a message says to a customer. Choosing one of those yourself because
|
|
953
|
+
the code would allow either is not answering it; it is the question. A wrong guess costs a
|
|
954
|
+
pull request, a question costs a reply, and the author can accept every proposal with a
|
|
955
|
+
thumbs-up.
|
|
956
|
+
|
|
918
957
|
Then classify the change complexity. A change is **trivial** only if every one of these
|
|
919
958
|
holds: ${{ env.TRIVIAL_CRITERIA }}
|
|
920
959
|
|
|
@@ -1044,14 +1083,23 @@ timeout-minutes: 90
|
|
|
1044
1083
|
The comment carries:
|
|
1045
1084
|
1. `${{ env.REFINE_MARKER }}`
|
|
1046
1085
|
2. `${{ env.SAFE_OUTPUT_COMMENT_PREFIX }}`
|
|
1047
|
-
3.
|
|
1048
|
-
4. Every set-aside question, gathered from all work units,
|
|
1086
|
+
3. `${{ env.QUESTIONS_LINE }}. Each one carries the answer I would take. Reply in one comment to change any of them, or react with 👍 to accept them all.`
|
|
1087
|
+
4. Every set-aside question, gathered from all work units, as a numbered list, each answerable in a sentence.
|
|
1088
|
+
5. A last line saying that if nobody replies the proposed answers stand and refinement carries on, except for any question that needs a decision.
|
|
1089
|
+
|
|
1090
|
+
Every question ends in one of two ways, on its own line under the question:
|
|
1091
|
+
|
|
1092
|
+
- `${{ env.PROPOSED_TAG }} <the answer you would take> -- <why, in one short clause: the convention, the existing behaviour or the issue text it follows>`. This is the usual case: you can name a sensible answer, and getting it wrong costs a change, not a loss.
|
|
1093
|
+
- `${{ env.DECISION_TAG }} <why no answer can be assumed>`. Only for a question where a wrong assumption is hard to undo: data deleted or rewritten, money or prices, permissions or security, personal data, or a contract another team or system consumes. These are never assumed; refinement waits for a person.
|
|
1049
1094
|
|
|
1050
1095
|
Write the questions in **plain business language, not technical jargon**. The person reading
|
|
1051
1096
|
them is a domain expert, not an engineer.
|
|
1052
1097
|
|
|
1053
1098
|
**The story is complete.** You answered every exploration question yourself and none remain
|
|
1054
|
-
for the author.
|
|
1099
|
+
for the author. A complete story has no open question anywhere in it: if the issue form has an
|
|
1100
|
+
`Open questions` field, it says `None`, and a question you settled goes into the acceptance
|
|
1101
|
+
criteria as the behaviour you chose. A question still open in the body is a question for the
|
|
1102
|
+
author, so that is the outcome above, not this one. Call `update_issue` first, with the wrapped replacement body, and wait
|
|
1055
1103
|
for it to come back. Send the whole body in that one call: it is the only thing this
|
|
1056
1104
|
step has to get right.
|
|
1057
1105
|
|
|
@@ -1063,6 +1111,8 @@ timeout-minutes: 90
|
|
|
1063
1111
|
- If the array includes the exact label `future`: `Refinement complete. The implement label has been added. Implementation is paused until the future label is removed.`
|
|
1064
1112
|
- Otherwise: `Refinement complete. The implement label has been added and the implement workflow will start shortly.`
|
|
1065
1113
|
|
|
1114
|
+
On a `${{ env.ASSUME_MODE }}` pass, add one sentence after that message: `The proposed answers were taken because nobody replied in time, and they are listed under "Assumed without a reply". If one is wrong, say so on the pull request.`
|
|
1115
|
+
|
|
1066
1116
|
If `update_issue` did not succeed, do not post either message: an issue that reads as
|
|
1067
1117
|
refined with its body untouched is worse than one that says the run failed. Call
|
|
1068
1118
|
`report_incomplete` with what the tool told you, and let the run be retried.
|
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
---
|
|
2
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/shared/platform-defaults.md. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
3
|
-
description: Shared network and safe-output defaults for catalog agent workflows, plus the verification rules every worker that builds or pushes has to follow. The body below is runtime-imported into each worker's prompt ahead of its own steps.
|
|
4
|
-
|
|
5
|
-
network:
|
|
6
|
-
allowed:
|
|
7
|
-
- defaults
|
|
8
|
-
- forge.plainconcepts.com
|
|
9
|
-
# opencode fetches its model registry from models.opencode.ai at startup; without
|
|
10
|
-
# this host on the allowlist the fetch blocks bootstrap and the run stalls after
|
|
11
|
-
# "init" until the runner kills it. The apex "opencode.ai" is not enough.
|
|
12
|
-
- models.opencode.ai
|
|
13
|
-
- node
|
|
14
|
-
- github
|
|
15
|
-
- dotnet
|
|
16
|
-
- fonts
|
|
17
|
-
- login.microsoftonline.com
|
|
18
|
-
# centralised AgentMemory (App Service); the MCP shim needs it through the egress proxy
|
|
19
|
-
- agentmemory-pro-01.azurewebsites.net
|
|
20
|
-
|
|
21
|
-
safe-outputs:
|
|
22
|
-
threat-detection: false
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
## These runs are not interactive
|
|
26
|
-
|
|
27
|
-
- Nothing resumes you. A turn that ends with text and no tool call ends the whole run, and the work
|
|
28
|
-
is discarded, whatever the text promised. "I'll read the context files first." as a last message
|
|
29
|
-
is a finished run: the Pliny-Bot merge gate did exactly that on #801, ended in twenty-three
|
|
30
|
-
seconds with exit code 0 and no outcome, and was retried twice before the pull request was handed
|
|
31
|
-
to a person for nothing.
|
|
32
|
-
- So every message you write carries a tool call until the last one, which is the safe-output call
|
|
33
|
-
the task names. Say what you are about to do and do it in the same message. Never announce a step
|
|
34
|
-
and stop, never wait for a command, a reviewer or a "next turn".
|
|
35
|
-
- A command the bash tool reports as moved to the background is still running: poll its output file
|
|
36
|
-
with tool calls (`sleep 40; tail -n 15 <file>`) until it shows an exit status, then go on.
|
|
37
|
-
|
|
38
|
-
## Verification, for every worker that builds or pushes
|
|
39
|
-
|
|
40
|
-
- Scope every check to the files you changed. This is the runner, not a preference: a
|
|
41
|
-
whole-repository lint, build or test run exhausts its memory and gets killed mid-run, which
|
|
42
|
-
fails the job with no useful output. Pass the changed paths to the linter (`pnpm exec biome
|
|
43
|
-
check <files>`), build only the projects containing them, and run only the test project that
|
|
44
|
-
covers them. Escalate to the full suite only after the scoped run passes and only when the
|
|
45
|
-
change crosses a project boundary.
|
|
46
|
-
- Run nothing at all for a change that touches only documentation. A cold Release build on a
|
|
47
|
-
shared runner is minutes the run does not get back.
|
|
48
|
-
- Never make a check pass by weakening it: not a deleted test, not a lowered threshold, not a
|
|
49
|
-
skipped step. Fix the cause and run it again.
|
|
50
|
-
- Once the checks pass, run the project's lint fix command over the files you changed
|
|
51
|
-
(`pnpm lint:fix`, `pnpm exec biome check --write <changed-files>`, or its equivalent), or
|
|
52
|
-
correct what lint reports where no fix command exists. A branch that arrives with lint errors
|
|
53
|
-
gets sent back for them.
|
|
1
|
+
---
|
|
2
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/shared/platform-defaults.md. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
3
|
+
description: Shared network and safe-output defaults for catalog agent workflows, plus the verification rules every worker that builds or pushes has to follow. The body below is runtime-imported into each worker's prompt ahead of its own steps.
|
|
4
|
+
|
|
5
|
+
network:
|
|
6
|
+
allowed:
|
|
7
|
+
- defaults
|
|
8
|
+
- forge.plainconcepts.com
|
|
9
|
+
# opencode fetches its model registry from models.opencode.ai at startup; without
|
|
10
|
+
# this host on the allowlist the fetch blocks bootstrap and the run stalls after
|
|
11
|
+
# "init" until the runner kills it. The apex "opencode.ai" is not enough.
|
|
12
|
+
- models.opencode.ai
|
|
13
|
+
- node
|
|
14
|
+
- github
|
|
15
|
+
- dotnet
|
|
16
|
+
- fonts
|
|
17
|
+
- login.microsoftonline.com
|
|
18
|
+
# centralised AgentMemory (App Service); the MCP shim needs it through the egress proxy
|
|
19
|
+
- agentmemory-pro-01.azurewebsites.net
|
|
20
|
+
|
|
21
|
+
safe-outputs:
|
|
22
|
+
threat-detection: false
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## These runs are not interactive
|
|
26
|
+
|
|
27
|
+
- Nothing resumes you. A turn that ends with text and no tool call ends the whole run, and the work
|
|
28
|
+
is discarded, whatever the text promised. "I'll read the context files first." as a last message
|
|
29
|
+
is a finished run: the Pliny-Bot merge gate did exactly that on #801, ended in twenty-three
|
|
30
|
+
seconds with exit code 0 and no outcome, and was retried twice before the pull request was handed
|
|
31
|
+
to a person for nothing.
|
|
32
|
+
- So every message you write carries a tool call until the last one, which is the safe-output call
|
|
33
|
+
the task names. Say what you are about to do and do it in the same message. Never announce a step
|
|
34
|
+
and stop, never wait for a command, a reviewer or a "next turn".
|
|
35
|
+
- A command the bash tool reports as moved to the background is still running: poll its output file
|
|
36
|
+
with tool calls (`sleep 40; tail -n 15 <file>`) until it shows an exit status, then go on.
|
|
37
|
+
|
|
38
|
+
## Verification, for every worker that builds or pushes
|
|
39
|
+
|
|
40
|
+
- Scope every check to the files you changed. This is the runner, not a preference: a
|
|
41
|
+
whole-repository lint, build or test run exhausts its memory and gets killed mid-run, which
|
|
42
|
+
fails the job with no useful output. Pass the changed paths to the linter (`pnpm exec biome
|
|
43
|
+
check <files>`), build only the projects containing them, and run only the test project that
|
|
44
|
+
covers them. Escalate to the full suite only after the scoped run passes and only when the
|
|
45
|
+
change crosses a project boundary.
|
|
46
|
+
- Run nothing at all for a change that touches only documentation. A cold Release build on a
|
|
47
|
+
shared runner is minutes the run does not get back.
|
|
48
|
+
- Never make a check pass by weakening it: not a deleted test, not a lowered threshold, not a
|
|
49
|
+
skipped step. Fix the cause and run it again.
|
|
50
|
+
- Once the checks pass, run the project's lint fix command over the files you changed
|
|
51
|
+
(`pnpm lint:fix`, `pnpm exec biome check --write <changed-files>`, or its equivalent), or
|
|
52
|
+
correct what lint reports where no fix command exists. A branch that arrives with lint errors
|
|
53
|
+
gets sent back for them.
|
|
@@ -83,6 +83,9 @@ run-name: >-
|
|
|
83
83
|
&& inputs.operation == 'audit'
|
|
84
84
|
&& format('Agent: Audit - manual{0}', '')
|
|
85
85
|
|| github.event_name == 'workflow_dispatch'
|
|
86
|
+
&& inputs.operation == 'docs-audit'
|
|
87
|
+
&& format('Agent: Docs Audit - manual{0}', '')
|
|
88
|
+
|| github.event_name == 'workflow_dispatch'
|
|
86
89
|
&& inputs.operation == 'release'
|
|
87
90
|
&& format('Agent: Release{0}', '')
|
|
88
91
|
|| github.event_name == 'workflow_dispatch'
|
|
@@ -91,6 +94,9 @@ run-name: >-
|
|
|
91
94
|
&& github.event.schedule == '17 1 * * 1'
|
|
92
95
|
&& format('Agent: Audit - weekly{0}', '')
|
|
93
96
|
|| github.event_name == 'schedule'
|
|
97
|
+
&& github.event.schedule == '47 2 1,15 * *'
|
|
98
|
+
&& format('Agent: Docs Audit - fortnightly{0}', '')
|
|
99
|
+
|| github.event_name == 'schedule'
|
|
94
100
|
&& github.event.schedule == '43 3 * * *'
|
|
95
101
|
&& format('Agentics: Audit Close - daily{0}', '')
|
|
96
102
|
|| github.event_name == 'schedule'
|
|
@@ -125,6 +131,9 @@ env:
|
|
|
125
131
|
# first and the two of them consume the whole fleet. Pick a free slot per repository: see
|
|
126
132
|
# "Scheduling across repositories" in the package README.
|
|
127
133
|
AUDIT_CRON: "17 1 * * 1"
|
|
134
|
+
# The same idea for the documentation audit, if it is installed: see the same section of the
|
|
135
|
+
# package README. Two days a month, so a slot is a pair of days rather than a weekday.
|
|
136
|
+
DOCS_AUDIT_CRON: "47 2 1,15 * *"
|
|
128
137
|
# The bot identities whose label changes and pull requests this router trusts. Space
|
|
129
138
|
# separated, and every login the same account can present: GitHub reports an App as
|
|
130
139
|
# `name[bot]` on some endpoints and `app/name` on others, so both spellings belong here.
|
|
@@ -185,6 +194,12 @@ env:
|
|
|
185
194
|
# `pnpm run dev`, and a pull request with no plan costs one API call. Set it to "false" for a
|
|
186
195
|
# repository whose app cannot start on a runner; operation=evidence still works by hand.
|
|
187
196
|
EVIDENCE_ENABLED: "true"
|
|
197
|
+
# Working hours, Monday to Friday, that refine's questions wait for an answer. Every question
|
|
198
|
+
# carries the answer the bot would take; once this passes with no reply, or as soon as the
|
|
199
|
+
# author reacts with a thumbs-up, refine carries on with those answers and labels the issue
|
|
200
|
+
# `assumed`. A question marked as needing a decision is never assumed. "0" turns this off, and
|
|
201
|
+
# questions wait for a person however long that takes.
|
|
202
|
+
REFINE_ANSWER_TIMEOUT_HOURS: "24"
|
|
188
203
|
|
|
189
204
|
on:
|
|
190
205
|
issues:
|
|
@@ -221,6 +236,7 @@ on:
|
|
|
221
236
|
# Cron to route mapping lives in .github/actions/classify-route/classify-route.sh.
|
|
222
237
|
schedule:
|
|
223
238
|
- cron: "17 1 * * 1" # audit slot, mirrored from env.AUDIT_CRON by the installer
|
|
239
|
+
- cron: "47 2 1,15 * *" # docs-audit slot, mirrored from env.DOCS_AUDIT_CRON by the installer
|
|
224
240
|
- cron: "43 3 * * *"
|
|
225
241
|
- cron: "0 6 * * *"
|
|
226
242
|
- cron: "17 * * * *"
|
|
@@ -241,6 +257,7 @@ on:
|
|
|
241
257
|
- merge-train
|
|
242
258
|
- evidence
|
|
243
259
|
- audit
|
|
260
|
+
- docs-audit
|
|
244
261
|
- audit-close
|
|
245
262
|
- cleanup-artifacts
|
|
246
263
|
- reconcile-bot-pr-runs
|
|
@@ -256,12 +273,13 @@ on:
|
|
|
256
273
|
required: false
|
|
257
274
|
type: string
|
|
258
275
|
mode:
|
|
259
|
-
description: "Refine mode: first or
|
|
276
|
+
description: "Refine mode: first, rerefine or assume"
|
|
260
277
|
required: false
|
|
261
278
|
type: choice
|
|
262
279
|
options:
|
|
263
280
|
- first
|
|
264
281
|
- rerefine
|
|
282
|
+
- assume
|
|
265
283
|
ci-conclusion:
|
|
266
284
|
description: "CI conclusion for merge-gate dispatch"
|
|
267
285
|
required: false
|
|
@@ -684,7 +702,12 @@ jobs:
|
|
|
684
702
|
gh issue view "$feature" --repo "$REPO" --json body --jq '.body // ""' 2>/dev/null \
|
|
685
703
|
| grep -oiE 'blocked by #[0-9]+' | grep -oE '[0-9]+' || true
|
|
686
704
|
} | sort -u ); do
|
|
687
|
-
|
|
705
|
+
# A feature's own children are not what the feature waits on. Refine's tracker lists them
|
|
706
|
+
# in the feature body as "#N ... Blocked by #M", and reading those lines as the feature's
|
|
707
|
+
# blockers held every child behind its siblings, and the first one behind itself.
|
|
708
|
+
info=$(gh issue view "$b" --repo "$REPO" --json state,body 2>/dev/null) || continue
|
|
709
|
+
printf '%s' "$info" | jq -e '(.state // "" | ascii_downcase) == "open"' >/dev/null || continue
|
|
710
|
+
printf '%s' "$info" | jq -r '.body // ""' | grep -qF "<!-- split-parent: ${feature} -->" && continue
|
|
688
711
|
waiting="$waiting #$b"
|
|
689
712
|
done
|
|
690
713
|
fi
|
|
@@ -741,6 +764,7 @@ jobs:
|
|
|
741
764
|
gh issue edit "$ISSUE_NUMBER" --repo "$REPO" --remove-label bot-working >/dev/null 2>&1 || true
|
|
742
765
|
marker='<!-- stack-held -->'
|
|
743
766
|
if ! gh api "repos/$REPO/issues/$ISSUE_NUMBER/comments?per_page=100" --jq '.[].body' 2>/dev/null | grep -qF "$marker"; then
|
|
767
|
+
# shellcheck disable=SC2016
|
|
744
768
|
printf '%s\nHeld until%s have landed on the default branch: this feature builds on them. It keeps `implement` and is released when they merge.\n' "$marker" "$waiting" > /tmp/held.md
|
|
745
769
|
gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body-file /tmp/held.md >/dev/null || true
|
|
746
770
|
fi
|
|
@@ -1148,6 +1172,24 @@ jobs:
|
|
|
1148
1172
|
BOT_PRIVATE_KEY: ${{ secrets.BOT_PRIVATE_KEY }}
|
|
1149
1173
|
AGENTMEMORY_SECRET: ${{ secrets.AGENTMEMORY_SECRET }}
|
|
1150
1174
|
|
|
1175
|
+
call-docs-audit:
|
|
1176
|
+
needs: classify
|
|
1177
|
+
if: needs.classify.outputs.route == 'docs-audit'
|
|
1178
|
+
uses: ./.github/workflows/agent-docs-audit.lock.yml
|
|
1179
|
+
concurrency:
|
|
1180
|
+
group: docs-audit
|
|
1181
|
+
cancel-in-progress: false
|
|
1182
|
+
permissions: write-all
|
|
1183
|
+
with:
|
|
1184
|
+
trigger-kind: ${{ needs.classify.outputs.trigger-kind }}
|
|
1185
|
+
secrets:
|
|
1186
|
+
OPENAI_API_KEY: ${{ secrets.FORGE_API_KEY }}
|
|
1187
|
+
CODEX_API_KEY: ${{ secrets.FORGE_API_KEY }}
|
|
1188
|
+
COPILOT_GITHUB_TOKEN: ${{ secrets.FORGE_API_KEY }}
|
|
1189
|
+
BOT_APP_ID: ${{ secrets.BOT_APP_ID }}
|
|
1190
|
+
BOT_PRIVATE_KEY: ${{ secrets.BOT_PRIVATE_KEY }}
|
|
1191
|
+
AGENTMEMORY_SECRET: ${{ secrets.AGENTMEMORY_SECRET }}
|
|
1192
|
+
|
|
1151
1193
|
call-release:
|
|
1152
1194
|
needs: [classify, authorize]
|
|
1153
1195
|
if: needs.classify.outputs.route == 'release' && needs.authorize.outputs.trusted == 'true'
|
|
@@ -1298,6 +1340,7 @@ jobs:
|
|
|
1298
1340
|
# each cut from the same branch, and every landing left the others conflicting. The
|
|
1299
1341
|
# next landing releases the next child. A held child with no `Blocked by` of its
|
|
1300
1342
|
# own (held because a sibling was in flight) is free once it carries the hold marker.
|
|
1343
|
+
# shellcheck disable=SC2086
|
|
1301
1344
|
for child in $(printf '%s\n' $children | sort -n); do
|
|
1302
1345
|
[ "$child" = "$closed_issue" ] && continue
|
|
1303
1346
|
meta=$(gh issue view "$child" --repo "$REPO" --json state,labels,body 2>/dev/null || echo '{}')
|
|
@@ -1424,7 +1467,11 @@ jobs:
|
|
|
1424
1467
|
gh api "repos/$REPO/issues/$1/dependencies/blocked_by" --jq '.[] | select(.state == "open") | .number' 2>/dev/null || true
|
|
1425
1468
|
for b in $(gh issue view "$1" --repo "$REPO" --json body --jq '.body // ""' 2>/dev/null \
|
|
1426
1469
|
| grep -oiE 'blocked by #[0-9]+' | grep -oE '[0-9]+' | sort -u || true); do
|
|
1427
|
-
|
|
1470
|
+
# The feature's own children, listed in its tracker with their own "Blocked by", are not blockers.
|
|
1471
|
+
info=$(gh issue view "$b" --repo "$REPO" --json state,body 2>/dev/null) || continue
|
|
1472
|
+
printf '%s' "$info" | jq -e '(.state // "" | ascii_downcase) == "open"' >/dev/null || continue
|
|
1473
|
+
printf '%s' "$info" | jq -r '.body // ""' | grep -qF "<!-- split-parent: $1 -->" && continue
|
|
1474
|
+
echo "$b"
|
|
1428
1475
|
done
|
|
1429
1476
|
} | grep -vx "$parent" | sort -u || true
|
|
1430
1477
|
}
|
|
@@ -1476,6 +1523,7 @@ jobs:
|
|
|
1476
1523
|
fi
|
|
1477
1524
|
# One child per feature, the lowest-numbered free one; each landing on the feature branch
|
|
1478
1525
|
# releases the next (land-stack-child), so siblings never build side by side.
|
|
1526
|
+
# shellcheck disable=SC2086
|
|
1479
1527
|
for child in $(printf '%s\n' ${next_children[$feature]} | sort -n); do
|
|
1480
1528
|
own=$(gh issue view "$child" --repo "$REPO" --json body --jq '.body // ""' 2>/dev/null \
|
|
1481
1529
|
| grep -oiE 'blocked by #[0-9]+' | grep -oE '[0-9]+' | sort -u || true)
|
|
@@ -1645,6 +1693,75 @@ jobs:
|
|
|
1645
1693
|
gh api "repos/$REPO/actions/runs/$run_id/approve" --method POST
|
|
1646
1694
|
done
|
|
1647
1695
|
done
|
|
1696
|
+
# Refine's questions do not wait forever. Nine of the 21 batches that were really asked in
|
|
1697
|
+
# Odyssey, Numa, Bots and DeliveryMetrics up to November 2026 never got a reply, and those
|
|
1698
|
+
# issues stayed parked. Each question now carries the answer the bot would take, and once
|
|
1699
|
+
# the working hours pass, or the author or an assignee reacts with a thumbs-up, refine runs
|
|
1700
|
+
# again in assume mode and takes them. Batches written before the proposals existed, and any
|
|
1701
|
+
# batch holding a question that needs a decision, still wait for a person.
|
|
1702
|
+
- name: Take refine's proposed answers once nobody replied in time
|
|
1703
|
+
if: env.REFINE_ANSWER_TIMEOUT_HOURS != '0'
|
|
1704
|
+
env:
|
|
1705
|
+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
1706
|
+
REPO: ${{ github.repository }}
|
|
1707
|
+
REFINE_MARKER: "<!-- agent-refine -->"
|
|
1708
|
+
QUESTIONS_LINE: "I have some questions about this issue"
|
|
1709
|
+
PROPOSED_TAG: "**Proposed:**"
|
|
1710
|
+
DECISION_TAG: "**Your decision needed:**"
|
|
1711
|
+
run: |
|
|
1712
|
+
set -euo pipefail
|
|
1713
|
+
timeout_hours="${REFINE_ANSWER_TIMEOUT_HOURS:-24}"
|
|
1714
|
+
|
|
1715
|
+
# Hours since $1 that fall on a weekday, in UTC, counted up to the timeout and no further.
|
|
1716
|
+
working_hours_since() {
|
|
1717
|
+
local t now h=0
|
|
1718
|
+
t=$(date -u -d "$1" +%s)
|
|
1719
|
+
now=$(date -u +%s)
|
|
1720
|
+
while [ "$t" -lt "$now" ] && [ "$h" -lt "$timeout_hours" ]; do
|
|
1721
|
+
[ "$(date -u -d "@$t" +%u)" -le 5 ] && h=$((h + 1))
|
|
1722
|
+
t=$((t + 3600))
|
|
1723
|
+
done
|
|
1724
|
+
echo "$h"
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
gh issue list --repo "$REPO" --state open --label refine --label review --limit 100 \
|
|
1728
|
+
--json number,author,assignees,labels \
|
|
1729
|
+
--jq '.[] | select([.labels[].name] | (index("bot-working") or index("assumed")) | not)
|
|
1730
|
+
| [.number, ([.author.login] + [.assignees[].login] | join(" "))] | @tsv' |
|
|
1731
|
+
while IFS=$'\t' read -r issue people; do
|
|
1732
|
+
[ -n "$issue" ] || continue
|
|
1733
|
+
comments=$(gh api --paginate "repos/$REPO/issues/$issue/comments?per_page=100" | jq -s 'add // []')
|
|
1734
|
+
asked=$(jq -c --arg marker "$REFINE_MARKER" --arg line "$QUESTIONS_LINE" '
|
|
1735
|
+
[.[] | select(.user.type == "Bot" and (.body | contains($marker)) and (.body | contains($line)))]
|
|
1736
|
+
| last // empty | {id, created_at, body}' <<<"$comments")
|
|
1737
|
+
[ -n "$asked" ] || continue
|
|
1738
|
+
asked_at=$(jq -r '.created_at' <<<"$asked")
|
|
1739
|
+
body=$(jq -r '.body' <<<"$asked")
|
|
1740
|
+
|
|
1741
|
+
# A person spoke after the questions: that reply is refine's to read, not a silence.
|
|
1742
|
+
replied=$(jq --arg at "$asked_at" '[.[] | select(.user.type != "Bot" and .created_at > $at)] | length' <<<"$comments")
|
|
1743
|
+
[ "$replied" -eq 0 ] || continue
|
|
1744
|
+
case "$body" in *"$DECISION_TAG"*) echo "#$issue waits for a decision; not assuming."; continue ;; esac
|
|
1745
|
+
case "$body" in *"$PROPOSED_TAG"*) ;; *) continue ;; esac
|
|
1746
|
+
|
|
1747
|
+
accepted=false
|
|
1748
|
+
thumbs=$(gh api "repos/$REPO/issues/comments/$(jq -r '.id' <<<"$asked")/reactions" \
|
|
1749
|
+
--jq '[.[] | select(.content == "+1") | .user.login] | join(" ")' 2>/dev/null || true)
|
|
1750
|
+
for login in $thumbs; do
|
|
1751
|
+
case " $people " in *" $login "*) accepted=true ;; esac
|
|
1752
|
+
done
|
|
1753
|
+
hours=$(working_hours_since "$asked_at")
|
|
1754
|
+
if [ "$accepted" = true ]; then
|
|
1755
|
+
echo "#$issue: the proposed answers were accepted with a thumbs-up; refining with them."
|
|
1756
|
+
elif [ "$hours" -ge "$timeout_hours" ]; then
|
|
1757
|
+
echo "#$issue: no reply in ${timeout_hours} working hours; refining with the proposed answers."
|
|
1758
|
+
else
|
|
1759
|
+
continue
|
|
1760
|
+
fi
|
|
1761
|
+
gh api "repos/$REPO/actions/workflows/work-router.yml/dispatches" --method POST \
|
|
1762
|
+
-f ref="$(gh api "repos/$REPO" --jq .default_branch)" \
|
|
1763
|
+
-f "inputs[operation]=refine" -f "inputs[issue-number]=$issue" -f "inputs[mode]=assume"
|
|
1764
|
+
done
|
|
1648
1765
|
|
|
1649
1766
|
audit-close:
|
|
1650
1767
|
needs: classify
|