You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit f9e63d6
Browse filesBrowse the repository at this point in the historyBrowse files
feat(fleet-write): pr_create carries labels in the create's stroke; os-dev declares skip-changeset there
The relay's `pr_create` op gains an optional `labels` list, the shape its
`assignees` leg already has: a later POST to the created pull's issue-labels
endpoint at the number the create answered, judged against `labelled` so a
name the answer lacks is a FAILED action, never a quiet partial. Both legs
now fill from the action's FIRST answer (the create's), never a later leg's.
The validator reuses the typed `labels` list `labels_add` takes.
`.claude/agents/os-dev.md`: three writes instead of four; `skip-changeset`
is declared in the `pr_create` stroke and `label-write.mjs` serves only a
label found after the PR exists; a classifier refusal routes through the
seat to the maintainer's word instead of the seat substituting.
Claude-Session: https://claude.ai/code/session_012PxNHU3dfov2hbpg9ycZ8q
Co-authored-by: Claude <noreply@anthropic.com>
return{ok: false,why: `HTTP ${status}, but the answer lists ${listed.length ? listed.join(', ') : 'no assignees'} — ${missing.join(', ')} not assigned (GitHub ignores a login the repository cannot assign without a word: it must be a collaborator the repository can assign)`};
247
254
}
248
255
}
256
+
// The labels leg (header): the answer is the pull's whole label set; a name sent and not listed did not land.
return{ok: false,why: `HTTP ${status}, but the answer lists ${listed.length ? listed.join(', ') : 'no labels'} — ${missing.join(', ')} not applied (the labels endpoint answers the pull's whole label set, so a name it lacks was not written; read the pull on the board before anything else)`};
* A descriptor carrying `fromAnswer` filled from the PREVIOUS request's answer
262
-
* (header): `{ req }` with the placeholder replaced by the positive integer
263
-
* that answer carries at the field, or `{ fail }` — the sentence the row
264
-
* prints — when no answer carries it. Pure; never throws.
281
+
* A descriptor carrying `fromAnswer` filled from its action's FIRST request's
282
+
* answer — the create's (header): `{ req }` with the placeholder replaced by
283
+
* the positive integer that answer carries at the field, or `{ fail }` — the
284
+
* sentence the row prints — when no answer carries it. Pure; never throws.
265
285
*/
266
286
exportfunctionfillFromAnswer(req,prev){
267
287
constfield=req.fromAnswer;
@@ -350,7 +370,8 @@ export function resultOf(req, json, status = null) {
350
370
if(!pr)return'ok';
351
371
returnpullStateText(pr);
352
372
}
353
-
if(Array.isArray(json))return`${json.length} label(s) now on the issue`;
373
+
// The labels leg's row prints the labels as the platform lists them (header); a plain labels_add is counted.
374
+
if(Array.isArray(json))returnArray.isArray(req.labelled) ? `${json.length} label(s) now on the pull · labels ${labelNames(json).join(', ')||'none'}` : `${json.length} label(s) now on the issue`;
'the GraphQL ops: the node id first, then the mutation; an errors array is a failure': 5,
625
646
'the PR-state landing: a 200 lands only when the answer SHOWS the state the op asked for — armed or queued for auto-merge, off for its disable, ready and draft for the flips': 12,
626
647
"the base sync: the PUT carries the expected head; a 202 is confirmed by polling the head off that sha inside the window and reading the merge commit's author; a 422 is a no-op only when the compare measures the head not behind its base; a moved head, a head still behind, an unreadable measure or an expired window is FAILED with nothing after it attempted": 10,
627
-
"the assignees leg: pr_create with assignees is the create, then ONE POST to the created pull's issue-assignees endpoint at the number the create ANSWERED — never the request; the answer must list every login sent; no assignees, no second request; an answer without a number stops the stroke with the leg not sent": 11,
648
+
"the assignees and labels legs: pr_create with assignees is the create, then ONE POST to the created pull's issue-assignees endpoint at the number the create ANSWERED — never the request; the answer must list every login sent; no assignees, no second request; an answer without a number stops the stroke with the leg not sent; with labels, ONE POST to the issue-labels endpoint at that same number — the create's, never the assignee answer's — whose answer must list every name sent": 19,
628
649
'the summary: request, sender and role, session, target, one row per request': 5,
629
650
'redaction: the token reaches no summary line, log line or error': 3,
630
651
'the wiring: both halves around every write verb, on the roster': 4,
@@ -890,7 +911,7 @@ export async function selfTest() {
890
911
}
891
912
892
913
// ── the assignees leg ───────────────────────────────────────────────────
893
-
battery("the assignees leg: pr_create with assignees is the create, then ONE POST to the created pull's issue-assignees endpoint at the number the create ANSWERED — never the request; the answer must list every login sent; no assignees, no second request; an answer without a number stops the stroke with the leg not sent");
914
+
battery("the assignees and labels legs: pr_create with assignees is the create, then ONE POST to the created pull's issue-assignees endpoint at the number the create ANSWERED — never the request; the answer must list every login sent; no assignees, no second request; an answer without a number stops the stroke with the leg not sent; with labels, ONE POST to the issue-labels endpoint at that same number — the create's, never the assignee answer's — whose answer must list every name sent");
@@ -916,6 +937,25 @@ export async function selfTest() {
916
937
constrefused=awaitrun(create(['marchtian']),{ ...allowed,[PULLS]: opened,[ASSIGN(22700)]: {status: 403,json: {message: 'Resource not accessible by integration'}}});
917
938
t("a refused assignee POST is a FAILED action with the platform's sentence, the pull already open",[refused.exit,refused.rows.map((r)=>r.status),refused.rows[1]?.result.includes('Resource not accessible by integration')],[EXIT_ACTION_FAILED,[201,403],true]);
918
939
t('no annotation is emitted for it — pr_create is not an annotated op, assignees or not',[ok.annotations,ok.logs.some((l)=>l.startsWith('::notice'))],[[],false]);
940
+
941
+
// The labels leg — the same shape, a dev's `skip-changeset` riding the create.
t('the table half: with labels the row carries a POST to the issue-labels path at the placeholder, fromAnswer number, the names as labelled — after the assignees leg when both are sent',both.map((r)=>[r.verb,r.path,r.fromAnswer??null,r.assigned??null,r.labelled??null]),[['POST',`/repos/${REPO}/pulls`,null,null,null],['POST',`/repos/${REPO}/issues/${answerPlaceholder('number')}/assignees`,'number',['marchtian'],null],['POST',`/repos/${REPO}/issues/${answerPlaceholder('number')}/labels`,'number',null,['skip-changeset']]]);
947
+
// The assignee answer here carries a DIFFERENT number: the label leg must still land on the pull the CREATE answered.
t("the run: the create, the assignee POST, then the label POST — at the number the CREATE answered, not the assignee answer's — exit 0",[labelled.exit,writes(labelled.seen).map((s)=>[s.call,s.body])],[EXIT_OK,[[PULLS,{title: 'T',head: 'claude/issue-1-x',base: 'main',body: 'B',draft: true}],[ASSIGN(22700),{assignees: ['marchtian']}],[LABEL(22700),{labels: ['skip-changeset']}]]]);
950
+
t("…and the leg's row prints the labels as the platform lists them — more than sent (the Auto Label workflow's additive writes) is a landing, never a failure",labelled.rows[2],{action: 1,op: 'pr_create',call: LABEL(22700),status: 200,result: '2 label(s) now on the pull · labels skip-changeset, documentation'});
t('labels without assignees: the create, then the label POST alone',[labelsOnly.exit,writes(labelsOnly.seen).map((s)=>s.call)],[EXIT_OK,[PULLS,LABEL(22700)]]);
t('⛔ a 200 whose answer lacks a name sent is a FAILED action naming it — never a quiet partial — and the stroke stops there',[unlabelled.exit,unlabelled.rows[1]?.result.includes('no-such-label not applied'),unlabelled.rows[1]?.result.includes('lists skip-changeset'),writes(unlabelled.seen).length,unlabelled.summary.includes('1 later action(s) NOT attempted')],[EXIT_ACTION_FAILED,true,true,2,true],unlabelled.rows[1]?.result);
955
+
t('…a name is matched as GitHub matches one, case-insensitively',requestLanded({verb: 'POST',path: '/x',labelled: ['Skip-Changeset']},{status: 200,json: [{name: 'skip-changeset'}]}).ok,true);
956
+
t("…and a plain labels_add answer is counted, not judged — labelled is the leg's alone",[requestLanded({verb: 'POST',path: '/x'},{status: 200,json: []}).ok,resultOf({verb: 'POST',path: '/x'},[{name: 'a'}],200)],[true,'1 label(s) now on the issue']);
t('a create whose answer carries no number: the label leg is NOT SENT, exit 5, the later action never attempted',[blankLabel.exit,blankLabel.rows.map((r)=>r.status),writes(blankLabel.seen).map((s)=>s.call)],[EXIT_ACTION_FAILED,[201,'not sent'],[PULLS]]);
919
959
}
920
960
921
961
// ── the transfer ────────────────────────────────────────────────────────
0 commit comments