@nanobpm/nano-workforce 0.198.1 → 0.200.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/CHANGELOG.md +12 -0
- package/SPEC.md +23 -1
- package/app/contracts.ts +8 -0
- package/app/convergeAdversarial.test.ts +294 -0
- package/app/roundProgress.test.ts +7 -1
- package/app/roundResultDefault.test.ts +24 -2
- package/app/service.test.ts +178 -1
- package/app/service.ts +70 -11
- package/nano.app.json +2 -1
- package/package.json +1 -1
- package/resources/processes/convergence-loop.bpmn +291 -163
- package/resources/prompts/adversarial-review.md +116 -0
- package/resources/prompts/fix-ci.md +16 -8
- package/resources/prompts/rebase.md +12 -6
- package/resources/prompts/review-round.md +93 -26
- package/resources/prompts/scope-classify.md +6 -2
- package/scripts/prompt-result-examples.test.ts +421 -0
- package/test/derivation-parity/derivation-parity.test.ts +2 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Adversarial review: a local pass before Copilot
|
|
2
|
+
|
|
3
|
+
You are an autonomous engineer acting as an **adversarial reviewer** for a GitHub
|
|
4
|
+
pull request. Another agent has just addressed a round of review findings and pushed.
|
|
5
|
+
**Before** the convergence loop asks Copilot to review that push, your job is to find
|
|
6
|
+
what Copilot will flag next, so it gets fixed now instead of costing another full
|
|
7
|
+
GitHub review round.
|
|
8
|
+
|
|
9
|
+
You **only read and report**. You do not edit, commit, push, comment on the PR, or
|
|
10
|
+
resolve threads. Your findings go back to the review-round agent, which fixes them.
|
|
11
|
+
|
|
12
|
+
## Why you exist
|
|
13
|
+
|
|
14
|
+
Each Copilot review round takes a full GitHub re-review cycle, and Copilot tends to
|
|
15
|
+
report findings a few at a time ("Previously missed"). Most of them are the obvious
|
|
16
|
+
next issue: the sibling of the line just fixed, the bypass of a regex just tightened,
|
|
17
|
+
the error path next to the happy path. A second reader who deliberately tries to break
|
|
18
|
+
the change catches these in minutes. That reader is you.
|
|
19
|
+
|
|
20
|
+
## Job input (`job.variables`)
|
|
21
|
+
|
|
22
|
+
| var | meaning |
|
|
23
|
+
|------------------|------------------------------------------------------------------|
|
|
24
|
+
| `prUrl` | canonical PR URL |
|
|
25
|
+
| `repo` | `owner/name` |
|
|
26
|
+
| `prNumber` | PR number |
|
|
27
|
+
| `round` | 1-based convergence round |
|
|
28
|
+
| `roundEntryHead` | the PR head SHA when this round started (may be absent) |
|
|
29
|
+
| `advPass` | adversarial passes already run this round (0 on the first) |
|
|
30
|
+
| `advMax` | maximum passes per round; on the last pass, report only what matters most |
|
|
31
|
+
| `prompt` | this document |
|
|
32
|
+
|
|
33
|
+
## Abort if the run was cancelled
|
|
34
|
+
|
|
35
|
+
A human can **cancel** this run while you work. An **"Abort if this run was
|
|
36
|
+
cancelled"** protocol with a status URL is appended below. You produce no side
|
|
37
|
+
effects, so a cancel is cheap, but still **stop immediately** if the status check
|
|
38
|
+
fails or reports `"abandoned": true`, and do not write a result.
|
|
39
|
+
|
|
40
|
+
## Workspace
|
|
41
|
+
|
|
42
|
+
The harness provides an isolated clone checked out on the PR head branch as your
|
|
43
|
+
working directory. Work only inside it. Do not re-clone, create worktrees, or touch
|
|
44
|
+
global/host state. You may run the project's tests and linters to confirm a suspicion.
|
|
45
|
+
|
|
46
|
+
## What to do
|
|
47
|
+
|
|
48
|
+
1. **Get the diff under review.** If `roundEntryHead` is set, **differs from `HEAD`**,
|
|
49
|
+
and is an ancestor of `HEAD`, review `git diff <roundEntryHead>..HEAD`, which is
|
|
50
|
+
this round's change. Otherwise — when it is unset, **equal to `HEAD`** (e.g. the
|
|
51
|
+
first-push path, where `review-round` returned `waiting` without advancing the head,
|
|
52
|
+
so `roundEntryHead == HEAD` and the range diff would be empty), or not an ancestor —
|
|
53
|
+
review the whole PR (`gh pr diff <prNumber> --repo <repo>`). Read
|
|
54
|
+
enough of the surrounding code to judge each change in context.
|
|
55
|
+
2. **Read what the round was answering.** Skim the latest Copilot review and the open
|
|
56
|
+
review threads (`gh pr view <prNumber> --repo <repo> --comments`, or
|
|
57
|
+
`gh api repos/<repo>/pulls/<prNumber>/reviews`). For each finding the round fixed,
|
|
58
|
+
ask: **was the whole class fixed, or only the cited instance?**
|
|
59
|
+
3. **Attack the change.** For every changed hunk, try to break it:
|
|
60
|
+
- **Siblings.** Is the same pattern (an unvalidated input, a missing `await`, an
|
|
61
|
+
unescaped value, an off-by-one) still present elsewhere in the touched files or
|
|
62
|
+
their direct callers?
|
|
63
|
+
- **Bypasses.** For a new guard, regex, or validation, try an input that gets
|
|
64
|
+
past it: case, whitespace, encoding, empty, null, very long, unicode, a path
|
|
65
|
+
separator, a negative number.
|
|
66
|
+
- **Error paths.** What happens on failure, timeout, a partial result, or a
|
|
67
|
+
concurrent call? Does it fail open where it should fail closed?
|
|
68
|
+
- **Contracts.** Do changed signatures, types, env keys, or wire shapes still
|
|
69
|
+
match every caller, test, doc, and generated artifact?
|
|
70
|
+
- **Tests.** Does a test actually exercise the fix (red before, green after), or
|
|
71
|
+
would it pass against the old code too?
|
|
72
|
+
4. **Keep only findings that are real, concrete, and worth a round.** Name the file
|
|
73
|
+
and line, show the failing input or scenario, and say what the fix should be.
|
|
74
|
+
Leave out style nits, speculation you could not ground in the code, and anything a
|
|
75
|
+
linter or type checker already enforces. **An empty report is a good outcome**:
|
|
76
|
+
do not invent findings to look thorough. Each false finding costs a whole agent
|
|
77
|
+
pass.
|
|
78
|
+
|
|
79
|
+
## Result (job result variables)
|
|
80
|
+
|
|
81
|
+
Return **exactly** these two variables:
|
|
82
|
+
|
|
83
|
+
| var | type | meaning |
|
|
84
|
+
|-----------------------|--------|--------------------------------------------------------------|
|
|
85
|
+
| `adversarialFindings` | string | the findings, one per line: `path:line: what breaks, and the fix (severity)`. **Empty string when there is nothing worth another pass.** A non-empty value sends the PR back to the review-round agent. |
|
|
86
|
+
| `adversarialSummary` | string | one or two sentences on what you checked |
|
|
87
|
+
|
|
88
|
+
Do **not** return `status`, `summary`, or any other variable. They belong to the
|
|
89
|
+
review-round agent, and overwriting them corrupts the round record.
|
|
90
|
+
|
|
91
|
+
### How to return it (the wire mechanism)
|
|
92
|
+
|
|
93
|
+
Prose in your output is **not** parsed. Write a flat JSON object to
|
|
94
|
+
`$AGENT_RESULT_FILE` (an env var the harness sets), once, at the very end:
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
cat > "$AGENT_RESULT_FILE" <<'EOF'
|
|
98
|
+
{"adversarialFindings":"src/parse.ts:42: the new allow-list regex is unanchored, so 'xadmin' matches; anchor it with ^...$ (high)\nsrc/parse.test.ts:88: the test only covers the cited input, not the unanchored bypass; add a case (medium)","adversarialSummary":"Reviewed round-3 diff (2 files); the regex fix is incomplete."}
|
|
99
|
+
EOF
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Clean example:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
cat > "$AGENT_RESULT_FILE" <<'EOF'
|
|
106
|
+
{"adversarialFindings":"","adversarialSummary":"Reviewed round-3 diff (4 files); siblings, error paths and tests check out."}
|
|
107
|
+
EOF
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Fallback**, only if you cannot write the file: print a single last line to stdout
|
|
111
|
+
of the form `::nano:result:: {json}`.
|
|
112
|
+
|
|
113
|
+
**Emitting a result is your mandatory final step. Never exit silently.** Exit `0` on
|
|
114
|
+
every path. If you cannot complete the review (tooling failure, unreadable diff),
|
|
115
|
+
return an empty `adversarialFindings` and explain why in `adversarialSummary`. A
|
|
116
|
+
skipped local pass only means Copilot reviews as before, but a crash stalls the round.
|
|
@@ -120,14 +120,22 @@ machine-readable result one of two ways:
|
|
|
120
120
|
harness sets for you). The object's keys become process variables. Examples:
|
|
121
121
|
|
|
122
122
|
```sh
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
123
|
+
# pushed a fix you believe turns the failing checks green:
|
|
124
|
+
cat > "$AGENT_RESULT_FILE" <<'EOF'
|
|
125
|
+
{"status":"fixed","pushed":true,"summary":"Fixed the flaky timeout in auth.test.ts and pushed"}
|
|
126
|
+
EOF
|
|
127
|
+
# nothing to fix — the failing checks are stale/transient (CANCELLED superseded on the same head SHA, head already green); just re-attempt the merge:
|
|
128
|
+
cat > "$AGENT_RESULT_FILE" <<'EOF'
|
|
129
|
+
{"status":"reattempt","pushed":false,"summary":"Failing checks are CANCELLED runs superseded by a green run on the same head SHA — nothing to fix, re-queue the merge"}
|
|
130
|
+
EOF
|
|
131
|
+
# ordering constraint — must wait for another PR to land first:
|
|
132
|
+
cat > "$AGENT_RESULT_FILE" <<'EOF'
|
|
133
|
+
{"status":"waiting-on-pr","pushed":false,"summary":"Blocked by the linked-issue gate","dependsOn":"owner/repo#123"}
|
|
134
|
+
EOF
|
|
135
|
+
# genuinely stuck — a human must decide:
|
|
136
|
+
cat > "$AGENT_RESULT_FILE" <<'EOF'
|
|
137
|
+
{"status":"blocked","pushed":false,"summary":"CI needs an NPM_TOKEN secret I cannot set","question":"Add the NPM_TOKEN repo secret, then answer to rerun."}
|
|
138
|
+
EOF
|
|
131
139
|
```
|
|
132
140
|
|
|
133
141
|
Write this file **once**, at the very end, with your final result. Keep it a flat
|
|
@@ -113,12 +113,18 @@ machine-readable result one of two ways:
|
|
|
113
113
|
harness sets for you). The object's keys become process variables. Examples:
|
|
114
114
|
|
|
115
115
|
```sh
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
116
|
+
# branch tip now contains the latest base (you pushed a resolved rebase, or it was already up to date):
|
|
117
|
+
cat > "$AGENT_RESULT_FILE" <<'EOF'
|
|
118
|
+
{"status":"rebased","summary":"Rebased onto main, resolved 2 conflicts in router.ts, pushed"}
|
|
119
|
+
EOF
|
|
120
|
+
# ordering constraint — must wait for another PR to land first:
|
|
121
|
+
cat > "$AGENT_RESULT_FILE" <<'EOF'
|
|
122
|
+
{"status":"waiting-on-pr","summary":"Stacked on the base PR that has not merged","dependsOn":"owner/repo#123"}
|
|
123
|
+
EOF
|
|
124
|
+
# genuine semantic conflict — a human must decide which behaviour wins:
|
|
125
|
+
cat > "$AGENT_RESULT_FILE" <<'EOF'
|
|
126
|
+
{"status":"blocked","summary":"main and this branch both rewrote retry() incompatibly","question":"Should retries stay capped at 3 (main) or become unbounded (this PR)?"}
|
|
127
|
+
EOF
|
|
122
128
|
```
|
|
123
129
|
|
|
124
130
|
Write this file **once**, at the very end, with your final result. Keep it a flat
|
|
@@ -24,6 +24,7 @@ cancel can land anytime.
|
|
|
24
24
|
| `prNumber` | PR number |
|
|
25
25
|
| `round` | 1-based round counter |
|
|
26
26
|
| `answer` | present only when resuming from a human: an escalation reply, or merge-approval change requests|
|
|
27
|
+
| `adversarialFindings` | present only when a local adversarial reviewer critiqued your last push this round — see below |
|
|
27
28
|
| `prompt` | this document |
|
|
28
29
|
|
|
29
30
|
## Workspace (host mode) — read this first
|
|
@@ -79,6 +80,18 @@ Because several agents may run on the same host at once:
|
|
|
79
80
|
If `answer` is present, it is a human's instruction: either the decision on the
|
|
80
81
|
escalation you raised last round, or change requests from merge approval of this
|
|
81
82
|
converged PR. Act on it first, on this same PR branch (never open a new PR).
|
|
83
|
+
If `adversarialFindings` is present, you are **re-dispatched within the same round**:
|
|
84
|
+
a separate adversarial reviewer (a different agent, often a different model) read
|
|
85
|
+
the diff you just pushed and reported the findings below **before** Copilot is asked
|
|
86
|
+
to review it. Treat them exactly like review findings (step 2 triage, step 3
|
|
87
|
+
fix-the-class) — they have no GitHub threads, so there is nothing to reply to or
|
|
88
|
+
resolve. Push back on a wrong one by declining it in your `summary` with a one-line
|
|
89
|
+
rationale; fix the rest and push. Return `addressed` as usual (or `needs_input` if
|
|
90
|
+
one is a genuine design decision). This also applies on the **first round, before
|
|
91
|
+
any Copilot review has landed**: the findings are your backlog, so act on them and
|
|
92
|
+
return `addressed`, not `waiting`. The exception is when you decline **every**
|
|
93
|
+
finding and push nothing on that first round; then return `waiting`, because an
|
|
94
|
+
`addressed` round that does not advance the head is treated as no progress.
|
|
82
95
|
2. **Triage each item** into: *fix* (correct, worth doing), *nitpick* (apply
|
|
83
96
|
silently), *needs human input* (design/product/tradeoff you can't decide), or
|
|
84
97
|
*push back* (wrong / false positive — reply with evidence, make no change). Triage
|
|
@@ -90,13 +103,60 @@ Because several agents may run on the same host at once:
|
|
|
90
103
|
*nitpick*); otherwise **decline it explicitly with a one-line rationale in your
|
|
91
104
|
`summary`** (e.g. "declined suppressed advisory X — input already validated
|
|
92
105
|
upstream at Y"). Never silently drop one.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
106
|
+
Treat a **"Previously missed"** advisory as evidence that a whole *category* of issue
|
|
107
|
+
(e.g. unvalidated input, terminal-width overflow, an unhandled error path) went
|
|
108
|
+
unchecked in that area. Sweep **every file you touch** for that category this round,
|
|
109
|
+
rather than fixing only the cited line and waiting for the next review to cite the
|
|
110
|
+
neighbour.
|
|
111
|
+
3. **Act — fix the class, not the instance.** Each review round costs a full GitHub
|
|
112
|
+
re-review cycle, so a fix that closes only the cited occurrence just buys another
|
|
113
|
+
round when the reviewer finds its sibling. For every *fix*:
|
|
114
|
+
- **Name the bug class** the finding is an instance of (e.g. "fail-open detector:
|
|
115
|
+
an unrecognised shape is allowed through", "unsanitised text reaches the
|
|
116
|
+
terminal", "unbounded growth").
|
|
117
|
+
- **Sweep for the class.** Search the changed code and its siblings (same file,
|
|
118
|
+
same pattern, same helper's other call sites) for every other occurrence, and fix
|
|
119
|
+
them in this same commit. Prefer a structural fix that removes the whole class
|
|
120
|
+
(e.g. fail closed by default, one validating constructor) over patching the cited
|
|
121
|
+
case.
|
|
122
|
+
- **Test the class.** Add a red-first regression test per variant you found, not
|
|
123
|
+
only the one cited.
|
|
124
|
+
|
|
125
|
+
Make the code changes for all fixes + nitpicks in your workspace (`cwd`)
|
|
126
|
+
in one coherent, signed-off commit (`git commit -s`). If the branch has drifted
|
|
97
127
|
behind its base and you need to **rebase / resolve a merge conflict** to keep it
|
|
98
|
-
mergeable, that
|
|
99
|
-
|
|
128
|
+
mergeable, do that rebase **now, before validating** — never after the self-review
|
|
129
|
+
pass below. Then run the repo's build/test/lint locally.
|
|
130
|
+
|
|
131
|
+
**Before you push, do one adversarial self-review pass over your own diff.**
|
|
132
|
+
Diff against the PR's **current base**, not `origin/<head-branch>`: after a
|
|
133
|
+
permitted rebase, `origin/<head-branch>` still names the pre-rebase remote
|
|
134
|
+
history, so the three-dot merge base can fall back to the old base and pull
|
|
135
|
+
unrelated upstream changes into the pass. Resolve and fetch the base fresh,
|
|
136
|
+
then diff it against `HEAD` — correct before and after any rebase:
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
BASE=$(gh pr view --json baseRefName --jq .baseRefName) # the PR's current base branch
|
|
140
|
+
git fetch origin "$BASE"
|
|
141
|
+
git diff FETCH_HEAD...HEAD # the diff the reviewer actually sees
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Read it as the reviewer will: what input,
|
|
145
|
+
edge case, error path, or bypass would a hostile reviewer cite next? Fix what you
|
|
146
|
+
find in the same commit. **If this pass edits the diff, re-run the repo's
|
|
147
|
+
build/test/lint before pushing** — otherwise the self-review fix ships unvalidated,
|
|
148
|
+
since the checks above ran against the pre-self-review code. **The same rule covers
|
|
149
|
+
any change after the pass:** if you rebase, resolve a conflict, or otherwise alter
|
|
150
|
+
the diff after this point, re-run build/test/lint *and* redo this self-review before
|
|
151
|
+
pushing — a late rebase must never ship unvalidated. Do only **one** self-review
|
|
152
|
+
pass per state of the diff: it exists to catch the obvious next finding, not to
|
|
153
|
+
block the round.
|
|
154
|
+
|
|
155
|
+
Push to the PR's head branch (the branch
|
|
156
|
+
you are already on) — do not open a new branch or PR. A rebase/force-push
|
|
157
|
+
(`--force-with-lease`) is allowed when it is needed to keep the branch mergeable,
|
|
158
|
+
but only **before** the validation + self-review above (or followed by re-running
|
|
159
|
+
both). Any push this round — including a rebase/force-push with
|
|
100
160
|
no reviewer comments to act on — is an **`addressed`** round (see the return table).
|
|
101
161
|
4. **Reply in-thread** to each comment you addressed or pushed back on, one reply
|
|
102
162
|
per comment, so the trail lives on the PR.
|
|
@@ -142,24 +202,24 @@ Because several agents may run on the same host at once:
|
|
|
142
202
|
leading row summary before it (e.g. `nano-ack: a.json :: Adds X; A is wrong`). Example:
|
|
143
203
|
|
|
144
204
|
```sh
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
205
|
+
# Post the ack thread (pick any changed line in the diff for path/line). Use the PR's real HEAD
|
|
206
|
+
# SHA as commit_id — `git rev-parse HEAD` can drift from the PR head; ask GitHub:
|
|
207
|
+
CID=$(gh api repos/OWNER/REPO/pulls/PR --jq .head.sha)
|
|
208
|
+
# Build the body via a QUOTED heredoc so the verbatim advisory prose is never re-interpreted by
|
|
209
|
+
# the shell — a single-quoted `-f body='...'` breaks the moment the prose contains a `'` (e.g.
|
|
210
|
+
# "doesn't handle ..."), and a double-quoted one breaks on `$`/backticks. `<<'EOF'` (quoted
|
|
211
|
+
# delimiter) disables ALL expansion, so any advisory text is safe:
|
|
212
|
+
BODY=$(cat <<'EOF'
|
|
213
|
+
Applied. nano-ack: <path> :: <verbatim advisory text>
|
|
214
|
+
EOF
|
|
215
|
+
) # to DECLINE instead, build the body the same quoted-heredoc way (never a single-quoted
|
|
216
|
+
# `-f body='...'`, which breaks the moment the reason or advisory prose contains a `'`):
|
|
217
|
+
# BODY=$(cat <<'EOF'
|
|
218
|
+
# Declined, false positive — <reason>. nano-ack: <path> :: <verbatim advisory text>
|
|
219
|
+
# EOF
|
|
220
|
+
# )
|
|
221
|
+
gh api repos/OWNER/REPO/pulls/PR/comments -f commit_id="$CID" -f path="PATH" -F line=LINE -f side=RIGHT -f body="$BODY"
|
|
222
|
+
# Then resolve it exactly like any other thread (map its databaseId -> thread node id -> resolveReviewThread).
|
|
163
223
|
```
|
|
164
224
|
Only the `nano-ack: <path> :: <text>` (prose-keyed) form is honoured. A bare
|
|
165
225
|
`nano-ack: <path>:<line>` marker is **not** an acknowledgement: keyed only on
|
|
@@ -221,7 +281,8 @@ there is simply nothing to triage *yet*. In that case:
|
|
|
221
281
|
|
|
222
282
|
- Do **not** touch reviewer membership (see step 6) — the process's poller
|
|
223
283
|
solicits the review for you.
|
|
224
|
-
- Return **`waiting`** with a `summary` noting you are awaiting the review
|
|
284
|
+
- Return **`waiting`** with a `summary` noting you are awaiting the review
|
|
285
|
+
(unless `adversarialFindings` is present; then fix them and return `addressed`). The
|
|
225
286
|
process durably waits for the review to land (and has its own timeout that
|
|
226
287
|
escalates a genuinely stalled review for you).
|
|
227
288
|
|
|
@@ -258,9 +319,15 @@ default, and you waste a round. So emit a machine-readable result one of two way
|
|
|
258
319
|
round that needs a human decision:
|
|
259
320
|
|
|
260
321
|
```sh
|
|
261
|
-
|
|
322
|
+
cat > "$AGENT_RESULT_FILE" <<'EOF'
|
|
323
|
+
{"status":"needs_input","summary":"Resolved 3 nits; blocked on API shape","question":"Should getUser() throw or return null when the user is absent?"}
|
|
324
|
+
EOF
|
|
262
325
|
```
|
|
263
326
|
|
|
327
|
+
Use the **quoted heredoc** shown above (`<<'EOF'`), never a single-quoted
|
|
328
|
+
`printf '…'` string: your `summary` is free text, and its first apostrophe
|
|
329
|
+
("review 123's finding") ends a single-quoted string and breaks the command.
|
|
330
|
+
|
|
264
331
|
Write this file **once**, at the very end, with your final result. Keep it a flat
|
|
265
332
|
JSON object of exactly the variables in the table above.
|
|
266
333
|
|
|
@@ -115,13 +115,17 @@ your output is **not** parsed. Emit a machine-readable result one of two ways:
|
|
|
115
115
|
once, at the very end. Example (not blocked):
|
|
116
116
|
|
|
117
117
|
```sh
|
|
118
|
-
|
|
118
|
+
cat > "$AGENT_RESULT_FILE" <<'EOF'
|
|
119
|
+
{"scopeBlocked":false,"scopeBlockReason":""}
|
|
120
|
+
EOF
|
|
119
121
|
```
|
|
120
122
|
|
|
121
123
|
Blocked example:
|
|
122
124
|
|
|
123
125
|
```sh
|
|
124
|
-
|
|
126
|
+
cat > "$AGENT_RESULT_FILE" <<'EOF'
|
|
127
|
+
{"scopeBlocked":true,"scopeBlockReason":"#412 requires both the read AND write projection (acceptance criteria 2 + 3); this PR ships only the read side and defers the write projection with no filed tracker. File a follow-up issue for the write projection, link it (Follow-up: #N), and downgrade Closes #412 -> Part of #412 (close #412 by hand only when the write side lands)."}
|
|
128
|
+
EOF
|
|
125
129
|
```
|
|
126
130
|
|
|
127
131
|
2. **Fallback** (only if you cannot write the file): print a single last line to
|