@nanobpm/nano-workforce 0.200.3 → 0.200.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.200.3",
3
+ "version": "0.200.5",
4
4
  "description": "Nano Workforce — an Agent Graph Orchestration application for Agentic SDLC: durable BPMN processes that coordinate a graph of AI agents across the software delivery lifecycle.",
5
5
  "type": "module",
6
6
  "main": "main.ts",
@@ -65,7 +65,7 @@
65
65
  },
66
66
  "dependencies": {
67
67
  "@nanobpm/agentic": "^0.15.0",
68
- "@nanobpm/urban": "^0.95.0",
68
+ "@nanobpm/urban": "^0.96.0",
69
69
  "bpmn-auto-layout": "^2.0.0-alpha.2"
70
70
  },
71
71
  "devDependencies": {
@@ -133,6 +133,17 @@ you on the right side of it:
133
133
  (`Refs #N` / `Part of #N`) and **leave #N open** (or convert #N into a
134
134
  tracking/umbrella issue for the remainder). The classifier flags a PR that
135
135
  closes #N while leaving part of #N's stated scope undelivered and untracked.
136
+ **This rule governs your COMMIT MESSAGES and your PR TITLE too, not just the PR
137
+ body.** This repo family squash-merges with `COMMIT_MESSAGES`, so a `Closes #N`
138
+ in **any commit body** closes the issue on merge — even after you correct the PR
139
+ body to `Part of #N`. The **PR title becomes the squash commit SUBJECT**, so a
140
+ closing keyword in the title (`fix: parser slice (Closes #N)`) likewise closes #N
141
+ on merge. A partial slice whose PR body says `Part of #N` but whose first commit —
142
+ or whose title — still carries `Closes #N` *still* closes the broader parent on
143
+ merge, and the scope gate scans the title and commit bodies as well as the PR
144
+ body. So when you split scope, write `Part of #N` (never a closing keyword) in the
145
+ PR **title**, the PR body, and every commit body; if an early commit already
146
+ closed it, amend/reword that commit before you push.
136
147
  2. **A deferred remainder must be a FILED, tracked issue — never just prose.** If
137
148
  your PR defers part of its scope, **file a follow-up issue for each deferred
138
149
  item** and link it in the PR body with an explicit tracking marker the classifier
@@ -84,10 +84,15 @@ For every distinct child issue number `N` you find:
84
84
  - Otherwise, emit **one task** for it (see the output contract), with:
85
85
  - `id` = `issue-N`,
86
86
  - `title` = the sub-issue's title,
87
- - `prompt` = a self-contained brief built from the sub-issue's body, and end
88
- the prompt with an explicit instruction to the implementing agent to open its
89
- PR against this specific sub-issue and include `Closes #N` in the PR body so
90
- the sub-issue is linked and auto-closed on merge.
87
+ - `prompt` = a self-contained brief built from the sub-issue's body carrying
88
+ **#N's FULL stated scope** (every acceptance criterion / checkbox of the
89
+ sub-issue — an adopted sub-issue task owns its whole sub-issue, so the brief
90
+ must not silently cover only part of it), and end the prompt with an explicit
91
+ instruction to the implementing agent to open its PR against this specific
92
+ sub-issue and include `Closes #N` in the PR body so the sub-issue is linked and
93
+ auto-closed on merge. A closing keyword is only correct because the task
94
+ delivers #N in full; **never pair `Closes #N` with a brief scoped to a subset
95
+ of #N** (see "Closing keywords" below).
91
96
  - `dependsOn` = **honour any inter-sub-issue ordering the human declared.** Scan
92
97
  the sub-issue's body for an explicit dependency directive — a line such as
