@plainconceptsplatform/workflows 1.3.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/catalog-installation.d.ts +2 -0
- package/dist/catalog-installation.js +44 -2
- package/dist/index.js +3 -0
- package/dist/stack-defaults.d.ts +1 -0
- package/dist/stack-defaults.js +13 -1
- package/dist/worker-env.js +2 -3
- 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 +5 -0
- 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 +64 -3
- 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 +95 -1
- 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 +5 -0
- package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +8 -2
- package/loops/actions/validate-refine-output/validate-refine-output.sh +30 -1
- 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 +17 -0
- package/loops/actions/verify-route-matrix/action.yml +9 -9
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +221 -7
- package/loops/docs/diagrams.md +90 -12
- package/loops/scripts/compile-agent-workflows.mjs +76 -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/templates/release/github-release.yml +30 -30
- package/loops/workflows/agent-implement.md +43 -8
- package/loops/workflows/agent-merge-gate.md +102 -6
- package/loops/workflows/agent-refine.md +152 -5
- package/loops/workflows/authorize-bot-work.yml +105 -105
- package/loops/workflows/shared/platform-defaults.md +23 -23
- package/loops/workflows/work-router.yml +345 -2
- package/package.json +1 -1
|
@@ -1183,19 +1183,21 @@ if [ -f "$HOUSEKEEPING_YML" ]; then
|
|
|
1183
1183
|
#
|
|
1184
1184
|
# Counted on the close shape, not on the words. A bare `state: 'closed'` is also how you ask
|
|
1185
1185
|
# the API for closed things, and the gate metrics list closed pull requests to find the merges:
|
|
1186
|
-
# counting the string alone reported that listing as a third close.
|
|
1186
|
+
# counting the string alone reported that listing as a third close. Every real close states a
|
|
1187
1187
|
# reason, so that is what is counted, and the assertion below keeps the two from drifting apart
|
|
1188
|
-
# by refusing any close that does not.
|
|
1188
|
+
# by refusing any close that does not. The counted set grew with stacks -- a landing child
|
|
1189
|
+
# whose issue close was lost, and a feature issue whose root merged -- and each is named in
|
|
1190
|
+
# the Stacked features section below, so the list here is four, not the original two.
|
|
1189
1191
|
closes=$(count -cE "state: 'closed', state_reason:" "$HOUSEKEEPING_YML")
|
|
1190
1192
|
if grep -nE "issues\.update\(.*state: 'closed'" "$HOUSEKEEPING_YML" | grep -qv "state_reason:"; then
|
|
1191
1193
|
HK_OK=0
|
|
1192
1194
|
echo "FAIL: housekeeping closes an issue without a state_reason; the close audit counts on it" >&2
|
|
1193
1195
|
fi
|
|
1194
|
-
if [ "$closes" -eq
|
|
1196
|
+
if [ "$closes" -eq 4 ]; then
|
|
1195
1197
|
PASS=$((PASS + 1))
|
|
1196
1198
|
else
|
|
1197
1199
|
HK_OK=0
|
|
1198
|
-
echo "FAIL: housekeeping closes issues in ${closes} place(s);
|
|
1200
|
+
echo "FAIL: housekeeping closes issues in ${closes} place(s); the split parent, the stack child, the stack feature and the digest are the four allowed" >&2
|
|
1199
1201
|
fi
|
|
1200
1202
|
|
|
1201
1203
|
# A retry is a workflow_dispatch, and GitHub starts no workflow run from an event raised
|
|
@@ -1611,6 +1613,13 @@ if [ -f "$GATE_VALIDATOR" ] && worker_installed merge-gate; then
|
|
|
1611
1613
|
human-review "$(gate_items "$(gate_comment assessed)")" success low false false ''
|
|
1612
1614
|
gate_case "remediating check-diff is accepted" remediated "$(gate_items "$(gate_comment remediated),${gate_push}")" success low false false fail
|
|
1613
1615
|
|
|
1616
|
+
# The verdict line is read by its shape. Pliny-Bot #639 wrote the bold around the whole line,
|
|
1617
|
+
# and the repair it pushed was discarded twice.
|
|
1618
|
+
gate_bold_line='{"type":"add_comment","item_number":7,"body":"## Remediation Report\n\n**Verdict: remediated**\n"}'
|
|
1619
|
+
gate_prose='{"type":"add_comment","item_number":7,"body":"The verdict is remediated, see the push."}'
|
|
1620
|
+
gate_case "a verdict line bolded whole is read" remediated "$(gate_items "${gate_bold_line},${gate_push}")" failure low
|
|
1621
|
+
gate_case "a verdict in prose is not a verdict" invalid "$(gate_items "${gate_prose},${gate_push}")" failure low
|
|
1622
|
+
|
|
1614
1623
|
# The agent's report decides the rest. This is the rule the whole redesign rests on: an
|
|
1615
1624
|
# unverified finding is a warning whatever severity it claims, so a model cannot fail the gate
|
|
1616
1625
|
# by asserting something it did not demonstrate, and cannot pass it by understating one it did.
|
|
@@ -2334,6 +2343,16 @@ if worker_installed refine; then
|
|
|
2334
2343
|
echo "FAIL: the refine refusal never releases bot-working; a refused issue stays reserved with no run behind it" >&2
|
|
2335
2344
|
fi
|
|
2336
2345
|
|
|
2346
|
+
# The split floor: a split child carries a whole stack's machinery (issue, feature branch,
|
|
2347
|
+
# PR into the branch), so a 1- or 2-point slice is a fragment and must be folded into a
|
|
2348
|
+
# sibling instead. The floor must be pinned through the env var the prompt interpolates,
|
|
2349
|
+
# so a consumer cannot lower it below 3 without editing the worker.
|
|
2350
|
+
if ! grep -qF 'MIN_CHILD_POINTS: "3"' "$REFINE_WORKER_MD" \
|
|
2351
|
+
|| ! grep -qF 'child estimated below ${{ env.MIN_CHILD_POINTS }}' "$REFINE_WORKER_MD"; then
|
|
2352
|
+
SIZE_GATE_OK=0
|
|
2353
|
+
echo "FAIL: refine allows split children below 3 points; the floor must stay interpolated from MIN_CHILD_POINTS" >&2
|
|
2354
|
+
fi
|
|
2355
|
+
|
|
2337
2356
|
if [ "$SIZE_GATE_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
2338
2357
|
fi
|
|
2339
2358
|
|
|
@@ -2620,8 +2639,10 @@ if [ -f "$APP_ERRORS_MJS" ]; then
|
|
|
2620
2639
|
# 3. The field allowlist is what actually holds the line, because a redaction rule filters
|
|
2621
2640
|
# values and cannot see a field somebody adds next year. Compared as a set, so reordering is
|
|
2622
2641
|
# fine and an addition is not. run_id is the one that matters: it is unhashed on this
|
|
2623
|
-
# telemetry and joins to a conversation and to a person.
|
|
2624
|
-
|
|
2642
|
+
# telemetry and joins to a conversation and to a person. category, fingerprint, message and
|
|
2643
|
+
# topFrames joined in v0.28.7 to enrich the filed issues (a84928a); they are set-side
|
|
2644
|
+
# fields, never engine telemetry, so the allowlist grows with them.
|
|
2645
|
+
expected_fields="category exceptionType fingerprint firstSeen frames lastSeen message occurrences operationName operations problemId roleName topFrames"
|
|
2625
2646
|
actual_fields="$(sed -n '/export const FINDING_FIELDS = \[/,/\];/p' "$APP_ERRORS_MJS" |
|
|
2626
2647
|
grep -oE '"[a-zA-Z]+"' | tr -d '"' | sort | tr '\n' ' ' | sed 's/ $//')"
|
|
2627
2648
|
if [ "$actual_fields" = "$(echo "$expected_fields" | tr ' ' '\n' | sort | tr '\n' ' ' | sed 's/ $//')" ]; then
|
|
@@ -2662,9 +2683,202 @@ if [ -f "$APP_ERRORS_MJS" ]; then
|
|
|
2662
2683
|
if [ "$AE_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
2663
2684
|
fi
|
|
2664
2685
|
|
|
2686
|
+
echo "── Stacked features ──────────────────────────────────────────────────────"
|
|
2687
|
+
|
|
2688
|
+
# Stacks are delivered by marker, not by label: every flat-mode rule still has to hold on a
|
|
2689
|
+
# repository that has never seen a marker. The assertions here are grouped as the two halves
|
|
2690
|
+
# of that contract. The first says the stack machinery exists and fails closed: an unreadable
|
|
2691
|
+
# or missing marker must route a child down the flat path, never dispatch at a branch nobody
|
|
2692
|
+
# resolved. The second says the flat path never learned about stacks: the default-path jobs
|
|
2693
|
+
# and prompts carry no stack condition that could fire on an issue without markers.
|
|
2694
|
+
STACK_OK=1
|
|
2695
|
+
st() {
|
|
2696
|
+
grep -qE "$1" "$2" || { STACK_OK=0; echo "FAIL: stacked features ${3}" >&2; }
|
|
2697
|
+
}
|
|
2698
|
+
# The router's stack job is a block of a derived file, not a file of its own, so its
|
|
2699
|
+
# assertions take the block on stdin: handing a multi-line string to grep as a filename
|
|
2700
|
+
# fails with "File name too long" and reads as a wiring failure that is not one.
|
|
2701
|
+
st_str() {
|
|
2702
|
+
printf '%s' "$2" | grep -qE "$1" || { STACK_OK=0; echo "FAIL: stacked features ${3}" >&2; }
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
# 1. The resolver. check-stack-branch reads the child's own marker first, then the
|
|
2706
|
+
# split-parent chain; both are `|| true` so a flat body (no match) cannot fail the job --
|
|
2707
|
+
# call-implement names this job in its needs, so a hard failure would block flat implements.
|
|
2708
|
+
CHECK_STACK_JOB=$(sed -n '/^ check-stack-branch:$/,/^ [a-z0-9_-]*:$/p' "$ROUTER_YML")
|
|
2709
|
+
if [ -n "$CHECK_STACK_JOB" ]; then
|
|
2710
|
+
st "stack-root-branch: \[\\^ \]\+" "$ROUTER_YML" 'has no marker regex for the feature branch'
|
|
2711
|
+
st_str 'exit 0' "$CHECK_STACK_JOB" 'resolver hard-fails instead of taking the flat path'
|
|
2712
|
+
st_str 'sed .s\|/\|%2F\|g.' "$CHECK_STACK_JOB" 'does not URL-escape the stack branch before the existence check'
|
|
2713
|
+
st_str 'sed -E .s/<!-- stack-root-branch: \(\[\^ \]\+\) -->/\\1/.' "$CHECK_STACK_JOB" 'does not strip the marker syntax off the branch'
|
|
2714
|
+
# The dupe guard: only dispatched stack implements run with head_branch = feat/N, so a
|
|
2715
|
+
# workflow_call (flat) run whose head_branch is the default branch can never match it.
|
|
2716
|
+
st_str 'head_branch == ..\$\{STACK_BRANCH\}..' "$CHECK_STACK_JOB" 'does not detect an in-flight implement on the feature branch'
|
|
2717
|
+
# Dispatch happens on the resolved ref, never on the event SHA. The `gh workflow run`
|
|
2718
|
+
# lives in dispatch-implement, which check-stack-branch gates: the two jobs verify the
|
|
2719
|
+
# branch exists, then hand off. The run needs a fresh App token -- GITHUB_TOKEN can
|
|
2720
|
+
# start no workflow -- and it reads repo-level secrets because a dispatch cannot forward
|
|
2721
|
+
# workflow-call ones, which is the activation prerequisite ownership.md documents.
|
|
2722
|
+
DISPATCH_JOB=$(sed -n '/^ dispatch-implement:$/,/^ [a-z0-9_-]*:$/p' "$ROUTER_YML")
|
|
2723
|
+
if [ -n "$DISPATCH_JOB" ]; then
|
|
2724
|
+
st_str 'gh workflow run agent-implement\.lock\.yml .* --ref "\$STACK_BRANCH"' "$DISPATCH_JOB" 'does not dispatch the stack implement at all'
|
|
2725
|
+
st_str 'create-github-app-token' "$DISPATCH_JOB" 'dispatches with GITHUB_TOKEN, which can start no run'
|
|
2726
|
+
# A child blocked by an open sibling is held, and the sibling's landing releases it. Without
|
|
2727
|
+
# both halves a dependent child cuts from a feature branch that lacks its dependency (Lyceum
|
|
2728
|
+
# #112, dispatched alongside #111), or is held forever.
|
|
2729
|
+
st_str "stack-blocked != 'true'" "$DISPATCH_JOB" 'dispatches a child whose blockers have not landed'
|
|
2730
|
+
st 'name: Hold a child whose blockers have not landed' "$ROUTER_YML" 'never holds a child behind the siblings it is blocked by'
|
|
2731
|
+
st 'state merged --base "\$STACK_BRANCH"' "$ROUTER_YML" 'holds a child behind a blocker whose pull request already landed, which nothing releases again'
|
|
2732
|
+
st 'Released #\$child: everything it was blocked by has landed' "$ROUTER_YML" 'holds blocked children but never releases them'
|
|
2733
|
+
fi
|
|
2734
|
+
# The child's pull request targets the ref the run was dispatched on, taken from the run.
|
|
2735
|
+
# Asking the agent for it failed in practice (Lyceum #72, run 38694942046): it emitted no
|
|
2736
|
+
# base, the handler fell back to the default branch, and a bundle cut from the feature
|
|
2737
|
+
# branch would not replay there. Both halves are checked, because a prompt that asks again
|
|
2738
|
+
# would invite an explicit base that disagrees with the ref.
|
|
2739
|
+
st 'base-branch: \$\{\{ github\.ref_name \}\}' "$IMPLEMENT_WORKER_MD" 'leaves the pull request base to the agent instead of the dispatched ref'
|
|
2740
|
+
if grep -qE 'base="feat/N"|pass `base: "feat/N"`' "$IMPLEMENT_WORKER_MD"; then
|
|
2741
|
+
STACK_OK=0
|
|
2742
|
+
echo "FAIL: stacked features prompt asks the agent for the base again; the run already sets it" >&2
|
|
2743
|
+
fi
|
|
2744
|
+
else
|
|
2745
|
+
# check-stack-branch serves only the implement route, so the CLI drops it from a router
|
|
2746
|
+
# that did not install implement. Nothing to assert then, and the section above must not
|
|
2747
|
+
# claim a pass for a job that is legitimately absent.
|
|
2748
|
+
:
|
|
2749
|
+
fi
|
|
2750
|
+
|
|
2751
|
+
# 2. Completion. The merge of a child PR into a feature branch (base != default branch) is
|
|
2752
|
+
# the only event that can mean "one fewer open child". The router block must be idempotent:
|
|
2753
|
+
# the announcement carries <!-- stack-complete --> and a re-run that finds it stays silent.
|
|
2754
|
+
st 'stack-complete' "$ROUTER_YML" 'has no idempotent completion marker on the root pull request'
|
|
2755
|
+
st 'closingIssuesReferences\[0\]\.number // empty' "$ROUTER_YML" 'does not read the child PR to find the closed issue'
|
|
2756
|
+
st 'split-parent: \[0-9\]+' "$ROUTER_YML" 'does not walk from the child to its feature issue'
|
|
2757
|
+
st 'stack-root-pr: \[0-9\]+' "$ROUTER_YML" 'does not read the root pull request number off the feature issue'
|
|
2758
|
+
# gh pr view and gh issue view answer OPEN, not open. An exact lowercase match on their state
|
|
2759
|
+
# never matches, which left every root closed and every child done as far as completion knew.
|
|
2760
|
+
# The child whose pull request just landed is done even if the gate has not closed its issue yet.
|
|
2761
|
+
st '# Landed, though the gate may not have closed its issue yet' "$ROUTER_YML" 'counts the child that just landed as still open, so the stack never reads complete'
|
|
2762
|
+
if grep -nE "(gh pr view|gh issue view)[^|]*--json state[^|]*\| *grep -q?x open" "$ROUTER_YML" >/dev/null; then
|
|
2763
|
+
STACK_OK=0
|
|
2764
|
+
echo "FAIL: stacked features compares a GraphQL state (OPEN) to lowercase open; the root and children never read as open" >&2
|
|
2765
|
+
fi
|
|
2766
|
+
|
|
2767
|
+
# 3. The gate. A child PR that lands on a feature branch closes exactly its own child issue
|
|
2768
|
+
# there (GitHub's closing keywords do not fire off the default branch); the root is held
|
|
2769
|
+
# for a person with the App token, so anybody reading the timeline can replace it. The
|
|
2770
|
+
# marker test on the issue body is what makes both stack-specific and invisible flat.
|
|
2771
|
+
if worker_installed merge-gate; then
|
|
2772
|
+
st 'stack-root-pr: \$PR -->' "$MERGE_GATE_WORKER_MD" 'does not test the feature issue for the root marker'
|
|
2773
|
+
st 'needs\.subject\.outputs\.stack_root == .true.' "$MERGE_GATE_WORKER_MD" 'has no root-hold branch before the merge step'
|
|
2774
|
+
# The stack step reads the subject's PR; with no subject it fails on an empty number.
|
|
2775
|
+
gate_stack_step=$(sed -n '/- name: Detect the pull request.s base and its role in a stack/,/run: |/p' "$MERGE_GATE_WORKER_MD")
|
|
2776
|
+
st_str "if: steps.subject.outputs.found == 'true'" "$gate_stack_step" 'runs the stack step without a subject, so a gate dispatched for a held root fails'
|
|
2777
|
+
st '\*\*Stack root held\.\*\*' "$MERGE_GATE_WORKER_MD" 'posts no human-readable hold explanation on the root'
|
|
2778
|
+
st 'gh issue close "\$ISSUE" --repo "\$REPO" --reason completed \|\| true' "$MERGE_GATE_WORKER_MD" 'does not close the child issue when its PR lands off the default branch'
|
|
2779
|
+
st 'needs\.subject\.outputs\.pr_base != .. \&\& needs\.subject\.outputs\.pr_base != github\.event\.repository\.default_branch' "$MERGE_GATE_WORKER_MD" 'gates the child-issue close on the base not being the default branch'
|
|
2780
|
+
fi
|
|
2781
|
+
|
|
2782
|
+
# 4. The split. Refine creates the feature branch and the held root PR together with the
|
|
2783
|
+
# children, then stamps markers on parent and children before any hand-over: the markers are
|
|
2784
|
+
# what the resolver, the gate and the janitor read, so no new label exists to move. The root
|
|
2785
|
+
# PR body must close the feature issue -- that is what ends the feature when the human merges.
|
|
2786
|
+
if worker_installed refine; then
|
|
2787
|
+
REFINE_WORKER_MD="${WORKFLOWS_DIR}/agent-refine.md"
|
|
2788
|
+
st 'allowed-branches: \["feat/\*"\]' "$REFINE_WORKER_MD" 'does not constrain the feature branch name'
|
|
2789
|
+
st 'allowed-base-branches: \["main"\]' "$REFINE_WORKER_MD" 'lets the root target a branch other than the default'
|
|
2790
|
+
st 'preserve-branch-name: true' "$REFINE_WORKER_MD" 'does not preserve the feature branch name on root PR creation'
|
|
2791
|
+
# Refine stages its outputs and applies them itself, and gh-aw opens no pull request while a
|
|
2792
|
+
# worker is staged, not even one marked `staged: false`. The root has to be opened by the
|
|
2793
|
+
# workflow, and the stamp and the hand-over have to read that number, or every child goes flat.
|
|
2794
|
+
st '^ staged: true$' "$REFINE_WORKER_MD" 'no longer stages the body and children for validation before they are applied'
|
|
2795
|
+
st 'name: Open the stack root pull request' "$REFINE_WORKER_MD" 'never opens the root pull request; the staged handler only previews it'
|
|
2796
|
+
st 'PR_NUMBER: \$\{\{ steps\.root\.outputs\.pr_number' "$REFINE_WORKER_MD" 'stamps the stack markers without the root the workflow opened'
|
|
2797
|
+
st 'ROOT_PR: \$\{\{ steps\.root\.outputs\.pr_number' "$REFINE_WORKER_MD" 'hands the children over without the root the workflow opened'
|
|
2798
|
+
st 'stack-root-branch: %s' "$REFINE_WORKER_MD" 'does not stamp the feature branch on the parent issue'
|
|
2799
|
+
st 'Fixes #\$\{\{ inputs\.issue-number \}\}' "$REFINE_WORKER_MD" 'does not make the root PR close the feature issue'
|
|
2800
|
+
# The hand-over stamp: a child handed to implement carries the feature branch marker, so
|
|
2801
|
+
# the resolver reads it off the child body without chasing the chain first.
|
|
2802
|
+
st 'stack-root-branch: %s' "$REFINE_WORKER_MD" 'does not stamp the feature branch on each child at hand-over'
|
|
2803
|
+
# The children have to be found. gh-aw's sanitizer strips HTML comments from agent_output.json,
|
|
2804
|
+
# so the split-parent marker the agent writes never arrives, and staged, nothing links the
|
|
2805
|
+
# children as sub-issues either (Lyceum #45, run 38747143251: three children, none handed over).
|
|
2806
|
+
# The workflow tells the apply which issue the children belong to, and the apply stamps the
|
|
2807
|
+
# marker and links the sub-issue itself.
|
|
2808
|
+
CREATE_ISSUES_ACTION="${HERE}/../create-agent-issues/action.yml"
|
|
2809
|
+
# Dependencies travel as `Blocked by #aw_<id>` lines the apply resolves, never as the
|
|
2810
|
+
# blocked_by field: a value naming two siblings failed the staged preview and threw away a
|
|
2811
|
+
# finished refinement (Lyceum #45, run 38770811334).
|
|
2812
|
+
st 'never set the .blocked_by. field' "$REFINE_WORKER_MD" 'lets the agent express dependencies through blocked_by, which one bad value turns into a lost run'
|
|
2813
|
+
st 'Blocked by #aw_<id>' "$REFINE_WORKER_MD" 'asks for sibling numbers the agent cannot know yet'
|
|
2814
|
+
st 'parent-issue: \$\{\{ needs\.validate_output\.outputs\.outcome == .split.' "$REFINE_WORKER_MD" 'does not tell the apply which issue the split children belong to'
|
|
2815
|
+
st 'issues/\{issue_number\}/sub_issues' "$CREATE_ISSUES_ACTION" 'does not link split children as sub-issues of their parent'
|
|
2816
|
+
st 'split-parent: \$\{parent\}' "$CREATE_ISSUES_ACTION" 'does not stamp the split-parent marker that the sanitizer strips from the agent'
|
|
2817
|
+
# The guard against re-handing a child already handed over: it carries the refined label,
|
|
2818
|
+
# and handing it again would start a second implement on work in flight.
|
|
2819
|
+
st 'grep -qx "\$REFINED_LABEL"' "${WORKFLOWS_DIR}/agent-refine.md" 'does not skip an already-handed-over child'
|
|
2820
|
+
fi
|
|
2821
|
+
|
|
2822
|
+
# 5. Housekeeping. The janitor's stack section is the backstop for every event the router can
|
|
2823
|
+
# miss, and it ran unattended for months before stacks existed. Three of its guardrails are
|
|
2824
|
+
# asserted here the way section 3's are. The marker test fails first on a repository without
|
|
2825
|
+
# stacks, so the flat cost is one regex per open issue.
|
|
2826
|
+
if [ -f "$HOUSEKEEPING_YML" ]; then
|
|
2827
|
+
st 'stack-root-pr: \(\\d\+\)' "$HOUSEKEEPING_YML" 'does not find the feature issues to sweep'
|
|
2828
|
+
st 'implement-pr: \(\\d\+\)' "$HOUSEKEEPING_YML" 'does not read each child pull request number'
|
|
2829
|
+
st "merged_at" "$HOUSEKEEPING_YML" 'does not test whether the child pull request actually landed'
|
|
2830
|
+
# The orphan close: a child whose named PR merged but whose issue never closed is closed
|
|
2831
|
+
# here. Both its closes state a reason, like every other close in the file.
|
|
2832
|
+
if ! grep -qE "github\.rest\.issues\.update\(\{ \.\.\.context\.repo, issue_number: child\.number, state: 'closed', state_reason: 'completed' \}\)" "$HOUSEKEEPING_YML"; then
|
|
2833
|
+
STACK_OK=0
|
|
2834
|
+
echo "FAIL: housekeeping no longer closes an orphaned child whose pull request landed" >&2
|
|
2835
|
+
fi
|
|
2836
|
+
# The flat guarantees. Section 3 must still refuse to close a stack parent (the root PR
|
|
2837
|
+
# closing the feature issue is what ends it, not the sweep), and section 4 must still not
|
|
2838
|
+
# delete a branch whose pull request is open -- an open root PR protects feat/N. The skip
|
|
2839
|
+
# is asserted on its real shape: the marker test inside the issues.some() predicate.
|
|
2840
|
+
st 'stack-root-branch: \\S\+ -->/\.test\(i\.body' "$HOUSEKEEPING_YML" 'no longer skips stack parents in the split-parent close'
|
|
2841
|
+
st "p\.state === 'open'\)\) continue" "$HOUSEKEEPING_YML" 'can delete a feature branch whose root pull request is still open'
|
|
2842
|
+
fi
|
|
2843
|
+
|
|
2844
|
+
# 6. The compat guarantee, stated as absence. A repository without markers sees none of
|
|
2845
|
+
# this: the default path carries no stack condition that could fire. The implement worker's
|
|
2846
|
+
# prompt lines that name the markers are conditional on them being present on the issue.
|
|
2847
|
+
if worker_installed implement; then
|
|
2848
|
+
# The instruction naming the marker exists (the marker-gated base instruction), and the
|
|
2849
|
+
# flat instruction ("propose against main") is still in place alongside it.
|
|
2850
|
+
st 'stack-root-branch: feat/N -->' "$IMPLEMENT_WORKER_MD" 'does not tell stack children to base their PR on the feature branch'
|
|
2851
|
+
st 'allowed-base-branches:' "$IMPLEMENT_WORKER_MD" 'does not declare the base-branch allowlist for create-pull-request'
|
|
2852
|
+
# The base allowlist denies everything the marker test did not name. main stays first and
|
|
2853
|
+
# unconditional; the feature pattern is additive, never a replacement.
|
|
2854
|
+
if [ "$(count -c '^ allowed-base-branches:' "$IMPLEMENT_WORKER_MD")" -ne 1 ]; then
|
|
2855
|
+
STACK_OK=0
|
|
2856
|
+
echo "FAIL: agent-implement.md declares allowed-base-branches more than once or not at all" >&2
|
|
2857
|
+
fi
|
|
2858
|
+
if ! sed -n '/allowed-base-branches:/,/^ [a-z]/p' "$IMPLEMENT_WORKER_MD" | grep -q '"main"'; then
|
|
2859
|
+
STACK_OK=0
|
|
2860
|
+
echo "FAIL: agent-implement.md allowed-base-branches dropped the default branch" >&2
|
|
2861
|
+
fi
|
|
2862
|
+
# The dispatch trigger must stay optional and marker-gated: a flat implement is a
|
|
2863
|
+
# workflow_call, never a dispatch, so the checkout ternary keys off workflow_dispatch.
|
|
2864
|
+
st 'workflow_dispatch' "$IMPLEMENT_WORKER_MD" 'no longer accepts workflow_dispatch'
|
|
2865
|
+
fi
|
|
2866
|
+
|
|
2867
|
+
# 7. No stack label anywhere. Stack semantics live in markers on issues and pull requests;
|
|
2868
|
+
# a label would have to be moved by the App token to raise events, and every new label is a
|
|
2869
|
+
# new consumer-visible surface. Assert the absence of any stack-named label action.
|
|
2870
|
+
if grep -nE "add-label .*$|add-labels$" "$ROUTER_YML" "$MERGE_GATE_WORKER_MD" "${WORKFLOWS_DIR}/agent-refine.md" "$IMPLEMENT_WORKER_MD" 2>/dev/null |
|
|
2871
|
+
grep -qiE 'stack'; then
|
|
2872
|
+
STACK_OK=0
|
|
2873
|
+
echo "FAIL: a workflow adds a stack-named label; stack semantics belong to markers" >&2
|
|
2874
|
+
fi
|
|
2875
|
+
|
|
2876
|
+
if [ "$STACK_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
2877
|
+
|
|
2665
2878
|
echo
|
|
2666
2879
|
if [ "$FAIL" -eq 0 ]; then
|
|
2667
|
-
|
|
2880
|
+
TcMsg="Route matrix: ${PASS} passed"
|
|
2881
|
+
echo "$TcMsg"
|
|
2668
2882
|
else
|
|
2669
2883
|
echo "Route matrix: ${PASS} passed, ${FAIL} FAILED" >&2
|
|
2670
2884
|
fi
|
package/loops/docs/diagrams.md
CHANGED
|
@@ -57,7 +57,7 @@ flowchart TD
|
|
|
57
57
|
wTriage("agent-triage")
|
|
58
58
|
classify -.->|"cron or dispatch"| plumbing
|
|
59
59
|
plumbing["deterministic jobs<br/>no model runs in any of these"] --> pJobs
|
|
60
|
-
pJobs("bot-approve · audit-close<br/>cleanup-artifacts · validate<br/>reconcile-bot-pr-runs · detect-pr-conflicts")
|
|
60
|
+
pJobs("bot-approve · audit-close<br/>cleanup-artifacts · validate<br/>reconcile-bot-pr-runs · detect-pr-conflicts<br/>check-stack-branch · dispatch-implement")
|
|
61
61
|
idle(("Idle<br/>run ends, ~10s, every job skipped"))
|
|
62
62
|
|
|
63
63
|
classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
|
|
@@ -83,7 +83,7 @@ flowchart LR
|
|
|
83
83
|
triage -.->|block| closed(("Closed"))
|
|
84
84
|
triage -.->|needs-info| author("Author replies") --> triage
|
|
85
85
|
write("Write+ user<br/>self-labels") --> refine
|
|
86
|
-
refine{"refine<br/>estimate in points"} -->|"
|
|
86
|
+
refine{"refine<br/>estimate in points"} -->|"3 to 5"| implement
|
|
87
87
|
refine -->|"8 or more"| split("Split into children") --> refine
|
|
88
88
|
refine -.->|questions| author
|
|
89
89
|
implement("implement<br/>branch, PR, closes the issue") --> ci("CI")
|
|
@@ -110,6 +110,42 @@ Audit creates work rather than consuming it, and files into `refine` rather than
|
|
|
110
110
|
`implement`, so a report of several unrelated findings becomes one properly sized issue per finding
|
|
111
111
|
instead of one pull request that has to fix them all.
|
|
112
112
|
|
|
113
|
+
### How a feature stacks
|
|
114
|
+
|
|
115
|
+
When refine splits a story that carries real sequencing — a feature whose children build on each
|
|
116
|
+
other — the split can open a stack instead of independent work. The children stay ordinary issues
|
|
117
|
+
walking the normal pipeline above; only where they land changes: each child's pull request targets
|
|
118
|
+
the feature branch rather than the default branch, and one root pull request (spec plus the
|
|
119
|
+
feature's acceptance journey, open from the start) carries the whole feature to `main` at the end.
|
|
120
|
+
|
|
121
|
+
```mermaid
|
|
122
|
+
flowchart TD
|
|
123
|
+
feat("Feature issue<br/>8+ points, ordered children") --> splitStack("refine split<br/>stamps stack-root-branch + stack-root-pr<br/>on the feature issue") --> rootPr("root pull request on feat/<N><br/>spec + red journey e2e,<br/>base: main, HELD")
|
|
124
|
+
splitStack --> child1("child T1: implement<br/>PR base: feat/<N>")
|
|
125
|
+
child1 --> merge1("merge-gate<br/>auto-merge into feat/<N><br/>closes T1") --> child2("child T2, T3 ...<br/>same shape, one at a time")
|
|
126
|
+
child2 --> stacked{"every child landed<br/>and the root is green?"}
|
|
127
|
+
stacked -.->|"no: a bug found"| fixChild("new child issue<br/>mid-stack insertion") --> child2
|
|
128
|
+
stacked -->|yes| announce("stack-complete announcement<br/>on the root pull request")
|
|
129
|
+
announce --> humanStack(("human merges the root<br/>feature issue closes with it"))
|
|
130
|
+
flat("story with no<br/>sequencing") -.->|"no stack markers:<br/>byte-for-byte the old path"| implement
|
|
131
|
+
|
|
132
|
+
classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
|
|
133
|
+
classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
|
|
134
|
+
classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
|
|
135
|
+
classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
|
|
136
|
+
classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
|
|
137
|
+
class feat,flat start
|
|
138
|
+
class splitStack,rootPr,child1,merge1,child2,announce,fixChild action
|
|
139
|
+
class stacked decision
|
|
140
|
+
class humanStack success
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The stack exists only in markers on the issues — `stack-root-branch` and `stack-root-pr` on the
|
|
144
|
+
feature issue, `stack-root-branch` stamped on each child at hand-over — so a repository with none
|
|
145
|
+
of them runs exactly the pipeline above and nothing else. The root is never auto-merged: a person
|
|
146
|
+
merges it after every child has landed and the feature branch is green, which is the point where
|
|
147
|
+
the feature's own acceptance journey, not one child's scope, is the thing being verified.
|
|
148
|
+
|
|
113
149
|
---
|
|
114
150
|
|
|
115
151
|
## agent-triage.md
|
|
@@ -165,7 +201,8 @@ flowchart TD
|
|
|
165
201
|
## agent-refine.md
|
|
166
202
|
|
|
167
203
|
Decides the size of the work, which is the decision that determines whether it ever lands. An
|
|
168
|
-
estimate of 8 or more is split into children of 5
|
|
204
|
+
estimate of 8 or more is split into children of 3 to 5 points, and each child walks the pipeline
|
|
205
|
+
alone. A slice smaller than 3 points is a fragment and is folded into a sibling.
|
|
169
206
|
The refined story is wrapped in the repository's own issue form, and when questions remain the
|
|
170
207
|
worker leaves a temporal draft on the issue and asks everything in one batched comment.
|
|
171
208
|
|
|
@@ -188,7 +225,9 @@ flowchart TD
|
|
|
188
225
|
refSplit -->|no| refOutcome
|
|
189
226
|
refSplit -->|"yes, and it splits"| refChildren
|
|
190
227
|
refSplit -->|"yes, indivisible"| refOutcome
|
|
191
|
-
refChildren(("Split<br/>2-6 children, each
|
|
228
|
+
refChildren(("Split<br/>2-6 children, each 3 to 5<br/>parent becomes their tracker"))
|
|
229
|
+
refChildren -->|"children build on each other"| refStack
|
|
230
|
+
refStack("Stamp stack-root-branch + stack-root-pr<br/>on the feature issue, and open the<br/>held root pull request on feat/<N>") --> refOutcome
|
|
192
231
|
refOutcome{"Questions left<br/>for the author?"}
|
|
193
232
|
refOutcome -->|no| refDone
|
|
194
233
|
refOutcome -->|yes| refAsk
|
|
@@ -203,7 +242,7 @@ flowchart TD
|
|
|
203
242
|
classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
|
|
204
243
|
classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
|
|
205
244
|
class refStart start
|
|
206
|
-
class refReserve,refFacts,refExplore,refTrivial,refStory,refWrap,refProse action
|
|
245
|
+
class refReserve,refFacts,refExplore,refTrivial,refStory,refWrap,refProse,refStack action
|
|
207
246
|
class refClassify,refEstimate,refSplit,refOutcome decision
|
|
208
247
|
class refFail failure
|
|
209
248
|
class refDone,refAsk,refChildren success
|
|
@@ -242,6 +281,9 @@ flowchart TD
|
|
|
242
281
|
implIdle(("Idle<br/>closed, or held by future"))
|
|
243
282
|
implFail(("Parked<br/>review added, a rerun<br/>would fail the same way"))
|
|
244
283
|
|
|
284
|
+
implStackStart("check-stack-branch<br/>(router job, rung 1)<br/>the issue carries<br/>stack-root-branch?") -.->|"no: flat mode,<br/>nothing happens"| implIdle
|
|
285
|
+
implStackStart -->|"yes: the workflow dispatches<br/>this worker with ref: feat/N<br/>so the agent never fetches"| implStart
|
|
286
|
+
|
|
245
287
|
classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
|
|
246
288
|
classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
|
|
247
289
|
classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
|
|
@@ -249,7 +291,7 @@ flowchart TD
|
|
|
249
291
|
classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
|
|
250
292
|
classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
|
|
251
293
|
class implStart start
|
|
252
|
-
class implReserve,implFacts,implTodos,implCode,implPr action
|
|
294
|
+
class implReserve,implFacts,implTodos,implCode,implPr,implStackStart action
|
|
253
295
|
class implEligible,implCheck,implVerify,implRetry decision
|
|
254
296
|
class implIdle idle
|
|
255
297
|
class implFail failure
|
|
@@ -261,6 +303,13 @@ and that used to burn the issue and hand it to a human. A run that worked for ha
|
|
|
261
303
|
failed produced an answer that was wrong; repeating it costs the whole fleet the same half hour to
|
|
262
304
|
be wrong again, so only the short deaths are retried.
|
|
263
305
|
|
|
306
|
+
A stacked child enters the same worker through a different door: the router's `check-stack-branch`
|
|
307
|
+
job reads the issue's markers before anything runs, and dispatches the compiled workflow with
|
|
308
|
+
`ref:` set to the feature branch, because the agent sandbox has no credentials to fetch or choose
|
|
309
|
+
a branch itself. The prompt then tells `create_pull_request` its base: the feature branch for a
|
|
310
|
+
stacked child, the default branch for everything else. Nothing about the standard path above
|
|
311
|
+
changes for an issue without the markers.
|
|
312
|
+
|
|
264
313
|
---
|
|
265
314
|
|
|
266
315
|
## agent-merge-gate.md
|
|
@@ -287,11 +336,14 @@ flowchart TD
|
|
|
287
336
|
gateProtected -->|"measured low, medium or high"| gateAssess
|
|
288
337
|
gateAssess("Agent (rung 5)<br/>find defects, verify them,<br/>rate recoverability") --> gateDisposition
|
|
289
338
|
gateDisposition{"Disposition<br/>computed in shell from<br/>the facts and the report"}
|
|
290
|
-
gateDisposition -->|"no verified blocker,<br/>radius low or recoverable medium"|
|
|
339
|
+
gateDisposition -->|"no verified blocker,<br/>radius low or recoverable medium"| gateRootCheck
|
|
340
|
+
gateRootCheck{"PR base is a<br/>stack feature branch<br/>and this PR is the root?"}
|
|
341
|
+
gateRootCheck -->|"child PR: no"| gateMerge
|
|
291
342
|
gateDisposition -.->|"radius high, protected<br/>or owner path"| gateOwner
|
|
292
343
|
gateDisposition -.->|"fragile, unmet criteria<br/>or low confidence"| gateHuman
|
|
293
344
|
gateDisposition -.->|"CI red or a verified<br/>high finding"| gateBlocked
|
|
294
|
-
gateMerge(("AUTO-MERGE<br/>squash, issue
|
|
345
|
+
gateMerge(("AUTO-MERGE<br/>squash into the PR's base<br/>(main, or the feature branch:<br/>then the child issue closes,<br/>pr-pending removed)"))
|
|
346
|
+
gateRootCheck -->|"yes: the root"| gateHeld(("HELD<br/>stack root never auto-merges,<br/>a person merges it once every<br/>child has landed and it is green"))
|
|
295
347
|
gateHuman(("HUMAN REVIEW<br/>review label,<br/>implement label kept"))
|
|
296
348
|
gateOwner(("OWNER REVIEW<br/>review + owner-review,<br/>CODEOWNERS asked when named"))
|
|
297
349
|
gateHuman --> gatePreview
|
|
@@ -310,12 +362,20 @@ flowchart TD
|
|
|
310
362
|
classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
|
|
311
363
|
class gateStart start
|
|
312
364
|
class gateBranch,gateFacts,gateFix,gateAssess,gatePreview action
|
|
313
|
-
class gateSubject,gateCi,gateProtected,gateDisposition decision
|
|
365
|
+
class gateSubject,gateCi,gateProtected,gateDisposition,gateRootCheck decision
|
|
314
366
|
class gateIdle idle
|
|
315
367
|
class gateHuman,gateOwner,gateBlocked failure
|
|
316
368
|
class gateMerge,gatePush,gateApproved success
|
|
317
369
|
```
|
|
318
370
|
|
|
371
|
+
For a child pull request — one whose base is a stack feature branch — everything above is
|
|
372
|
+
unchanged: the risk assessment sees the same diff it always would, and auto-merge means squash
|
|
373
|
+
into the feature branch, closing exactly that child issue. The root pull request is the one
|
|
374
|
+
exception in the whole system: it can be as clean as any merge that ever shipped and the gate
|
|
375
|
+
still will not take it, because a feature is only done when the whole branch is done. AABB holds
|
|
376
|
+
it with an App-token comment and a person takes it from there, after the router's stack-completion
|
|
377
|
+
check has announced that every child landed and the branch is green.
|
|
378
|
+
|
|
319
379
|
A protected-path match holds the merge but does not stop the repair path: the agent may still fix
|
|
320
380
|
failed CI on those files, and `conclude` is what refuses to merge them.
|
|
321
381
|
|
|
@@ -488,7 +548,16 @@ flowchart TB
|
|
|
488
548
|
hkRetry --> hkStrand
|
|
489
549
|
|
|
490
550
|
hkStrand("Strands: drop pr-pending<br/>where no open PR closes the issue") --> hkParent("Close a split parent<br/>once every child is closed")
|
|
491
|
-
hkParent -->
|
|
551
|
+
hkParent --> hkStack{"stack-root-pr<br/>marker on the<br/>feature issue?"}
|
|
552
|
+
hkStack -.->|"no: flat mode"| hkBranch
|
|
553
|
+
hkStack -->|"root open"| hkStackWait{"children left,<br/>or the completion<br/>announcement missing?"}
|
|
554
|
+
hkStackWait -->|"children open: mid-stack,<br/>the normal state"| hkBranch
|
|
555
|
+
hkStackWait -->|"all landed, never told"| hkDigest
|
|
556
|
+
hkStackWait -->|"told, but nobody merged for days"| hkDigest
|
|
557
|
+
hkStack -->|"root merged,<br/>feature issue still open"| hkStackClose("close the feature issue,<br/>close child issues whose<br/>PR merged but stayed open")
|
|
558
|
+
hkStack -->|"root closed unmerged"| hkDigest
|
|
559
|
+
hkStackClose --> hkBranch
|
|
560
|
+
hkBranch{"Branch whose PRs<br/>are all finished<br/>and all bot-authored?"}
|
|
492
561
|
hkBranch -->|yes| hkDelete("Delete the branch")
|
|
493
562
|
hkBranch -.->|"open PR, human PR,<br/>or the default branch"| hkDigest
|
|
494
563
|
hkDelete --> hkDigest
|
|
@@ -500,8 +569,8 @@ flowchart TB
|
|
|
500
569
|
classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
|
|
501
570
|
classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
|
|
502
571
|
class hkCron start
|
|
503
|
-
class hkScan,hkRetry,hkStrand,hkParent,hkDelete,hkDigest action
|
|
504
|
-
class hkPark,hkWhy,hkBudget,hkBranch decision
|
|
572
|
+
class hkScan,hkRetry,hkStrand,hkParent,hkDelete,hkDigest,hkStackClose action
|
|
573
|
+
class hkPark,hkWhy,hkBudget,hkBranch,hkStack,hkStackWait decision
|
|
505
574
|
class hkEnd success
|
|
506
575
|
```
|
|
507
576
|
|
|
@@ -512,6 +581,15 @@ would be a green no-op. Every write goes through one `act()` wrapper, which is t
|
|
|
512
581
|
kinds of issue a split parent whose children are all done, and its own digest which the route
|
|
513
582
|
matrix asserts by counting.
|
|
514
583
|
|
|
584
|
+
The stack section extends the same two rules to stacked features without adding a third closed
|
|
585
|
+
kind: a merged child whose issue stayed open is closed exactly like a flat split parent (its work
|
|
586
|
+
landed, the close was lost to a race), and a merged root closes the feature issue the same way
|
|
587
|
+
section 3 closes a split parent. Everything else about a stack is a *report*, never an action:
|
|
588
|
+
mid-stack is the normal state and stays silent; a root that finished but was never announced, one
|
|
589
|
+
held for days after its announcement, a root closed by accident with children open, and a missing
|
|
590
|
+
root pull request each get one line in the digest. No labels move, so nothing the router listens
|
|
591
|
+
for is raised by the sweep itself.
|
|
592
|
+
|
|
515
593
|
---
|
|
516
594
|
|
|
517
595
|
## agentics-app-errors
|
|
@@ -342,6 +342,13 @@ for (const file of readdirSync(workflowDirectory)) {
|
|
|
342
342
|
// pull request as an issue instead of a pull request). Checking out the branch tip at
|
|
343
343
|
// execution time makes the commit parent current main; the implement-global queue
|
|
344
344
|
// already keeps sibling merges out of the window while the agent runs.
|
|
345
|
+
//
|
|
346
|
+
// A stack child dispatches this workflow directly (`gh workflow run
|
|
347
|
+
// agent-implement.lock.yml --ref feat/N`), and there the pin must select the feature
|
|
348
|
+
// branch tip, not main: github.ref_name is the ref the dispatch named. Under
|
|
349
|
+
// workflow_call the callee inherits the caller's event, and the router is only ever
|
|
350
|
+
// dispatched on the default branch, so the dispatch arm of the ternary resolves to
|
|
351
|
+
// main there too -- flat behavior is byte-identical.
|
|
345
352
|
if (file === "agent-implement.lock.yml") {
|
|
346
353
|
const start = rewritten.indexOf("\n agent:\n");
|
|
347
354
|
const rest = start === -1 ? "" : rewritten.slice(start + 1);
|
|
@@ -351,12 +358,56 @@ for (const file of readdirSync(workflowDirectory)) {
|
|
|
351
358
|
const span = rewritten.slice(start, end).replace(
|
|
352
359
|
/(uses: actions\/checkout@[^\n]*\n(\s+)with:\n(?:\2 [^\n]+\n)*)/,
|
|
353
360
|
(whole, _block, indent) =>
|
|
354
|
-
whole.includes("ref:")
|
|
361
|
+
whole.includes("ref:")
|
|
362
|
+
? whole
|
|
363
|
+
: whole +
|
|
364
|
+
indent +
|
|
365
|
+
" ref: ${{ github.event_name == 'workflow_dispatch' && github.ref_name || github.event.repository.default_branch }}\n",
|
|
355
366
|
);
|
|
356
367
|
rewritten = rewritten.slice(0, start) + span + rewritten.slice(end);
|
|
357
368
|
}
|
|
358
369
|
}
|
|
359
370
|
|
|
371
|
+
// update_issue declares no required field, so the safe-outputs MCP server accepts a call
|
|
372
|
+
// carrying only issue_number and answers success. An agent that drops the body - the
|
|
373
|
+
// documented way is omitting the trailing `.` so the bridge never reads stdin - loses its
|
|
374
|
+
// whole story, is told nothing, and goes on to claim completion. add_comment declares body
|
|
375
|
+
// required and rejects the identical mistake, which is how the agent recovers from it on
|
|
376
|
+
// the next turn. Measured on Lyceum before this: one refine run in five wrote its story,
|
|
377
|
+
// the other four ended green with the issue untouched (#44, #59 twice, #62), and Numa #698
|
|
378
|
+
// reached `refined, implement` still carrying its original form body.
|
|
379
|
+
//
|
|
380
|
+
// Only agent-refine calls update_issue, and always to replace the body; labels and status
|
|
381
|
+
// are workflow-owned and the prompt forbids the agent from touching them, so requiring the
|
|
382
|
+
// body costs no legitimate call. The check below fails the compile if this stops matching.
|
|
383
|
+
{
|
|
384
|
+
const toolStart = rewritten.indexOf('"update_issue": {');
|
|
385
|
+
if (toolStart !== -1) {
|
|
386
|
+
const bodyField = rewritten.indexOf('"body": {', toolStart);
|
|
387
|
+
if (bodyField !== -1) {
|
|
388
|
+
const insertAt = bodyField + '"body": {'.length;
|
|
389
|
+
if (!rewritten.slice(insertAt, insertAt + 80).includes('"required": true')) {
|
|
390
|
+
rewritten = rewritten.slice(0, insertAt) + '\n' + ' '.repeat(20) + '"required": true,' + rewritten.slice(insertAt);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// The pull request base belongs to the handler, never to the MCP server. agent-implement sets
|
|
397
|
+
// base-branch from the dispatched ref so a stack child lands on its feature branch, and gh-aw
|
|
398
|
+
// writes that base into both configs. The safe-outputs MCP server runs inside the sandbox,
|
|
399
|
+
// where remote refs are not visible, and given a base it insists on a merge-base against
|
|
400
|
+
// origin/<base>: on Lyceum #73 it answered "No remote refs available for merge-base
|
|
401
|
+
// calculation" twice and the run died at max-turns with the code already written (run
|
|
402
|
+
// 38737284343). With no base it patches from the pinned SHA, which the stack dispatch sets to
|
|
403
|
+
// the feature tip, and that is exactly the patch the handler then applies onto the base. So
|
|
404
|
+
// the MCP config loses the base and the handler config keeps it. The check below fails the
|
|
405
|
+
// compile if this stops matching.
|
|
406
|
+
rewritten = rewritten.replace(
|
|
407
|
+
/^([ \t]+GH_AW_SAFE_OUTPUTS_CONFIG: "[^\n]*?)\\"base_branch\\":\\"[^\\"]*\\",/m,
|
|
408
|
+
"$1",
|
|
409
|
+
);
|
|
410
|
+
|
|
360
411
|
// Guard the two rewrites the compiled agent jobs cannot work without. The anchors are
|
|
361
412
|
// upstream template text: when a gh-aw upgrade reshapes either step, the silent
|
|
362
413
|
// failure mode returns (an artifact with no bundle, a skipped push, a green gate on
|
|
@@ -365,8 +416,31 @@ for (const file of readdirSync(workflowDirectory)) {
|
|
|
365
416
|
{ 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/ },
|
|
366
417
|
{ 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/ },
|
|
367
418
|
{ name: "failed-jobs reporter disabled", ok: /- name: Report failed jobs\n[ \t]*id: report_failed_jobs\n[ \t]*if: false[^\n]*\n/ },
|
|
419
|
+
// Only agent-refine declares update_issue; the other workers never carry the tool.
|
|
420
|
+
{
|
|
421
|
+
name: "update_issue requires a body",
|
|
422
|
+
ok: lock =>
|
|
423
|
+
!lock.includes('"update_issue": {') ||
|
|
424
|
+
/"update_issue": \{[\s\S]{0,3000}?"body": \{\s*\n\s*"required": true,/.test(lock),
|
|
425
|
+
},
|
|
426
|
+
// The base reaches the handler and never the MCP server; see the rewrite above. Anchored on
|
|
427
|
+
// the escaped quotes because allowed_base_branches contains the bare text base_branch.
|
|
428
|
+
{
|
|
429
|
+
name: "pull request base stays out of the MCP config",
|
|
430
|
+
ok: lock => {
|
|
431
|
+
const mcp = (lock.match(/^[ \t]+GH_AW_SAFE_OUTPUTS_CONFIG: "[^\n]*/m) || [""])[0];
|
|
432
|
+
const handler = (lock.match(/^[ \t]+GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "[^\n]*/m) || [""])[0];
|
|
433
|
+
if (mcp.includes('\\"base_branch\\"')) return false;
|
|
434
|
+
// The handler config keeps the expression for Actions to expand before the job starts;
|
|
435
|
+
// only the MCP config is rewritten to an env reference. Either form is the run's ref.
|
|
436
|
+
return !file.startsWith("agent-implement") ||
|
|
437
|
+
handler.includes('\\"base_branch\\":\\"${{ github.ref_name }}\\"') ||
|
|
438
|
+
handler.includes('\\"base_branch\\":\\"${GITHUB_REF_NAME}\\"');
|
|
439
|
+
},
|
|
440
|
+
},
|
|
368
441
|
]) {
|
|
369
|
-
|
|
442
|
+
const passed = typeof check.ok === "function" ? check.ok(rewritten) : check.ok.test(rewritten);
|
|
443
|
+
if (!passed) {
|
|
370
444
|
process.stderr.write(
|
|
371
445
|
`compile patch "${check.name}" no longer matches in ${file}: the gh-aw template changed. ` +
|
|
372
446
|
`Re-anchor the rewrite in loops/scripts/compile-agent-workflows.mjs, or agent pushes are silently dropped.\n`,
|