jinaga-server 3.7.5 → 3.8.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.
Files changed (38) hide show
  1. package/.claude/skills/degrees-of-freedom/.openskills.json +7 -0
  2. package/.claude/skills/degrees-of-freedom/SKILL.md +18 -0
  3. package/.claude/skills/degrees-of-freedom/degrees-of-freedom-constitution.md +142 -0
  4. package/.claude/skills/night-shift-setup/.openskills.json +7 -0
  5. package/.claude/skills/night-shift-setup/SKILL.md +95 -0
  6. package/.claude/skills/night-shift-setup/configuring-a-repository.md +111 -0
  7. package/.claude/skills/night-shift-setup/creating-the-log.md +59 -0
  8. package/.claude/skills/night-shift-setup/creating-the-routines.md +80 -0
  9. package/.claude/skills/night-shift-setup/log-model.factual +971 -0
  10. package/.claude/skills/night-shift-worker/.openskills.json +7 -0
  11. package/.claude/skills/night-shift-worker/SKILL.md +199 -0
  12. package/.claude/skills/night-shift-worker/night-shift-log.md +72 -0
  13. package/.claude/skills/night-shift-worker/scripts/register-stack.sh +173 -0
  14. package/.claude/skills/night-shift-worker/stacking.md +57 -0
  15. package/.claude/skills/night-shift-worker/verifying-a-merged-fix.md +16 -0
  16. package/.claude/skills/refining-issues/.openskills.json +7 -0
  17. package/.claude/skills/refining-issues/SKILL.md +327 -0
  18. package/.claude/skills/refining-issues/references/worked-example.md +234 -0
  19. package/.github/workflows/main.yml +2 -2
  20. package/.night-shift/config.md +84 -0
  21. package/.night-shift/skills.md +17 -0
  22. package/README.md +21 -0
  23. package/dist/http/router.js +19 -23
  24. package/dist/http/router.js.map +1 -1
  25. package/dist/http/stream.d.ts +0 -1
  26. package/dist/http/stream.js +6 -17
  27. package/dist/http/stream.js.map +1 -1
  28. package/dist/jinaga-server.js +74 -7
  29. package/dist/jinaga-server.js.map +1 -1
  30. package/integration-test/jinaga-test/package-lock.json +4 -4
  31. package/integration-test/jinaga-test/package.json +1 -1
  32. package/package.json +2 -2
  33. package/src/http/router.ts +31 -39
  34. package/src/http/stream.ts +21 -36
  35. package/src/jinaga-server.ts +92 -4
  36. package/test/http/streamLogVolumeSpec.ts +284 -0
  37. package/test/jinaga-server/poolConfigurationSpec.ts +147 -0
  38. package/test/jinaga-server/tracePoolSpec.ts +368 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "source": "factoryengineering/skills",
