@plainconceptsplatform/workflows 1.3.0 → 1.4.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/worker-env.js +2 -3
- package/loops/actions/apply-agent-output/action.yml +113 -108
- package/loops/actions/create-agent-issues/action.yml +113 -52
- package/loops/actions/housekeeping/action.yml +95 -1
- package/loops/actions/validate-merge-gate-output/action.yml +6 -1
- package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +9 -2
- package/loops/actions/validate-refine-output/validate-refine-output.sh +30 -1
- package/loops/actions/verify-refine-output/verify-refine-output.sh +17 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +224 -9
- package/loops/docs/diagrams.md +90 -12
- package/loops/scripts/compile-agent-workflows.mjs +76 -2
- package/loops/workflows/agent-apply-review.md +5 -0
- package/loops/workflows/agent-implement.md +43 -7
- package/loops/workflows/agent-merge-gate.md +136 -8
- package/loops/workflows/agent-refine.md +152 -5
- package/loops/workflows/work-router.yml +330 -2
- package/package.json +1 -1
|
@@ -60,6 +60,27 @@ jq -r --arg marker "$marker" --arg prefix "$comment_prefix" --arg issue "$issue_
|
|
|
60
60
|
) or
|
|
61
61
|
.type == "create_issue");
|
|
62
62
|
|
|
63
|
+
# A stack split also opens the root pull request: the one pull request refine may open, and
|
|
64
|
+
# only alongside the children it integrates. Lyceum #45 emitted the parent body, two children
|
|
65
|
+
# and that root, and was called invalid because a pull request is not a source item, so no
|
|
66
|
+
# stack split had ever been accepted. Complete and questions keep has_only_source_items, so a
|
|
67
|
+
# pull request anywhere but a split is still invalid.
|
|
68
|
+
def root_pr_count:
|
|
69
|
+
[.items[] | select(.type == "create_pull_request")] | length;
|
|
70
|
+
|
|
71
|
+
def has_only_split_items:
|
|
72
|
+
all(.items[] | select(is_run_signal | not);
|
|
73
|
+
(
|
|
74
|
+
.type == "update_issue" and
|
|
75
|
+
(.item_number == null or (.item_number | tostring) == $issue)
|
|
76
|
+
) or
|
|
77
|
+
(
|
|
78
|
+
.type == "add_comment" and
|
|
79
|
+
(.item_number | tostring) == $issue
|
|
80
|
+
) or
|
|
81
|
+
.type == "create_issue" or
|
|
82
|
+
.type == "create_pull_request");
|
|
83
|
+
|
|
63
84
|
# The agent saying it did not finish. Survivable when it left real work behind, never
|
|
64
85
|
# when all it left was a comment: a failed run must not be read as a question, or the
|
|
65
86
|
# loop waits for an answer to something nobody asked.
|
|
@@ -77,7 +98,15 @@ jq -r --arg marker "$marker" --arg prefix "$comment_prefix" --arg issue "$issue_
|
|
|
77
98
|
# complete refinement. A body carrying the draft marker is a temporal draft, not a
|
|
78
99
|
# finished refinement: it only counts as the questions outcome when the matching
|
|
79
100
|
# batched-questions comment is also there.
|
|
80
|
-
|
|
101
|
+
#
|
|
102
|
+
# A comment with no update at all is a question asked before anything was drafted, which
|
|
103
|
+
# the worker is allowed to do. A comment alongside an update that carries no draft is not:
|
|
104
|
+
# that is the shape of a run that called update_issue and delivered no body, which Lyceum
|
|
105
|
+
# #44 produced when the safe-outputs bridge passed 19 bytes instead of the 16,175 the
|
|
106
|
+
# agent piped in. Requiring the draft unconditionally also rejected the first shape and
|
|
107
|
+
# kept this branch red for eleven days; the two are told apart by has_update, not by
|
|
108
|
+
# demanding a draft from both.
|
|
109
|
+
if child_count >= 2 and has_replacement_body and has_only_split_items and root_pr_count <= 1 then "split"
|
|
81
110
|
elif child_count > 0 then "invalid"
|
|
82
111
|
elif has_replacement_body and (has_draft_body | not) and has_only_source_items then "complete"
|
|
83
112
|
elif has_clarification and ((has_update | not) or has_draft_body) and has_only_source_items
|
|
@@ -52,6 +52,23 @@ assert_output 'question with an update is complete' complete \
|
|
|
52
52
|
'{"items":[{"type":"update_issue","item_number":42,"body":"# User story"},{"type":"add_comment","item_number":42,"body":"Which users need this feature?"}]}'
|
|
53
53
|
assert_output 'question with a blank update is invalid' invalid \
|
|
54
54
|
'{"items":[{"type":"update_issue","item_number":42,"body":" "},{"type":"add_comment","item_number":42,"body":"Which users need this feature?"}]}'
|
|
55
|
+
# Lyceum #44: the safe-outputs bridge passed 19 bytes, {"issue_number":44}, instead of the
|
|
56
|
+
# 16,175 the agent piped in, so update_issue arrived with no body key at all. The run ended
|
|
57
|
+
# green and the issue parked waiting for an answer to a question nobody asked. A missing key
|
|
58
|
+
# is not the same shape as a blank string, so it is pinned separately from the case above.
|
|
59
|
+
assert_output 'update with no body key is invalid' invalid \
|
|
60
|
+
'{"items":[{"type":"update_issue","item_number":42}]}'
|
|
61
|
+
assert_output 'update with no body key plus a comment is invalid' invalid \
|
|
62
|
+
'{"items":[{"type":"update_issue","item_number":42},{"type":"add_comment","item_number":42,"body":"Refinement complete"}]}'
|
|
63
|
+
# A stack split opens the root pull request alongside its children. Lyceum #45: the agent
|
|
64
|
+
# emitted the parent body, two children and the root, and the validator called it invalid
|
|
65
|
+
# because a pull request was not a source item, so no stack split had ever been accepted.
|
|
66
|
+
assert_output 'stack split with a root pull request is a split' split \
|
|
67
|
+
'{"items":[{"type":"update_issue","item_number":42,"body":"# Parent"},{"type":"create_issue","title":"A","body":"child A"},{"type":"create_issue","title":"B","body":"child B"},{"type":"create_pull_request","title":"Root","body":"Fixes #42","branch":"feat/42"}]}'
|
|
68
|
+
assert_output 'a split with two root pull requests is invalid' invalid \
|
|
69
|
+
'{"items":[{"type":"update_issue","item_number":42,"body":"# Parent"},{"type":"create_issue","title":"A","body":"child A"},{"type":"create_issue","title":"B","body":"child B"},{"type":"create_pull_request","title":"Root","body":"x","branch":"feat/42"},{"type":"create_pull_request","title":"Again","body":"y","branch":"feat/43"}]}'
|
|
70
|
+
assert_output 'a pull request on a complete refinement is invalid' invalid \
|
|
71
|
+
'{"items":[{"type":"update_issue","item_number":42,"body":"# User story"},{"type":"create_pull_request","title":"Root","body":"x","branch":"feat/42"}]}'
|
|
55
72
|
assert_output 'wrong issue output is invalid' invalid \
|
|
56
73
|
'{"items":[{"type":"add_comment","item_number":7,"body":"Which users need this feature?"}]}'
|
|
57
74
|
assert_output 'complete output cannot update another issue' invalid \
|
|
@@ -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
|
|
@@ -1574,10 +1576,10 @@ if [ -f "$GATE_VALIDATOR" ] && worker_installed merge-gate; then
|
|
|
1574
1576
|
|
|
1575
1577
|
gate_case() {
|
|
1576
1578
|
local name="$1" want="$2" json="$3" conclusion="$4"
|
|
1577
|
-
local blast="${5:-low}" protected="${6:-false}" owner="${7:-false}"
|
|
1579
|
+
local blast="${5:-low}" protected="${6:-false}" owner="${7:-false}" code="${8-pass}"
|
|
1578
1580
|
printf '%s' "$json" > "$gate_fixture"
|
|
1579
1581
|
local got
|
|
1580
|
-
got=$(bash "$GATE_VALIDATOR" "$gate_fixture" 7 "$conclusion" "$blast" "$protected" "$owner" 0.8 2>&1)
|
|
1582
|
+
got=$(bash "$GATE_VALIDATOR" "$gate_fixture" 7 "$conclusion" "$blast" "$protected" "$owner" 0.8 "$code" 2>&1)
|
|
1581
1583
|
if [ "$got" != "$want" ]; then
|
|
1582
1584
|
VALIDATOR_OK=0
|
|
1583
1585
|
echo "FAIL: the merge-gate validator called '${name}' ${got}, expected ${want}" >&2
|
|
@@ -1603,6 +1605,14 @@ if [ -f "$GATE_VALIDATOR" ] && worker_installed merge-gate; then
|
|
|
1603
1605
|
gate_case "an owner path needs the owner" owner-review "$(gate_items "$(gate_comment assessed)")" success low false true
|
|
1604
1606
|
gate_case "a non-success CI conclusion blocks" blocked "$(gate_items "$(gate_comment assessed)")" failure low
|
|
1605
1607
|
|
|
1608
|
+
# check-diff on the pull request head is a measured fact like CI: failing it never merges,
|
|
1609
|
+
# a repository without the script is not held to it, and a job that did not report is a person's call.
|
|
1610
|
+
gate_case "failing check-diff blocks" blocked "$(gate_items "$(gate_comment assessed)")" success low false false fail
|
|
1611
|
+
gate_case "no check-diff installed auto-merges" auto-merge "$(gate_items "$(gate_comment assessed)")" success low false false absent
|
|
1612
|
+
gate_case "check-diff that did not report needs a person" \
|
|
1613
|
+
human-review "$(gate_items "$(gate_comment assessed)")" success low false false ''
|
|
1614
|
+
gate_case "remediating check-diff is accepted" remediated "$(gate_items "$(gate_comment remediated),${gate_push}")" success low false false fail
|
|
1615
|
+
|
|
1606
1616
|
# The agent's report decides the rest. This is the rule the whole redesign rests on: an
|
|
1607
1617
|
# unverified finding is a warning whatever severity it claims, so a model cannot fail the gate
|
|
1608
1618
|
# by asserting something it did not demonstrate, and cannot pass it by understating one it did.
|
|
@@ -2326,6 +2336,16 @@ if worker_installed refine; then
|
|
|
2326
2336
|
echo "FAIL: the refine refusal never releases bot-working; a refused issue stays reserved with no run behind it" >&2
|
|
2327
2337
|
fi
|
|
2328
2338
|
|
|
2339
|
+
# The split floor: a split child carries a whole stack's machinery (issue, feature branch,
|
|
2340
|
+
# PR into the branch), so a 1- or 2-point slice is a fragment and must be folded into a
|
|
2341
|
+
# sibling instead. The floor must be pinned through the env var the prompt interpolates,
|
|
2342
|
+
# so a consumer cannot lower it below 3 without editing the worker.
|
|
2343
|
+
if ! grep -qF 'MIN_CHILD_POINTS: "3"' "$REFINE_WORKER_MD" \
|
|
2344
|
+
|| ! grep -qF 'child estimated below ${{ env.MIN_CHILD_POINTS }}' "$REFINE_WORKER_MD"; then
|
|
2345
|
+
SIZE_GATE_OK=0
|
|
2346
|
+
echo "FAIL: refine allows split children below 3 points; the floor must stay interpolated from MIN_CHILD_POINTS" >&2
|
|
2347
|
+
fi
|
|
2348
|
+
|
|
2329
2349
|
if [ "$SIZE_GATE_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
2330
2350
|
fi
|
|
2331
2351
|
|
|
@@ -2612,8 +2632,10 @@ if [ -f "$APP_ERRORS_MJS" ]; then
|
|
|
2612
2632
|
# 3. The field allowlist is what actually holds the line, because a redaction rule filters
|
|
2613
2633
|
# values and cannot see a field somebody adds next year. Compared as a set, so reordering is
|
|
2614
2634
|
# fine and an addition is not. run_id is the one that matters: it is unhashed on this
|
|
2615
|
-
# telemetry and joins to a conversation and to a person.
|
|
2616
|
-
|
|
2635
|
+
# telemetry and joins to a conversation and to a person. category, fingerprint, message and
|
|
2636
|
+
# topFrames joined in v0.28.7 to enrich the filed issues (a84928a); they are set-side
|
|
2637
|
+
# fields, never engine telemetry, so the allowlist grows with them.
|
|
2638
|
+
expected_fields="category exceptionType fingerprint firstSeen frames lastSeen message occurrences operationName operations problemId roleName topFrames"
|
|
2617
2639
|
actual_fields="$(sed -n '/export const FINDING_FIELDS = \[/,/\];/p' "$APP_ERRORS_MJS" |
|
|
2618
2640
|
grep -oE '"[a-zA-Z]+"' | tr -d '"' | sort | tr '\n' ' ' | sed 's/ $//')"
|
|
2619
2641
|
if [ "$actual_fields" = "$(echo "$expected_fields" | tr ' ' '\n' | sort | tr '\n' ' ' | sed 's/ $//')" ]; then
|
|
@@ -2654,9 +2676,202 @@ if [ -f "$APP_ERRORS_MJS" ]; then
|
|
|
2654
2676
|
if [ "$AE_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
2655
2677
|
fi
|
|
2656
2678
|
|
|
2679
|
+
echo "── Stacked features ──────────────────────────────────────────────────────"
|
|
2680
|
+
|
|
2681
|
+
# Stacks are delivered by marker, not by label: every flat-mode rule still has to hold on a
|
|
2682
|
+
# repository that has never seen a marker. The assertions here are grouped as the two halves
|
|
2683
|
+
# of that contract. The first says the stack machinery exists and fails closed: an unreadable
|
|
2684
|
+
# or missing marker must route a child down the flat path, never dispatch at a branch nobody
|
|
2685
|
+
# resolved. The second says the flat path never learned about stacks: the default-path jobs
|
|
2686
|
+
# and prompts carry no stack condition that could fire on an issue without markers.
|
|
2687
|
+
STACK_OK=1
|
|
2688
|
+
st() {
|
|
2689
|
+
grep -qE "$1" "$2" || { STACK_OK=0; echo "FAIL: stacked features ${3}" >&2; }
|
|
2690
|
+
}
|
|
2691
|
+
# The router's stack job is a block of a derived file, not a file of its own, so its
|
|
2692
|
+
# assertions take the block on stdin: handing a multi-line string to grep as a filename
|
|
2693
|
+
# fails with "File name too long" and reads as a wiring failure that is not one.
|
|
2694
|
+
st_str() {
|
|
2695
|
+
printf '%s' "$2" | grep -qE "$1" || { STACK_OK=0; echo "FAIL: stacked features ${3}" >&2; }
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
# 1. The resolver. check-stack-branch reads the child's own marker first, then the
|
|
2699
|
+
# split-parent chain; both are `|| true` so a flat body (no match) cannot fail the job --
|
|
2700
|
+
# call-implement names this job in its needs, so a hard failure would block flat implements.
|
|
2701
|
+
CHECK_STACK_JOB=$(sed -n '/^ check-stack-branch:$/,/^ [a-z0-9_-]*:$/p' "$ROUTER_YML")
|
|
2702
|
+
if [ -n "$CHECK_STACK_JOB" ]; then
|
|
2703
|
+
st "stack-root-branch: \[\\^ \]\+" "$ROUTER_YML" 'has no marker regex for the feature branch'
|
|
2704
|
+
st_str 'exit 0' "$CHECK_STACK_JOB" 'resolver hard-fails instead of taking the flat path'
|
|
2705
|
+
st_str 'sed .s\|/\|%2F\|g.' "$CHECK_STACK_JOB" 'does not URL-escape the stack branch before the existence check'
|
|
2706
|
+
st_str 'sed -E .s/<!-- stack-root-branch: \(\[\^ \]\+\) -->/\\1/.' "$CHECK_STACK_JOB" 'does not strip the marker syntax off the branch'
|
|
2707
|
+
# The dupe guard: only dispatched stack implements run with head_branch = feat/N, so a
|
|
2708
|
+
# workflow_call (flat) run whose head_branch is the default branch can never match it.
|
|
2709
|
+
st_str 'head_branch == ..\$\{STACK_BRANCH\}..' "$CHECK_STACK_JOB" 'does not detect an in-flight implement on the feature branch'
|
|
2710
|
+
# Dispatch happens on the resolved ref, never on the event SHA. The `gh workflow run`
|
|
2711
|
+
# lives in dispatch-implement, which check-stack-branch gates: the two jobs verify the
|
|
2712
|
+
# branch exists, then hand off. The run needs a fresh App token -- GITHUB_TOKEN can
|
|
2713
|
+
# start no workflow -- and it reads repo-level secrets because a dispatch cannot forward
|
|
2714
|
+
# workflow-call ones, which is the activation prerequisite ownership.md documents.
|
|
2715
|
+
DISPATCH_JOB=$(sed -n '/^ dispatch-implement:$/,/^ [a-z0-9_-]*:$/p' "$ROUTER_YML")
|
|
2716
|
+
if [ -n "$DISPATCH_JOB" ]; then
|
|
2717
|
+
st_str 'gh workflow run agent-implement\.lock\.yml .* --ref "\$STACK_BRANCH"' "$DISPATCH_JOB" 'does not dispatch the stack implement at all'
|
|
2718
|
+
st_str 'create-github-app-token' "$DISPATCH_JOB" 'dispatches with GITHUB_TOKEN, which can start no run'
|
|
2719
|
+
# A child blocked by an open sibling is held, and the sibling's landing releases it. Without
|
|
2720
|
+
# both halves a dependent child cuts from a feature branch that lacks its dependency (Lyceum
|
|
2721
|
+
# #112, dispatched alongside #111), or is held forever.
|
|
2722
|
+
st_str "stack-blocked != 'true'" "$DISPATCH_JOB" 'dispatches a child whose blockers have not landed'
|
|
2723
|
+
st 'name: Hold a child whose blockers have not landed' "$ROUTER_YML" 'never holds a child behind the siblings it is blocked by'
|
|
2724
|
+
st 'state merged --base "\$STACK_BRANCH"' "$ROUTER_YML" 'holds a child behind a blocker whose pull request already landed, which nothing releases again'
|
|
2725
|
+
st 'Released #\$child: everything it was blocked by has landed' "$ROUTER_YML" 'holds blocked children but never releases them'
|
|
2726
|
+
fi
|
|
2727
|
+
# The child's pull request targets the ref the run was dispatched on, taken from the run.
|
|
2728
|
+
# Asking the agent for it failed in practice (Lyceum #72, run 38694942046): it emitted no
|
|
2729
|
+
# base, the handler fell back to the default branch, and a bundle cut from the feature
|
|
2730
|
+
# branch would not replay there. Both halves are checked, because a prompt that asks again
|
|
2731
|
+
# would invite an explicit base that disagrees with the ref.
|
|
2732
|
+
st 'base-branch: \$\{\{ github\.ref_name \}\}' "$IMPLEMENT_WORKER_MD" 'leaves the pull request base to the agent instead of the dispatched ref'
|
|
2733
|
+
if grep -qE 'base="feat/N"|pass `base: "feat/N"`' "$IMPLEMENT_WORKER_MD"; then
|
|
2734
|
+
STACK_OK=0
|
|
2735
|
+
echo "FAIL: stacked features prompt asks the agent for the base again; the run already sets it" >&2
|
|
2736
|
+
fi
|
|
2737
|
+
else
|
|
2738
|
+
# check-stack-branch serves only the implement route, so the CLI drops it from a router
|
|
2739
|
+
# that did not install implement. Nothing to assert then, and the section above must not
|
|
2740
|
+
# claim a pass for a job that is legitimately absent.
|
|
2741
|
+
:
|
|
2742
|
+
fi
|
|
2743
|
+
|
|
2744
|
+
# 2. Completion. The merge of a child PR into a feature branch (base != default branch) is
|
|
2745
|
+
# the only event that can mean "one fewer open child". The router block must be idempotent:
|
|
2746
|
+
# the announcement carries <!-- stack-complete --> and a re-run that finds it stays silent.
|
|
2747
|
+
st 'stack-complete' "$ROUTER_YML" 'has no idempotent completion marker on the root pull request'
|
|
2748
|
+
st 'closingIssuesReferences\[0\]\.number // empty' "$ROUTER_YML" 'does not read the child PR to find the closed issue'
|
|
2749
|
+
st 'split-parent: \[0-9\]+' "$ROUTER_YML" 'does not walk from the child to its feature issue'
|
|
2750
|
+
st 'stack-root-pr: \[0-9\]+' "$ROUTER_YML" 'does not read the root pull request number off the feature issue'
|
|
2751
|
+
# gh pr view and gh issue view answer OPEN, not open. An exact lowercase match on their state
|
|
2752
|
+
# never matches, which left every root closed and every child done as far as completion knew.
|
|
2753
|
+
# The child whose pull request just landed is done even if the gate has not closed its issue yet.
|
|
2754
|
+
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'
|
|
2755
|
+
if grep -nE "(gh pr view|gh issue view)[^|]*--json state[^|]*\| *grep -q?x open" "$ROUTER_YML" >/dev/null; then
|
|
2756
|
+
STACK_OK=0
|
|
2757
|
+
echo "FAIL: stacked features compares a GraphQL state (OPEN) to lowercase open; the root and children never read as open" >&2
|
|
2758
|
+
fi
|
|
2759
|
+
|
|
2760
|
+
# 3. The gate. A child PR that lands on a feature branch closes exactly its own child issue
|
|
2761
|
+
# there (GitHub's closing keywords do not fire off the default branch); the root is held
|
|
2762
|
+
# for a person with the App token, so anybody reading the timeline can replace it. The
|
|
2763
|
+
# marker test on the issue body is what makes both stack-specific and invisible flat.
|
|
2764
|
+
if worker_installed merge-gate; then
|
|
2765
|
+
st 'stack-root-pr: \$PR -->' "$MERGE_GATE_WORKER_MD" 'does not test the feature issue for the root marker'
|
|
2766
|
+
st 'needs\.subject\.outputs\.stack_root == .true.' "$MERGE_GATE_WORKER_MD" 'has no root-hold branch before the merge step'
|
|
2767
|
+
# The stack step reads the subject's PR; with no subject it fails on an empty number.
|
|
2768
|
+
gate_stack_step=$(sed -n '/- name: Detect the pull request.s base and its role in a stack/,/run: |/p' "$MERGE_GATE_WORKER_MD")
|
|
2769
|
+
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'
|
|
2770
|
+
st '\*\*Stack root held\.\*\*' "$MERGE_GATE_WORKER_MD" 'posts no human-readable hold explanation on the root'
|
|
2771
|
+
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'
|
|
2772
|
+
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'
|
|
2773
|
+
fi
|
|
2774
|
+
|
|
2775
|
+
# 4. The split. Refine creates the feature branch and the held root PR together with the
|
|
2776
|
+
# children, then stamps markers on parent and children before any hand-over: the markers are
|
|
2777
|
+
# what the resolver, the gate and the janitor read, so no new label exists to move. The root
|
|
2778
|
+
# PR body must close the feature issue -- that is what ends the feature when the human merges.
|
|
2779
|
+
if worker_installed refine; then
|
|
2780
|
+
REFINE_WORKER_MD="${WORKFLOWS_DIR}/agent-refine.md"
|
|
2781
|
+
st 'allowed-branches: \["feat/\*"\]' "$REFINE_WORKER_MD" 'does not constrain the feature branch name'
|
|
2782
|
+
st 'allowed-base-branches: \["main"\]' "$REFINE_WORKER_MD" 'lets the root target a branch other than the default'
|
|
2783
|
+
st 'preserve-branch-name: true' "$REFINE_WORKER_MD" 'does not preserve the feature branch name on root PR creation'
|
|
2784
|
+
# Refine stages its outputs and applies them itself, and gh-aw opens no pull request while a
|
|
2785
|
+
# worker is staged, not even one marked `staged: false`. The root has to be opened by the
|
|
2786
|
+
# workflow, and the stamp and the hand-over have to read that number, or every child goes flat.
|
|
2787
|
+
st '^ staged: true$' "$REFINE_WORKER_MD" 'no longer stages the body and children for validation before they are applied'
|
|
2788
|
+
st 'name: Open the stack root pull request' "$REFINE_WORKER_MD" 'never opens the root pull request; the staged handler only previews it'
|
|
2789
|
+
st 'PR_NUMBER: \$\{\{ steps\.root\.outputs\.pr_number' "$REFINE_WORKER_MD" 'stamps the stack markers without the root the workflow opened'
|
|
2790
|
+
st 'ROOT_PR: \$\{\{ steps\.root\.outputs\.pr_number' "$REFINE_WORKER_MD" 'hands the children over without the root the workflow opened'
|
|
2791
|
+
st 'stack-root-branch: %s' "$REFINE_WORKER_MD" 'does not stamp the feature branch on the parent issue'
|
|
2792
|
+
st 'Fixes #\$\{\{ inputs\.issue-number \}\}' "$REFINE_WORKER_MD" 'does not make the root PR close the feature issue'
|
|
2793
|
+
# The hand-over stamp: a child handed to implement carries the feature branch marker, so
|
|
2794
|
+
# the resolver reads it off the child body without chasing the chain first.
|
|
2795
|
+
st 'stack-root-branch: %s' "$REFINE_WORKER_MD" 'does not stamp the feature branch on each child at hand-over'
|
|
2796
|
+
# The children have to be found. gh-aw's sanitizer strips HTML comments from agent_output.json,
|
|
2797
|
+
# so the split-parent marker the agent writes never arrives, and staged, nothing links the
|
|
2798
|
+
# children as sub-issues either (Lyceum #45, run 38747143251: three children, none handed over).
|
|
2799
|
+
# The workflow tells the apply which issue the children belong to, and the apply stamps the
|
|
2800
|
+
# marker and links the sub-issue itself.
|
|
2801
|
+
CREATE_ISSUES_ACTION="${HERE}/../create-agent-issues/action.yml"
|
|
2802
|
+
# Dependencies travel as `Blocked by #aw_<id>` lines the apply resolves, never as the
|
|
2803
|
+
# blocked_by field: a value naming two siblings failed the staged preview and threw away a
|
|
2804
|
+
# finished refinement (Lyceum #45, run 38770811334).
|
|
2805
|
+
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'
|
|
2806
|
+
st 'Blocked by #aw_<id>' "$REFINE_WORKER_MD" 'asks for sibling numbers the agent cannot know yet'
|
|
2807
|
+
st 'parent-issue: \$\{\{ needs\.validate_output\.outputs\.outcome == .split.' "$REFINE_WORKER_MD" 'does not tell the apply which issue the split children belong to'
|
|
2808
|
+
st 'issues/\{issue_number\}/sub_issues' "$CREATE_ISSUES_ACTION" 'does not link split children as sub-issues of their parent'
|
|
2809
|
+
st 'split-parent: \$\{parent\}' "$CREATE_ISSUES_ACTION" 'does not stamp the split-parent marker that the sanitizer strips from the agent'
|
|
2810
|
+
# The guard against re-handing a child already handed over: it carries the refined label,
|
|
2811
|
+
# and handing it again would start a second implement on work in flight.
|
|
2812
|
+
st 'grep -qx "\$REFINED_LABEL"' "${WORKFLOWS_DIR}/agent-refine.md" 'does not skip an already-handed-over child'
|
|
2813
|
+
fi
|
|
2814
|
+
|
|
2815
|
+
# 5. Housekeeping. The janitor's stack section is the backstop for every event the router can
|
|
2816
|
+
# miss, and it ran unattended for months before stacks existed. Three of its guardrails are
|
|
2817
|
+
# asserted here the way section 3's are. The marker test fails first on a repository without
|
|
2818
|
+
# stacks, so the flat cost is one regex per open issue.
|
|
2819
|
+
if [ -f "$HOUSEKEEPING_YML" ]; then
|
|
2820
|
+
st 'stack-root-pr: \(\\d\+\)' "$HOUSEKEEPING_YML" 'does not find the feature issues to sweep'
|
|
2821
|
+
st 'implement-pr: \(\\d\+\)' "$HOUSEKEEPING_YML" 'does not read each child pull request number'
|
|
2822
|
+
st "merged_at" "$HOUSEKEEPING_YML" 'does not test whether the child pull request actually landed'
|
|
2823
|
+
# The orphan close: a child whose named PR merged but whose issue never closed is closed
|
|
2824
|
+
# here. Both its closes state a reason, like every other close in the file.
|
|
2825
|
+
if ! grep -qE "github\.rest\.issues\.update\(\{ \.\.\.context\.repo, issue_number: child\.number, state: 'closed', state_reason: 'completed' \}\)" "$HOUSEKEEPING_YML"; then
|
|
2826
|
+
STACK_OK=0
|
|
2827
|
+
echo "FAIL: housekeeping no longer closes an orphaned child whose pull request landed" >&2
|
|
2828
|
+
fi
|
|
2829
|
+
# The flat guarantees. Section 3 must still refuse to close a stack parent (the root PR
|
|
2830
|
+
# closing the feature issue is what ends it, not the sweep), and section 4 must still not
|
|
2831
|
+
# delete a branch whose pull request is open -- an open root PR protects feat/N. The skip
|
|
2832
|
+
# is asserted on its real shape: the marker test inside the issues.some() predicate.
|
|
2833
|
+
st 'stack-root-branch: \\S\+ -->/\.test\(i\.body' "$HOUSEKEEPING_YML" 'no longer skips stack parents in the split-parent close'
|
|
2834
|
+
st "p\.state === 'open'\)\) continue" "$HOUSEKEEPING_YML" 'can delete a feature branch whose root pull request is still open'
|
|
2835
|
+
fi
|
|
2836
|
+
|
|
2837
|
+
# 6. The compat guarantee, stated as absence. A repository without markers sees none of
|
|
2838
|
+
# this: the default path carries no stack condition that could fire. The implement worker's
|
|
2839
|
+
# prompt lines that name the markers are conditional on them being present on the issue.
|
|
2840
|
+
if worker_installed implement; then
|
|
2841
|
+
# The instruction naming the marker exists (the marker-gated base instruction), and the
|
|
2842
|
+
# flat instruction ("propose against main") is still in place alongside it.
|
|
2843
|
+
st 'stack-root-branch: feat/N -->' "$IMPLEMENT_WORKER_MD" 'does not tell stack children to base their PR on the feature branch'
|
|
2844
|
+
st 'allowed-base-branches:' "$IMPLEMENT_WORKER_MD" 'does not declare the base-branch allowlist for create-pull-request'
|
|
2845
|
+
# The base allowlist denies everything the marker test did not name. main stays first and
|
|
2846
|
+
# unconditional; the feature pattern is additive, never a replacement.
|
|
2847
|
+
if [ "$(count -c '^ allowed-base-branches:' "$IMPLEMENT_WORKER_MD")" -ne 1 ]; then
|
|
2848
|
+
STACK_OK=0
|
|
2849
|
+
echo "FAIL: agent-implement.md declares allowed-base-branches more than once or not at all" >&2
|
|
2850
|
+
fi
|
|
2851
|
+
if ! sed -n '/allowed-base-branches:/,/^ [a-z]/p' "$IMPLEMENT_WORKER_MD" | grep -q '"main"'; then
|
|
2852
|
+
STACK_OK=0
|
|
2853
|
+
echo "FAIL: agent-implement.md allowed-base-branches dropped the default branch" >&2
|
|
2854
|
+
fi
|
|
2855
|
+
# The dispatch trigger must stay optional and marker-gated: a flat implement is a
|
|
2856
|
+
# workflow_call, never a dispatch, so the checkout ternary keys off workflow_dispatch.
|
|
2857
|
+
st 'workflow_dispatch' "$IMPLEMENT_WORKER_MD" 'no longer accepts workflow_dispatch'
|
|
2858
|
+
fi
|
|
2859
|
+
|
|
2860
|
+
# 7. No stack label anywhere. Stack semantics live in markers on issues and pull requests;
|
|
2861
|
+
# a label would have to be moved by the App token to raise events, and every new label is a
|
|
2862
|
+
# new consumer-visible surface. Assert the absence of any stack-named label action.
|
|
2863
|
+
if grep -nE "add-label .*$|add-labels$" "$ROUTER_YML" "$MERGE_GATE_WORKER_MD" "${WORKFLOWS_DIR}/agent-refine.md" "$IMPLEMENT_WORKER_MD" 2>/dev/null |
|
|
2864
|
+
grep -qiE 'stack'; then
|
|
2865
|
+
STACK_OK=0
|
|
2866
|
+
echo "FAIL: a workflow adds a stack-named label; stack semantics belong to markers" >&2
|
|
2867
|
+
fi
|
|
2868
|
+
|
|
2869
|
+
if [ "$STACK_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
2870
|
+
|
|
2657
2871
|
echo
|
|
2658
2872
|
if [ "$FAIL" -eq 0 ]; then
|
|
2659
|
-
|
|
2873
|
+
TcMsg="Route matrix: ${PASS} passed"
|
|
2874
|
+
echo "$TcMsg"
|
|
2660
2875
|
else
|
|
2661
2876
|
echo "Route matrix: ${PASS} passed, ${FAIL} FAILED" >&2
|
|
2662
2877
|
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
|