93
98
  `Depends-on: #7`, `Depends on #7`, or `Blocked by #7` (case-insensitive; there
@@ -192,6 +197,58 @@ a cohesive published artifact at the same time. Reserve genuinely separate packa
192
197
  for the consumer-facing cases above, and say in the task prompt which consumer
193
198
  justifies the split.
194
199
 
200
+ ### Closing keywords → only a FULL-scope slice may close its issue
201
+
202
+ When you decompose one issue into several slices, each slice delivers only **part**
203
+ of that issue's acceptance criteria. A slice scoped to a subset of an issue's scope
204
+ **must not be told to close that issue** — that is the exact planning defect this
205
+ rule exists to prevent (a node scoped to one of an issue's three acceptance
206
+ criteria was told to "close" it, the remainder was lost, and a human had to file
207
+ the follow-up and rewrite the PR by hand).
208
+
209
+ So, when you author each slice's `prompt`:
210
+
211
+ - **Only instruct a closing keyword (`Closes/Fixes/Resolves #N`) in a slice whose
212
+ brief delivers #N's FULL stated scope — and say so in an issue-anchored
213
+ whole-scope phrase the deterministic validator recognises.** If exactly one
214
+ slice covers the whole issue (the common single-task case), it may close it. If
215
+ you split the issue across slices, the parent issue is **broader** than any one
216
+ slice — so **no slice** closes it with a bare brief: instruct each partial slice
217
+ to reference the parent non-blockingly (`Part of #N` / `Refs #N`) and **leave
218
+ #N open**, OR designate one final slice that carries the `Closes #N`. That final
219
+ slice is the exception only when its brief makes it **own and verify #N's
220
+ COMPLETE stated scope** — every acceptance criterion, not just the remainder.
221
+ `dependsOn` on the sibling slices orders the merges, but it does **not** by
222
+ itself make the final slice's brief full-scope: a final slice scoped only to
223
+ "the remaining criterion" is still a partial brief and must **not** close #N.
224
+ Scope the final closer to re-verify the whole issue (its brief must direct it to
225
+ check every checkbox and confirm the siblings' merged work satisfies them)
226
+ before it emits `Closes #N`.
227
+ **The validator (`validateDeliveryGraph`) enforces this deterministically:** an
228
+ `agent.prompt` that closes an issue is rejected unless the brief also carries an
229
+ explicit **issue-anchored whole-scope acknowledgement TIED TO THE ISSUE IT
230
+ CLOSES**. Anchor the acknowledgement to the **same** issue — e.g. "delivers #N's
231
+ full stated scope", "every acceptance criterion of #N", "all of #N", "the whole
232
+ issue #N". A marker that names a **different** issue than the one you close
233
+ (acknowledge #11's scope, but `Closes #12`), or one that attributes the scope to
234
+ **siblings/other slices** ("the full scope of #N is handled by siblings"), does
235
+ **not** count — the validator credits a whole-scope phrase to issue #N only when it
236
+ sits in the same clause as #N (or when the prompt references exactly one issue) and
237
+ is not attributed to others. When a prompt closes more than one issue, give **each**
238
+ closing target its own anchored acknowledgement. A **bare adverb of completeness does not count** —
239
+ "fully", "completely", "end-to-end", "in full", "in its entirety" can modify a
240
+ *partial* deliverable ("implement one criterion fully"), so the validator
241
+ ignores them; always name the issue's whole scope, not just an adverb.
242
+ - **The remainder must be tracked, never dropped.** Every acceptance criterion of
243
+ the split issue must be covered by **some** slice in this plan (collectively the
244
+ slices must deliver #N's whole scope). Do not emit a slice set that silently
245
+ under-delivers the parent while one slice closes it.
246
+ - **This governs the COMMIT MESSAGES you tell the agent to write, too.** This repo
247
+ family squash-merges with `COMMIT_MESSAGES`, so a `Closes #N` in **any commit
248
+ body** closes the issue on merge even if the PR body says `Part of #N`. When a
249
+ slice is partial, instruct the agent to write `Part of #N` (never a closing
250
+ keyword) in **both** the PR body and every commit body.
251
+
195
252
  ## Output contract
196
253
 
197
254
  Write a JSON object of **result variables** to the file named by the
@@ -46,13 +46,46 @@ result.
46
46
 
47
47
  ## What to do
48
48
 
49
- 1. **Read the PR body.** `gh pr view <prNumber> --repo <repo> --json body,title`.
49
+ 1. **Read the PR body, the PR TITLE, AND the commit bodies.**
50
+ `gh pr view <prNumber> --repo <repo> --json body,title`.
50
51
  Extract every issue the body **closes with a GitHub closing keyword** —
51
52
  `close/closes/closed`, `fix/fixes/fixed`, `resolve/resolves/resolved` followed by
52
- `#N`, `owner/repo#N`, or a full issue URL. A **non-closing** reference (`Refs #N`,
53
+ `#N`, `owner/repo#N`, or a full issue URL. GitHub also recognises an **optional colon**
54
+ between the keyword and its target (`Closes: #12`, `Fixes: owner/repo#12`), so treat the
55
+ colon form as a closing reference too. A **non-closing** reference (`Refs #N`,
53
56
  `Part of #N`, `Depends-on #N`, `Follow-up: #N`) does **not** close an issue —
54
57
  ignore those for the closing-scope check (but note the follow-up links; see below).
55
58
 
59
+ **Scan the PR TITLE with these exact same closing-keyword rules too.** This repo
60
+ family squash-merges, so the **PR title becomes the squash commit SUBJECT**
61
+ (`AGENTS.md` "PR titles must be Conventional too — they become the release trigger"),
62
+ and GitHub honours a closing keyword in the squash subject exactly like one in the
63
+ body or a commit. So a conventional title such as `fix: parser slice (Closes #12)`
64
+ **closes #12 on merge** even when the body only says `Part of #12` and every commit
65
+ is clean. Treat a closing keyword found in the **title** exactly like one in the PR
66
+ body — add its `#N` to the set of closed issues you judge below (and if the slice is
67
+ partial, tell the human to reword the title to a non-closing form).
68
+
69
+ **Then scan the PR's commit messages for the same closing keywords.** Use the
70
+ **full commit history** — fetch base and head, then
71
+ `git log --format='%H%n%B' origin/<base>..<head>` (or `git log origin/<base>..<head>`).
72
+ **Do NOT rely on `gh pr view <prNumber> --repo <repo> --json commits` for this:**
73
+ the GitHub CLI silently truncates the `commits` field at **100 entries**, so on a
74
+ 101–250-commit PR it can miss an offending `Closes #N` in a later commit and
75
+ incorrectly clear the scope gate. If you must use the API instead of `git log`,
76
+ page it explicitly (`gh api --paginate .../pulls/<prNumber>/commits`) rather than
77
+ the capped `--json commits`. Likewise do **not** use `gh pr diff` here — it returns
78
+ only the patch, not the commit subjects/bodies, so it cannot detect a closing
79
+ keyword and would incorrectly clear the PR. This matters because this repo
80
+ family **squash-merges with `COMMIT_MESSAGES`**: the squash commit concatenates
81
+ every commit body, so a `Closes #N` left in **any commit body closes #N on
82
+ merge** even when the PR body was later corrected to `Part of #N`. Treat a
83
+ closing keyword found in a commit body **exactly** like one in the PR body — add
84
+ its `#N` to the set of closed issues you judge below. A PR whose body says
85
+ `Part of #N` but whose commit body still carries `Closes #N`, while part of #N's
86
+ scope is undelivered and untracked, is a genuine under-delivery you must block
87
+ (name the offending commit and tell the human to reword/amend it to `Part of #N`).
88
+
56
89
  2. **If there are no closing-keyword issues, the PR closes nothing** — there is no
57
90
  broader-scoped parent to under-deliver. Return **`scopeBlocked: false`** and stop.
58
91
 
@@ -0,0 +1,139 @@
1
+ // Guard for issue #858: a planner must not scope an agent node to PART of an issue while still
2
+ // telling it to CLOSE that issue, and the scope gate + agent contract must cover COMMIT bodies.
3
+ //
4
+ // Field case (merlin, delivery graph `5e36636255ab`): node `i12`'s prompt said "implement
5
+ // nano-supervisor#12 and open a PR … that closes it", but the body only described one of #12's
6
+ // three acceptance criteria. The agent did exactly that and wrote `Closes #12` in BOTH the PR body
7
+ // and the commit body. The convergence scope gate blocked, but a human still had to file the
8
+ // follow-up and rewrite the PR. The defect was in planning: a node scoped narrower than its issue
9
+ // must not be told to close it. The repo squash-merges with `COMMIT_MESSAGES`, so a `Closes #N` in a
10
+ // commit body closes the issue on merge even after the PR body is corrected — so the agent contract,
11
+ // the planner, and the scope gate must all reason about commit bodies, not just the PR body.
12
+ //
13
+ // These are prompt CONTRACTS (not code paths): the planner (`plan.md`), the implementation agent
14
+ // (`feature.md`), and the scope gate (`scope-classify.md`) are all prompt-driven. This test pins the
15
+ // guidance so a future prompt edit cannot silently drop the guard that closes this failure class.
16
+ import { test } from "node:test";
17
+ import { assert } from "#test-assert";
18
+ import { readFileSync } from "node:fs";
19
+ import { join } from "node:path";
20
+
21
+ const PROMPTS_DIR = join(import.meta.dirname, "..", "resources", "prompts");
22
+ const read = (name: string): string => readFileSync(join(PROMPTS_DIR, name), "utf8");
23
+
24
+ // The GitHub closing keywords, as the gate/agent must recognise them. A "closing keyword" in prose
25
+ // is any of these; a non-closing ref (`Part of`, `Refs`, `Follow-up`) is explicitly NOT.
26
+ const CLOSING_KEYWORDS = ["close", "fix", "resolve"];
27
+
28
+ // The closing-keyword SYNTAX each prompt must document for EVERY verb family — the canonical
29
+ // instruction triplet `Closes/Fixes/Resolves` (or the inflected `close/closes/closed …
30
+ // fix/fixes/fixed … resolve/resolves/resolved` listing). Asserting the syntax (not a bare
31
+ // `includes("fix")`, which passes for unrelated reasons) means dropping a family from a contract —
32
+ // e.g. removing `Fixes` — turns the test red instead of leaving it green.
33
+ const CLOSING_SYNTAX = [
34
+ /closes?\/fixes?\/resolves?/i,
35
+ /close\/closes\/closed[\s\S]*fix\/fixes\/fixed[\s\S]*resolve\/resolves\/resolved/i,
36
+ ];
37
+
38
+ test("feature.md: the closing-keyword rule covers COMMIT MESSAGES, not just the PR body", () => {
39
+ const feature = read("feature.md").toLowerCase();
40
+ // The agent contract must tell the agent a closing keyword in a commit body closes the issue on
41
+ // merge (the COMMIT_MESSAGES squash concern), so a partial slice must write `Part of #N` in both.
42
+ assert(
43
+ feature.includes("commit messages") || feature.includes("commit body") || feature.includes("commit bodies"),
44
+ "feature.md must tie the closing-keyword rule to commit messages/bodies",
45
+ );
46
+ assert(feature.includes("commit_messages"), "feature.md must name the COMMIT_MESSAGES squash behaviour");
47
+ });
48
+
49
+ test("plan.md: a partial slice must not be told to close its broader-scoped parent issue", () => {
50
+ const plan = read("plan.md");
51
+ const lower = plan.toLowerCase();
52
+ // The decomposition step must carry explicit closing-keyword guidance for split issues.
53
+ assert(lower.includes("closing keyword"), "plan.md must discuss closing keywords for slices");
54
+ // A full-scope requirement must gate a closing keyword, and a partial slice must use `Part of #N`.
55
+ assert(lower.includes("full"), "plan.md must require FULL scope before a slice closes an issue");
56
+ assert(plan.includes("Part of #N"), "plan.md must steer a partial slice to `Part of #N`");
57
+ // The planner guidance must also reach commit messages (the COMMIT_MESSAGES squash concern).
58
+ assert(lower.includes("commit_messages") || lower.includes("commit bod"), "plan.md must cover commit bodies");
59
+ });
60
+
61
+ test("scope-classify.md: the scope gate scans COMMIT bodies for closing keywords too", () => {
62
+ const gate = read("scope-classify.md");
63
+ const lower = gate.toLowerCase();
64
+ // The gate must read the PR's commits (not only its body) for closing keywords.
65
+ assert(lower.includes("commit"), "scope-classify.md must mention commits");
66
+ // It must name the commit body/message as the thing to scan…
67
+ assert(
68
+ lower.includes("commit bod") || lower.includes("commit message"),
69
+ "scope-classify.md must instruct reading commit bodies/messages",
70
+ );
71
+ // …AND prescribe an actual FULL-HISTORY retrieval mechanism the contract permits (`git log` or an
72
+ // explicitly `--paginate`d API call). Merely mentioning "commit bodies" — or the capped, explicitly
73
+ // FORBIDDEN `--json commits` — is not enough: deleting the real retrieval instruction would leave a
74
+ // substring-only check green while the gate silently loses its ability to see a `Closes #N` in a
75
+ // later commit of a >100-commit PR (issue #858 round-5 review).
76
+ assert(
77
+ /git\s+log/.test(lower) || lower.includes("--paginate"),
78
+ "scope-classify.md must prescribe a full-history commit retrieval (`git log` or a `--paginate`d " +
79
+ "API call), not the capped `--json commits`",
80
+ );
81
+ // It must explain WHY: the COMMIT_MESSAGES squash carries a commit-body closing keyword to merge.
82
+ assert(lower.includes("commit_messages"), "scope-classify.md must name the COMMIT_MESSAGES squash behaviour");
83
+ });
84
+
85
+ test("scope-classify.md: the scope gate scans the PR TITLE for closing keywords (squash subject)", () => {
86
+ const gate = read("scope-classify.md");
87
+ const lower = gate.toLowerCase();
88
+ // A closing keyword in the PR title becomes the squash commit SUBJECT and closes the issue on merge,
89
+ // so the gate must scan the title with the same closing-keyword rules as the body/commits — not only
90
+ // fetch it (issue #858 round-16 review). Pin the instruction, not just the `--json body,title` fetch.
91
+ assert(lower.includes("title"), "scope-classify.md must reference the PR title");
92
+ assert(
93
+ /scan\s+the\s+pr\s+title|title[\s\S]{0,80}closing[- ]keyword|closing[- ]keyword[\s\S]{0,80}title/.test(lower),
94
+ "scope-classify.md must instruct scanning the PR TITLE with the closing-keyword rules",
95
+ );
96
+ // …and explain WHY the title matters: it becomes the squash commit subject.
97
+ assert(
98
+ /squash\s+(?:commit\s+)?subject|title\s+becomes\s+the\s+squash/.test(lower),
99
+ "scope-classify.md must explain the title becomes the squash commit subject",
100
+ );
101
+ });
102
+
103
+ test("feature.md: the no-close-on-partial rule also governs the PR TITLE (squash subject)", () => {
104
+ const lower = read("feature.md").toLowerCase();
105
+ // The agent authors the PR title, so its contract must warn that a closing keyword there closes the
106
+ // issue on merge (title -> squash subject), not only the body/commit bodies (issue #858 round-16).
107
+ assert(lower.includes("title"), "feature.md must reference the PR title in the closing-keyword rule");
108
+ assert(
109
+ /title\s+becomes\s+the\s+squash|squash\s+(?:commit\s+)?subject/.test(lower),
110
+ "feature.md must tie the PR title to the squash commit subject",
111
+ );
112
+ });
113
+
114
+ test("all three prompts recognise the full set of GitHub closing keywords", () => {
115
+ for (const name of ["feature.md", "plan.md", "scope-classify.md"]) {
116
+ const lower = read(name).toLowerCase();
117
+ for (const kw of CLOSING_KEYWORDS) {
118
+ assert(lower.includes(kw), `${name} must reference the closing keyword family "${kw}"`);
119
+ }
120
+ }
121
+ });
122
+
123
+ // Stronger than the substring check above: pin the closing-keyword SYNTAX in each prompt, so a future
124
+ // edit that drops a verb family from the contract (e.g. `Closes/Resolves` with `Fixes` removed) fails
125
+ // loudly instead of staying green because the word "fix" still appears somewhere unrelated.
126
+ test("all three prompts document the closing-keyword SYNTAX for every verb family (close/fix/resolve)", () => {
127
+ for (const name of ["feature.md", "plan.md", "scope-classify.md"]) {
128
+ const text = read(name);
129
+ assert(
130
+ CLOSING_SYNTAX.some((re) => re.test(text)),
131
+ `${name} must document the Closes/Fixes/Resolves closing-keyword triplet (or the inflected ` +
132
+ `close/closes/closed … listing), not merely the bare substrings`,
133
+ );
134
+ assert(
135
+ /\bcloses?\s+#(?:n|\d)/i.test(text),
136
+ `${name} must show the actionable \`Closes #N\` instruction form`,
137
+ );
138
+ }
139
+ });