3
+ "sourceType": "git",
4
+ "repoUrl": "https://github.com/factoryengineering/skills",
5
+ "subpath": "skills/night-shift-worker",
6
+ "installedAt": "2026-10-15T02:59:05.088Z"
7
+ }
@@ -0,0 +1,199 @@
1
+ ---
2
+ name: night-shift-worker
3
+ description: Works one GitHub issue through to a stacked pull request, unattended. Reproduces the defect or verifies a fix that already merged, fixes with a regression test, resolves a base branch that may not exist yet, registers the chain as a GitHub stack, drives CI green through one review round, then stops. Use when a scheduled routine or a person hands over a single issue number to work, when a merged pull request already exists and the job is verification rather than repair, when a fix must stack on a lower layer's branch, or when a blocker should become a recorded question instead of a guessed patch.
4
+ ---
5
+
6
+ # Night Shift Worker
7
+
8
+ One dispatch, one issue, one pull request. This skill is the whole protocol for that unit of work, so an unattended run needs nothing from whoever dispatched it beyond an issue number.
9
+
10
+ Sweeping the queue, deciding what is available, and ordering issues into chains belong to `night-shift-coordinator`. Standing the system up belongs to `night-shift-setup`.
11
+
12
+ Your dispatch names an **issue**, and optionally a lower issue to stack on. If it names no issue, stop and say so.
13
+
14
+ ## When to use
15
+
16
+ - **A dispatch names one issue** in this repository, and optionally a lower issue to stack on.
17
+ - **A merged pull request already covers the issue**, so the job is verification rather than repair.
18
+ - **A blocker cannot be settled** from the code, the tests, or the issue text.
19
+ - **A pull request is open and waiting** on CI or on a review round.
20
+
21
+ ---
22
+
23
+ ## What these instructions serve
24
+
25
+ Two purposes stand behind every rule in this skill and behind the dispatch that sent you.
26
+
27
+ **Progress.** The run exists to move the repository forward. Ambiguity that has an obvious resolution is not a reason to stall.
28
+
29
+ **Truthfulness.** What a run writes is checked against the real, current artifacts rather than assumed from prior wording. Read the released package, the merged diff, or the running system.
30
+
31
+ **Where an instruction's literal wording would violate the purpose it was written to serve, follow the purpose.** Then say so plainly in the pull request, under its own heading, as a factual report rather than an apology. A deviation you reported is a finding the maintainer needs. A deviation you buried is a defect.
32
+
33
+ ---
34
+
35
+ ## Before you start
36
+
37
+ Read `.night-shift/config.md` at the repository root. It carries everything about this repository that the protocol does not fix: what to read first, how to reproduce, which commands must be green, which workflow gates the merge, and what happens after the pull request opens.
38
+
39
+ **A missing file, or a missing required heading, is a stop.** Say which heading you could not find and open no pull request. Do not substitute a default. `configuring-a-repository.md` in the `night-shift-setup` skill lists the required headings.
40
+
41
+ **An open pull request on your issue does not mean a session is working it.** A worker that finished its round stopped with its pull request open, exactly as *Monitor the pull request, then stop* directs, and it now waits on a human to merge. Reading that as a live session is what strands an issue the first attempt left unfinished: the artifact of the partial work becomes the thing that blocks the rest of it.
42
+
43
+ The cost the check exists to prevent is **two sessions pushing different fixes for one issue**, and a re-fire, a manual dispatch, or a second sweep before the first one's work merges all arrive here looking exactly like a fresh assignment. So test whether a session is **live**, not whether a pull request exists. A live worker touches its pull request — it pushes, it comments, its checks run — and a stopped one cannot. Read the pull request once, and take one of three branches.
44
+
45
+ | What the pull request shows | What it means | What to do |
46
+ |---|---|---|
47
+ | A check run in progress, or a push, comment or review inside the last six hours | A session is live | Comment `Duplicate dispatch, no-op.` on the pull request and stop. |
48
+ | None of those for six hours, and some of the issue's acceptance is unmet | A session stopped before the issue was done | **Resume**, below. |
49
+ | None of those for six hours, and the pull request covers the acceptance | The work is done and a human has yet to merge | **Stop.** Leave no comment. |
50
+
51
+ Six hours is several times the longest wait this protocol asks a worker to hold, the 90 minutes in `stacking.md`. It is still a guess about a session you cannot see, so err long: too short puts two sessions on one branch, and too long costs one sweep.
52
+
53
+ **Check the acceptance clause by clause against the diff on the branch**, not against the pull request body, which says what its author set out to do. What counts as met is what the config's `## Before you fix` says counts. One unmet clause is enough to resume.
54
+
55
+ **Resume only a branch matching `claude/issue-<number>-*`.** That pattern is a worker's own branch for this issue and nothing else. A pull request that reaches you by a closing keyword on some other branch is someone else's work, whatever it says about your issue: treat it as live, no-op, and never push to it.
56
+
57
+ **To resume, push to the branch and the pull request that already exist.** First comment on the pull request naming the clauses you found unmet, so a session you have wrongly read as stopped can stand down. Then check that branch out as it stands and work the rest of this protocol against it — the regression test, the verification commands, one review round — fixing only what those clauses need. Opening a second pull request is what this check exists to prevent, and one branch and one pull request per issue still holds.
58
+
59
+ **Comment the no-op once, not once a sweep.** An issue re-dispatched against a long-lived pull request collects an identical comment on every fire, and they bury the thread a maintainer came to read. If `Duplicate dispatch, no-op.` is already on the pull request, stop without adding another.
60
+
61
+ Then copy this checklist and work it:
62
+
63
+ ```
64
+ Task Progress:
65
+ - [ ] Read .night-shift/config.md and the files its `## Read first` names
66
+ - [ ] Check an open pull request on this issue, and no-op, resume or stop
67
+ - [ ] Open the Night Shift Log and resolve this repository
68
+ - [ ] Establish the work is warranted, per the config's `## Before you fix`
69
+ - [ ] Fix with a regression test that fails before, passes after, and can fail for a reason other than itself
70
+ - [ ] Run every command under `## Verification commands` green
71
+ - [ ] Resolve the base branch, push, open the pull request
72
+ - [ ] Register the stack, or record that registration is unavailable
73
+ - [ ] Confirm CI ran on this layer, by its `event`
74
+ - [ ] One review round, then stop
75
+ - [ ] Record the outcome, whatever it was
76
+ - [ ] Close the Night Shift Log console, before anything that can stop the run
77
+ - [ ] If the log was unreachable, send a push notification before stopping
78
+ - [ ] Run the config's `## After the pull request`
79
+ ```
80
+
81
+ The container may hand you a working tree whose `origin/*` refs are older than the tree itself. Run `git fetch origin` before you read any remote ref, and never conclude that a branch or a file is missing from a ref you have not just fetched.
82
+
83
+ ---
84
+
85
+ ## Work the issue
86
+
87
+ **Establish that the work is warranted, exactly as `## Before you fix` in the config directs.** That heading is where a repository says what counts, and repositories differ. One asks you to reproduce a defect. Another has no defect to reproduce, because its issues are slices of an accepted specification, and points you at a spec section and the issue's conformance criteria instead. Do not assume the first: where a repository says there is nothing to reproduce, a missing repro is not a finding.
88
+
89
+ Where the config does ask for a repro and it fails, that is a finding rather than a failure. An issue's repro may have been reconstructed rather than verified by its reporter. Report it and open no pull request.
90
+
91
+ Then fix, with a regression test that fails before and passes after. Keep the change minimal. Record anything you notice beyond the issue's scope as a note in the pull request rather than widening the diff.
92
+
93
+ **A test earns its place by being able to fail for a reason other than someone editing the very thing it quotes.** Where the fix changes behavior, the regression test is that proof, it fails before and passes after, and it is not optional.
94
+
95
+ Where the fix changes only prose — a doc comment, a page, a README — a test that matches a sentence against itself proves nothing. The assertion is a second copy of its own subject, so the only edit that turns it red is a deliberate edit to that sentence. It makes the wording harder to improve and establishes nothing about whether the code is right. The same holds for an assertion that a default equals the literal the source assigns it, or that a config file contains a line it contains.
96
+
97
+ Derive one side instead, so both sides can move and the check still has bite: compile a documented example against the shipped build, resolve a documented path against the filesystem, read a default back out of the object the factory built. Each of those fails when the *code* changes, which is the failure a document cannot produce on its own. Where no derivation is available, say so in the pull request and let the change stand on review. A missing test you named is a finding the maintainer needs; a copy you wrote to fill the checkbox is a maintenance cost they did not ask for.
98
+
99
+ **Where the issue's own acceptance asks for the copy, that is a question, not an instruction to follow.** Record it per `## When to stop and ask instead`, because writing the test the issue asked for is what puts it in the suite.
100
+
101
+ Run every command under `## Verification commands` green before every push, in the order the config lists them.
102
+
103
+ When a merged pull request already covers the issue, your job changes from fix to verify. Read **[verifying-a-merged-fix.md](verifying-a-merged-fix.md)**.
104
+
105
+ ---
106
+
107
+ ## Open the pull request, stacked
108
+
109
+ Branch name: `claude/issue-<number>-<slug>`, unless the config's optional `## Branch prefix` says otherwise. The session harness may assign a branch of its own. Create this one instead and push only this one, because stacking and the claim check find a worker's branch by its issue number and a harness name carries none. Following this name is the protocol, so the pull request does not report it as a deviation.
110
+
111
+ With no stacking clause, branch from `origin/main` after fetching. When the dispatch names a lower issue, branch from **that issue's branch** and set it as your pull request base. Read **[stacking.md](stacking.md)** for how to resolve a branch that does not exist yet, how to register the chain, and how to confirm CI actually ran on your layer.
112
+
113
+ **Never fall back to `main`** when a lower layer's branch is absent. Branching from `main` while that work is missing renders it as deletions in your diff, which reads as a revert and passes review by looking small.
114
+
115
+ ---
116
+
117
+ ## Write what stays true
118
+
119
+ A pull request body is read long after it is written, and other pull requests merge in between. **Do not write a value into prose that another change can silently falsify.** A number that was accurate when you wrote it goes stale with nothing to detect it, and a reader has no way to tell which of your sentences still hold.
120
+
121
+ - **Report verification as what ran and what held.** Name the commands the config lists under `## Verification commands` and say they ran green from a clean checkout. Copy them from the config rather than from memory, because the set differs by repository and a remembered one misreports what ran. A total or a pass ratio is not verification.
122
+ - **Name a test by its description**, which survives a test inserted above it. Never cite a test by its position.
123
+ - **Do not state test totals or before-and-after counts.** The diff already shows what you added, and it cannot go stale.
124
+ - **Where a number genuinely carries the argument, pin it to what it measured**: a commit, a CI run id. A rate measured in one named run stays true of that run.
125
+
126
+ This holds for everything the run writes: the pull request body, a comment on the issue, a recorded question, and the log summary.
127
+
128
+ ---
129
+
130
+ ## When to stop and ask instead
131
+
132
+ Record a blocking question when proceeding either way could produce the wrong patch and you cannot settle it from the code, the tests, or the issue text. A question about a detail you can work around is not blocking, so do everything that does not depend on the answer first.
133
+
134
+ Record one **also** at the far end of a run, when the work is finished and the issue still is not: the shape does not reproduce anywhere you can reach, and settling it needs a capture from a live system, a reproduction repo, or an answer from the reporter. Left in the queue, an issue like that draws a fresh claim check every night and re-derives a verification that is already complete.
135
+
136
+ **A gap and a stale instruction are not the same thing, and only one of them is a question.**
137
+
138
+ A **genuine gap** is something nobody has decided yet. The issue does not say, the code does not settle it, and either choice could be the wrong patch. Raise it here and do not resolve it alone.
139
+
140
+ A **stale instruction** is something already decided, where the ground has since moved. A release now ships what the wording assumed absent, a dependency merged, or a measured fact changed. That is not a gap, and it does not need a maintainer to decide it again. Carry the decision out against current reality, and report the deviation from the literal wording in the pull request.
141
+
142
+ What separates them is what you would be deciding. Reading a settled decision against today's artifacts is your job. Changing what was decided is the maintainer's, and where the config forbids a change outright, that prohibition holds and the question is the only move.
143
+
144
+ To record a question:
145
+
146
+ 1. Comment on the issue. State what you found, why it blocks you, the candidate answers, and what you would do under each. Make it answerable in one reply. If a pull request already exists, post it there too and link it from the issue comment.
147
+ 2. Remove the queue label and add `question`.
148
+ 3. Stop. Do not guess and push a speculative fix.
149
+
150
+ The label swap moves the issue out of the queue, so the next sweep will not pick it up again while it waits on an answer.
151
+
152
+ ---
153
+
154
+ ## Monitor the pull request, then stop
155
+
156
+ 1. Subscribe to its activity.
157
+ 2. Request a review.
158
+ 3. Drive CI to green. A red check on your own pull request is work now, at every wake: diagnose, fix, push. If a failure is genuinely not yours, meaning it is red on the base branch too, say so in one comment rather than going silent.
159
+ 4. Complete **one round** with the reviewer. Address every suggestion with a pushed commit, or reply on the thread explaining why it is wrong or out of scope. Resolve the threads you addressed.
160
+
161
+ A review comment is a claim, not a verdict. Verify it against the repository before you act on it, and check that your `origin/*` refs are current before you agree that something a reviewer named is missing. Reply with what you found either way.
162
+
163
+ **Stop when CI is green on the current head and that one review round is complete**, either because the reviewer left no suggested changes or because you have addressed all of them. Then unsubscribe. Do not cycle into further rounds. Until both conditions hold, schedule a check-in before ending a turn, and re-arm it each time.
164
+
165
+ **A resume comment means stand down.** A later dispatch that reads your pull request as stopped says so on it, naming the acceptance it found unmet. One posted after your last push means that session now owns the branch: reply once that you are standing down, unsubscribe, and stop. Two sessions pushing to one branch is the failure the duplicate check exists to prevent, and yielding to the newer one always terminates, because it only resumed after you had gone quiet.
166
+
167
+ **Check-run events name a stale head.** An event can arrive for a commit the pull request has already moved past, so acting on the SHA in the event can declare green on a commit that is no longer current. Always re-read the pull request's own head before concluding anything about its state, and treat the event as a nudge to look rather than as a report of what is true.
168
+
169
+ ---
170
+
171
+ ## Record the run
172
+
173
+ Every outcome goes in the Night Shift Log, including "nothing to fix", and **every run closes the console it opened**, including one that stopped before it got here. Read **[night-shift-log.md](night-shift-log.md)**.
174
+
175
+ Only then run the config's `## After the pull request`. A value of `none` means there is nothing to do; an absent heading means stop. **The close comes first because that stop is real**: a run that reaches an absent heading ends there, and a console it has not closed by then is one it never closes.
176
+
177
+ ---
178
+
179
+ ## Hard limits
180
+
181
+ - Never push to `main`, and never push to a branch a live session holds. Resuming a stopped session's branch, under *Before you start*, is the only exception.
182
+ - Never merge a pull request.
183
+ - Never close an issue, and never remove the queue label except as part of the question swap.
184
+ - Never skip, disable, or quarantine a test to get a green build.
185
+ - Never push an empty commit, and never close and reopen a pull request, to re-trigger CI.
186
+ - Never fall back to `main` as a base when a lower layer's branch is absent.
187
+ - Never dispatch another issue's worker. You are one layer.
188
+ - Never put a mutating call in a retry or fallback position. A shell `cmd-a || cmd-b` runs `cmd-b` when `cmd-a` merely prints something unexpected, and a "test" invocation of a create endpoint is a real write.
189
+ - End every comment you leave on an issue or a pull request with your agent attribution footer.
190
+ - Do not put model identifiers in commit messages, pull request text, or code comments.
191
+
192
+ ## Resources
193
+
194
+ | Resource | Purpose |
195
+ |---|---|
196
+ | [stacking.md](stacking.md) | Resolving a base branch, registering the chain, confirming a run by its `event` |
197
+ | [verifying-a-merged-fix.md](verifying-a-merged-fix.md) | What to do when a fix already landed |
198
+ | [night-shift-log.md](night-shift-log.md) | Recording the outcome of a dispatch |
199
+ | [scripts/register-stack.sh](scripts/register-stack.sh) | Execute. Registers a chain of pull requests as a GitHub stack |
@@ -0,0 +1,72 @@
1
+ # Recording a dispatch in the Night Shift Log
2
+
3
+ A run that leaves no trace teaches nothing. The host keeps the artifacts — a pull request, a comment, a label — but not the judgments, and those die with the container unless you record them.
4
+
5
+ The log is a Jinaga application reached through the Factual MCP server. Open a console, run `applications`, and open the one whose routing matches. That console is yours to close, and *Close the console, on every exit path* below says when and why. **Its manifest carries the full action catalog with argument guidance, so run `describe <action>` there rather than trusting an argument list in this file.**
6
+
7
+ ## The whole shape, for orientation
8
+
9
+ The coordinator performs steps 1 to 5, before your session exists. You perform 6, and 7 when it later applies.
10
+
11
+ 1. `practicesForAdministrator` — resolve the practice and this repository. *(coordinator)*
12
+ 2. `startSweep` — once per repository per sweep. *(coordinator)*
13
+ 3. `correctedVerdicts` and the previous `considerationsInSweep`. *(coordinator)*
14
+ 4. `considerIssue`, then exactly one finding action. *(coordinator)*
15
+ 5. `dispatchWork` or `skipIssue`. *(coordinator)*
16
+ 6. **`openPullRequest`, `raiseQuestion`, or `findNoChange`** — exactly one, when your dispatch finishes. *(you)*
17
+ 7. **`answerQuestion`, or `correctVerdictFromWork`** naming the consideration whose work produced the disproof. *(you, later)*
18
+
19
+ ## Finding your dispatch
20
+
21
+ Your outcome attaches to the `Dispatch` the coordinator created for your issue. You were not given its reference, so resolve it:
22
+
23
+ 1. `practicesForAdministrator($me)`, and take the `repositoryRef` whose current name matches this repository.
24
+ 2. `sweepsInRepository($repository)`, and take the most recent sweep.
25
+ 3. `considerationsInSweep($sweep)`, and find the row whose issue number is yours. Its `dispatches` entry carries the `dispatchRef` you need.
26
+
27
+ The `branch` on that dispatch is a **glob**, not a branch name — the coordinator writes `claude/issue-<N>-*` because the slug is yours to choose. It is not the branch you pushed, and you should not try to reconcile it.
28
+
29
+ **The log does not record your actual branch anywhere.** `PullRequestOpened` carries the number, the summary and the time, and no branch. What ties this dispatch to the work that came of it is the pull request number. Do not reach for `findBranch` to fill the gap: that action means *the claim check found a branch that already existed*, so recording your own branch through it would make the next sweep read your own work as prior work and skip the issue.
30
+
31
+ ## Two console forms that cost a retry each
32
+
33
+ **A `call` yields a frame of named bindings, so it cannot be bound to one name.** Destructure what you need:
34
+
35
+ ```
36
+ let { $pullRequest as $pr } = call openPullRequest($dispatch, 123, "Bounded the retry so a stalled feed fails instead of hanging.")
37
+ ```
38
+
39
+ `let $pr = call openPullRequest(...)` is a parse error, not a runtime one, so it takes the whole batch with it.
40
+
41
+ **Every variable carries `$`,** including the bound name and a specification's own variables. One statement per line; `;` only joins two on one line.
42
+
43
+ ## Close the console, on every exit path
44
+
45
+ **`close_console` is the last log step of every run, including a run that stops early.** The server caps one identity at 16 open consoles, and every routine in a practice runs as that same identity, so each run that leaves its console open takes a slot the next one needs. **The pool does heal on its own, and far too slowly to lean on.** A console that nothing touches for a full recovery window — seven days, as the server's own `list_consoles` description states — stops counting against the cap. It does not disappear when it does: `list_consoles` still reports it, marked `expired`, and its working copy survives. Consoles idle for five days have been seen still holding their slots. A practice that dispatches a few issues a night reaches 16 long before its oldest leak expires, so recovery never arrives in time to help, and every run from then on works through the reuse path below instead of opening a console of its own.
46
+
47
+ **The paths that leak are the ones that stop early**, because the close sits at the bottom of a procedure they never reach. A stop for a missing config heading or a missing file, a duplicate dispatch that no-ops, a repro that failed, a recorded question, a fix abandoned on a blocker. Close the console on each of them.
48
+
49
+ **A refused `create_console` is a full pool, not an unreachable server.** `console_limit` comes back with a `candidates` list, and the run continues on one of them: take a candidate whose `dirty` is false and whose `stagedFacts` is `0`, and work in that. **Never work in a dirty candidate, and never close a dirty one.** Its staged facts belong to a session that is not yours, and closing it discards them. The clean candidate you do take is yours for the run and you close it at the end, exactly as you would close one you had opened yourself.
50
+
51
+ **If every candidate is dirty the pool is genuinely exhausted, and that is still not an outage.** Do the hosting-platform work, and report the run unrecorded naming a full console pool as the reason. The two read alike in a report and have different remedies: a full pool was caused by earlier runs that did not close their consoles, and an outage belongs to whoever owns the connector.
52
+
53
+ ## When the log is unreachable
54
+
55
+ **Unreachable means the server cannot be reached at all**, and it has exactly two shapes: its tools are absent from the session, or its connector reports that it needs authorization. A `console_limit` refusal is neither, because the server answered; a full pool is the section above, and so is a pool in which every candidate is dirty.
56
+
57
+ **Do the hosting-platform work anyway.** A missing log entry is a gap; a blocked run is a worse one.
58
+
59
+ **Then raise it where a person will see it the same day.** Send a push notification as well as saying it in your final report. A scheduled routine's final report lives in a session transcript that nobody reads by default, which is how a connector that had lost its authorization cost two consecutive unrecorded nights before anyone noticed. The notification names the repository, this session, and that the run is unrecorded.
60
+
61
+ **Send it before you stop.** Your outcome attaches to a `Dispatch` that may itself be unrecorded, in which case there is nothing to attach to and no trace that this dispatch produced anything. Name the pull request you opened, the question you recorded, or the no-change you found, so the artifact can be found without the log.
62
+
63
+ ## Four rules about what goes in
64
+
65
+ - **Never record availability.** The hosting platform is the queue and the only authority on what is currently ready. The log holds what was observed and decided, and when. Storing "this issue is available" would create a second source of truth that can go stale, which is the exact failure the claim rule exists to catch.
66
+ - **Write a rationale you can support.** A rationale is your own account, so quote a rule only after reading it, and name the file it comes from. A confident paraphrase of a rule that does not exist reads as evidence to every later run.
67
+ - **Summarize the change, not its evidence.** The `openPullRequest` summary says what changed and why, as the example above does. Restating the pull request's evidence makes a second copy that drifts from the first, and one run's pull request body and log summary disagreed on a single count before anyone read either. Anything evidential that does appear follows *Write what stays true* in `SKILL.md`.
68
+ - **If the server is unreachable, do the hosting-platform work anyway**, and raise the outage the same day rather than only in your final report. *When the log is unreachable* above says what counts as unreachable and what raising it takes.
69
+
70
+ ## Nothing to fix is a result
71
+
72
+ `findNoChange` exists because a dispatch that concludes there is nothing to fix has produced a real finding. Record it rather than leaving the dispatch open, and never manufacture a change to avoid using it.
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Register a chain of pull requests as a GitHub stack, or append to an existing one.
4
+ #
5
+ # See SKILL.md (night-shift-worker skill), and stacking.md, for when to run this
6
+ # and what to do with each outcome.
7
+ #
8
+ # There is no MCP tool for the Stacks API, which is why this script exists: it
9
+ # gives an automated session one narrow, allowlistable entry point instead of a
10
+ # general-purpose HTTP client. It talks to the API over curl, so it works in a
11
+ # container where `gh` is absent.
12
+ #
13
+ # Usage:
14
+ # register-stack.sh list
15
+ # register-stack.sh create <pr> <pr> [<pr> ...] # bottom to top, min 2
16
+ # register-stack.sh add <stack_number> <pr> [...] # append above current top
17
+ #
18
+ # Exit status:
19
+ # 0 success
20
+ # 1 usage or precondition error (bad argument, no token, no repository,
21
+ # or a stack number that does not exist)
22
+ # 3 stacked pull requests are not enabled for the repository
23
+ #
24
+ # Requires `curl` and `git`, both of which are present in a standard Claude Code
25
+ # container. Nothing else is needed, and `gh` in particular is not.
26
+ #
27
+ # The repository is read from `git remote get-url origin`. Override it with
28
+ # STACK_REPO=owner/name when the remote is not the repository you mean, such as
29
+ # in a fork or a mirror.
30
+ #
31
+ # Requires GH_TOKEN or GITHUB_TOKEN with write access to pull requests. That is
32
+ # "Pull requests: Read and write" on a fine-grained token, `pull-requests: write`
33
+ # in an Actions workflow, or the `repo` scope on a classic token. The API reports
34
+ # the requirement itself as `X-Accepted-Github-Permissions: pull_requests=write`.
35
+
36
+ set -euo pipefail
37
+
38
+ die() { printf 'register-stack: %s\n' "$1" >&2; exit 1; }
39
+
40
+ # Sets REPO in the calling shell rather than printing it. `exit` inside a
41
+ # command substitution ends only the subshell, so a `die` in a substituted
42
+ # function cannot stop the script. Assigning here keeps every failure fatal.
43
+ derive_repo() {
44
+ local url
45
+ git rev-parse --show-toplevel >/dev/null 2>&1 \
46
+ || die "not inside a git working tree; run from the repository root, or set STACK_REPO=owner/name"
47
+ url="$(git remote get-url origin 2>/dev/null)" \
48
+ || die "no 'origin' remote; set STACK_REPO=owner/name"
49
+ url="${url%.git}"
50
+ case "$url" in
51
+ git@github.com:*) url="${url#git@github.com:}" ;;
52
+ ssh://git@github.com/*) url="${url#ssh://git@github.com/}" ;;
53
+ https://github.com/*) url="${url#https://github.com/}" ;;
54
+ http://github.com/*) url="${url#http://github.com/}" ;;
55
+ *) die "origin '$url' is not a github.com remote; set STACK_REPO=owner/name" ;;
56
+ esac
57
+ case "$url" in
58
+ */*/*|/*|*/) die "cannot read owner/name from origin '$url'; set STACK_REPO=owner/name" ;;
59
+ */*) REPO="$url" ;;
60
+ *) die "cannot read owner/name from origin '$url'; set STACK_REPO=owner/name" ;;
61
+ esac
62
+ }
63
+
64
+ if [ -n "${STACK_REPO:-}" ]; then
65
+ REPO="$STACK_REPO"
66
+ else
67
+ REPO=""
68
+ derive_repo
69
+ fi
70
+
71
+ API="https://api.github.com/repos/${REPO}"
72
+ TOKEN="${GH_TOKEN:-${GITHUB_TOKEN:-}}"
73
+
74
+ [ -n "$TOKEN" ] || die "no GH_TOKEN or GITHUB_TOKEN in the environment"
75
+
76
+ # Only ever accept bare positive integers. This keeps the allowlisted invocation
77
+ # from being widened by clever arguments. Validation must run in the calling
78
+ # shell, never inside a command substitution: `exit` in a substitution ends only
79
+ # the subshell, which would let a rejected argument through to the API.
80
+ # Pull request and stack numbers are always 1 or greater, so reject 0 and any
81
+ # leading-zero form here rather than letting the API reject them for us.
82
+ require_number() {
83
+ case "$1" in
84
+ ''|*[!0-9]*|0*) die "expected a positive integer, got '$1'" ;;
85
+ *) : ;;
86
+ esac
87
+ }
88
+
89
+ require_numbers() {
90
+ local n
91
+ for n in "$@"; do require_number "$n"; done
92
+ }
93
+
94
+ # Build a JSON array from already-validated positional arguments.
95
+ json_numbers() {
96
+ local out="" n
97
+ for n in "$@"; do out="${out:+$out,}$n"; done
98
+ printf '[%s]' "$out"
99
+ }
100
+
101
+ call() {
102
+ local method="$1" path="$2" body="${3:-}"
103
+ local args=(-sS -X "$method"
104
+ -H "Authorization: Bearer ${TOKEN}"
105
+ -H "Accept: application/vnd.github+json"
106
+ -w '\n%{http_code}')
107
+ [ -n "$body" ] && args+=(-H "Content-Type: application/json" -d "$body")
108
+ curl "${args[@]}" "${API}${path}"
109
+ }
110
+
111
+ # A 404 is its own outcome, not a generic failure. Stacked pull requests are in
112
+ # public preview and are enabled per repository, so a caller has to be able to
113
+ # tell "this repository cannot do stacks" from "this call was wrong".
114
+ report() {
115
+ local response="$1" expected="$2" not_found="$3"
116
+ local code="${response##*$'\n'}"
117
+ local payload="${response%$'\n'*}"
118
+ printf '%s\n' "$payload"
119
+ case " $expected " in
120
+ *" $code "*) printf 'HTTP %s\n' "$code" >&2; return 0 ;;
121
+ esac
122
+ if [ "$code" = "404" ]; then
123
+ printf 'HTTP 404\n' >&2
124
+ printf 'register-stack: %s\n' "$not_found" >&2
125
+ exit 3
126
+ fi
127
+ printf 'HTTP %s (unexpected)\n' "$code" >&2
128
+ exit 1
129
+ }
130
+
131
+ NOT_ENABLED="stacked pull requests are not enabled for ${REPO}"
132
+
133
+ cmd="${1:-}"
134
+ [ -n "$cmd" ] || die "usage: register-stack.sh {list|create|add} ..."
135
+ shift || true
136
+
137
+ case "$cmd" in
138
+ list)
139
+ report "$(call GET /stacks)" "200" "$NOT_ENABLED"
140
+ ;;
141
+ create)
142
+ [ "$#" -ge 2 ] || die "create needs at least 2 pull request numbers, bottom to top"
143
+ require_numbers "$@"
144
+ report "$(call POST /stacks "{\"pull_requests\":$(json_numbers "$@")}")" "201" "$NOT_ENABLED"
145
+ ;;
146
+ add)
147
+ [ "$#" -ge 2 ] || die "add needs a stack number and at least 1 pull request number"
148
+ stack="$1"; shift
149
+ require_number "$stack"
150
+ require_numbers "$@"
151
+ add_response="$(call POST "/stacks/${stack}/add" "{\"pull_requests\":$(json_numbers "$@")}")"
152
+ if [ "${add_response##*$'\n'}" = "404" ]; then
153
+ # A 404 from /stacks/<n>/add is ambiguous, and the two meanings need
154
+ # opposite responses: a disabled feature means carry on with an
155
+ # unregistered chain, while a wrong stack number means fix the number.
156
+ # Probing the collection tells them apart. This is a GET, so it is a read
157
+ # in a fallback position rather than a mutating call in one, and the POST
158
+ # above is never repeated.
159
+ probe="$(call GET /stacks)"
160
+ printf '%s\n' "${add_response%$'\n'*}"
161
+ printf 'HTTP 404\n' >&2
162
+ if [ "${probe##*$'\n'}" = "404" ]; then
163
+ printf 'register-stack: %s\n' "$NOT_ENABLED" >&2
164
+ exit 3
165
+ fi
166
+ die "stack ${stack} does not exist in ${REPO}; stacks are enabled here, so check the number with 'list'"
167
+ fi
168
+ report "$add_response" "200" "$NOT_ENABLED"
169
+ ;;
170
+ *)
171
+ die "unknown command '$cmd' (expected list, create, or add)"
172
+ ;;
173
+ esac
@@ -0,0 +1,57 @@
1
+ # Stacking a pull request on a lower layer
2
+
3
+ A dispatch names the **issue** you stack on, never the branch, because the branch carries a slug only its own worker chooses. Resolve it by pattern, and expect to wait.
4
+
5
+ ## Resolving a base branch that does not exist yet
6
+
7
+ 1. `git fetch origin`, then match `origin/claude/issue-<M>-*`. Confirm against the hosting API rather than a cached ref.
8
+ 2. No match means the lower worker has not pushed yet. Its session is running concurrently with yours. Re-check every 5 minutes for up to 90 minutes, re-arming a check-in rather than blocking.
9
+ 3. Once it matches, branch from it and set it as your pull request base.
10
+ 4. If it never appears, report that issue #M produced no branch, and **open no pull request**.
11
+
12
+ Idle waiting is the correct behavior here. A layer that gives up and branches from `main` produces a diff that deletes the layer below it.
13
+
14
+ ## Registering the stack
15
+
16
+ Setting base branches is necessary but not sufficient. Until the chain is registered as a stack, the host treats the pull requests as ordinary ones with unusual bases.
17
+
18
+ Registration buys two things always: the chain merges bottom-up as one operation, and reviewers get a stack map. Where branch protection is configured, it buys a third — the host evaluates a stacked pull request against the base of the stack rather than the branch it targets, so protections, required checks and code ownership all resolve against `main`.
19
+
20
+ **Register the moment the upper layer's pull request exists.** The Stacks API takes pull request numbers, so the upper pull request opens first and joins a stack second.
21
+
22
+ Run the script from the repository root. Execute it; do not read it and reimplement it.
23
+
24
+ ```
25
+ bash .claude/skills/night-shift-worker/scripts/register-stack.sh list
26
+ bash .claude/skills/night-shift-worker/scripts/register-stack.sh create <lower-pr> <upper-pr> [...]
27
+ bash .claude/skills/night-shift-worker/scripts/register-stack.sh add <stack-number> <pr>
28
+ ```
29
+
30
+ `create` takes pull request numbers bottom to top and needs at least two. Use it when yours is the second layer and no stack exists. Use `add` when a stack already holds your base's pull request. **Run it once and report its exact output and HTTP status. Never retry.**
31
+
32
+ The script reads the repository from `git remote get-url origin`. Set `STACK_REPO=owner/name` only when the remote is not the repository you mean.
33
+
34
+ ### When stacks are not available
35
+
36
+ Stacked pull requests are enabled per repository, so run `list` once before you open the upper pull request. **Exit status 3 means they are not enabled here.** That is not a failure of your run:
37
+
38
+ 1. Set the base branch correctly anyway. The chain is still a chain.
39
+ 2. Call neither `create` nor `add`.
40
+ 3. Say in the pull request body that the chain is correctly based but unregistered, and name the layer below.
41
+ 4. Continue to CI and the review round as normal.
42
+
43
+ Exit 1 is a usage or precondition error and needs fixing before you proceed. **A wrong stack number is exit 1, not exit 3**: `add` distinguishes them by probing the collection, so a 404 that means *this number does not exist* does not get read as *this repository cannot stack*. Check the number with `list` and run `add` again. Exit 0 is success.
44
+
45
+ ## Confirm a run happened, by its `event`
46
+
47
+ Read each layer's run through its `event` field, not its conclusion. A green check says a run passed. It does not say which trigger produced it, and a layer whose only run came from a manual dispatch is not being checked by its own pull request.
48
+
49
+ This matters most right after registration. A workflow triggers only on `opened`, `synchronize` and `reopened`; joining a stack fires none of the three, so a layer keeps whatever checks its `opened` event produced. Whether that was a full set depends on the workflow, which the config's `## CI workflow` names.
50
+
51
+ Scope the lookup to one workflow and one branch. An unscoped listing returns tens of kilobytes and will overflow a tool result. Address the workflow by its **file name**, taken from the config, not by matching a display name: a run's `name` is the *run* name, which a workflow can override, so a name filter that stops matching returns nothing, which reads exactly like "CI never ran."
52
+
53
+ The filter takes a **branch**. Match `head_sha` yourself against the rows it returns rather than asking the filter for a SHA.
54
+
55
+ Only if a layer shows no run at all, dispatch the workflow yourself and **report that as a finding** rather than as routine. Never push an empty commit, and never close and reopen a pull request, to provoke a run.
56
+
57
+ This is the same genus of mistake as reading a pull request's `merged` field without `merged_at`. A surface field that reads like an answer is not one until you know what populates it.
@@ -0,0 +1,16 @@
1
+ # Verifying a fix that already merged
2
+
3
+ A dispatch can hand you an issue that a merged pull request already covers. Your job changes from fix to verify, and that is a complete and valuable outcome. **Do not manufacture a change to justify the session.**
4
+
5
+ 1. Read the merged diff and any analysis document it added.
6
+ 2. Enumerate every symptom the issue and its comments describe, **separately**. A large fix often resolves some and not others, and a single verdict on the issue as a whole hides that.
7
+ 3. Test each symptom against current `main` and record a verdict per item.
8
+ 4. If everything is resolved, open a pull request carrying only the regression coverage that is still missing, or open none at all if coverage is complete. Then comment on the issue with your per-symptom verdicts and the evidence behind each, and recommend closing.
9
+ 5. If part survives, fix that part. Say plainly in the pull request which symptoms the earlier fix handled and which yours addresses.
10
+ 6. If the verification is complete but one item still needs evidence you cannot reach, say so in the comment and take the question swap in SKILL.md. An issue no unattended run can advance should not sit in the queue collecting a re-verification every night.
11
+
12
+ Steps 4 and 6 differ in who is blocked. **Resolved** means recommend closing and leave the label alone, because the maintainer decides. **Blocked on outside evidence** means the swap, because the queue is the wrong place for it either way.
13
+
14
+ **Never close the issue yourself, and never remove the queue label from an issue you believe is resolved.** Recommend, and let the maintainer decide. The one label change you may make is the question swap.
15
+
16
+ Record the outcome as `findNoChange` when you open no pull request, or as `openPullRequest` when you open one. See [night-shift-log.md](night-shift-log.md).
@@ -0,0 +1,7 @@
1
+ {
2
+ "source": "factoryengineering/skills",
3
+ "sourceType": "git",
4
+ "repoUrl": "https://github.com/factoryengineering/skills",
5
+ "subpath": "skills/refining-issues",
6
+ "installedAt": "2026-10-15T02:59:05.094Z"
7
+ }