@nanobpm/nano-workforce 0.61.1 → 0.63.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 CHANGED
@@ -1,3 +1,17 @@
1
+ # [0.63.0](https://github.com/nanobpm/nano-workforce/compare/v0.62.0...v0.63.0) (2026-08-13)
2
+
3
+
4
+ ### Features
5
+
6
+ * Overview landing page with collapsible active-work sections ([#206](https://github.com/nanobpm/nano-workforce/issues/206)) ([64c91df](https://github.com/nanobpm/nano-workforce/commit/64c91dfacd6b9b660a7bbbb1a3c9a8a78603b247)), closes [nanobpm/nano-ide#227](https://github.com/nanobpm/nano-ide/issues/227) [#205](https://github.com/nanobpm/nano-workforce/issues/205)
7
+
8
+ # [0.62.0](https://github.com/nanobpm/nano-workforce/compare/v0.61.1...v0.62.0) (2026-08-13)
9
+
10
+
11
+ ### Features
12
+
13
+ * **feature-prompt:** claim the issue with a comment before starting work ([#207](https://github.com/nanobpm/nano-workforce/issues/207)) ([b1b23c9](https://github.com/nanobpm/nano-workforce/commit/b1b23c9928704616aa2173cfa056668a11d32e1c))
14
+
1
15
  ## [0.61.1](https://github.com/nanobpm/nano-workforce/compare/v0.61.0...v0.61.1) (2026-08-13)
2
16
 
3
17
 
@@ -104,6 +104,8 @@ test("startFeature: seeds the single task slice + base-branch brief onto the ins
104
104
  assertEquals(v.task.prompt.includes("owner/repo#42"), true);
105
105
  assertEquals(v.converge, true);
106
106
  assertEquals(v.autoMerge, false);
107
+ // A single-issue run owns its issue, so the agent is told it may claim it (epic slices never set this).
108
+ assertEquals(v.claimIssue, true);
107
109
  assertEquals(v.baseBranch, "epic/x");
108
110
  // The brief is the authoritative base-branch override the agent gets via appendPrompt.
109
111
  assertEquals(v.baseBranchBrief.includes("epic/x"), true);
package/app/feature.ts CHANGED
@@ -202,6 +202,11 @@ export async function startFeature(
202
202
  // merge-loop. `converge=false` ⇒ merge is moot.
203
203
  converge,
204
204
  autoMerge,
205
+ // A single-issue feature run OWNS its issue (the whole issue is the slice), so the agent may
206
+ // claim it with a "starting work" comment on a first run (prompts/feature.md). Epic slices
207
+ // (plan-fanout) deliberately DO NOT set this — their `issue` is the shared parent epic, which
208
+ // must never be claimed per-slice.
209
+ claimIssue: true,
205
210
  // Seed the agent-result variables so the escalation loop + record worker can reference them
206
211
  // before the first `senior:feature` job completes (the harness merges the real values in).
207
212
  answer: null,
package/nano.app.json CHANGED
@@ -178,7 +178,7 @@
178
178
  "pages": {
179
179
  "enabled": true,
180
180
  "pagesDir": "pages",
181
- "homePage": "home",
181
+ "homePage": "overview",
182
182
  "sourceName": "app"
183
183
  }
184
184
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.61.1",
3
+ "version": "0.63.0",
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",
@@ -9,6 +9,7 @@
9
9
  "variant": "bar",
10
10
  "title": "Nano Workforce",
11
11
  "items": [
12
+ { "label": "Overview", "page": "overview" },
12
13
  { "label": "Convergence", "page": "home" },
13
14
  { "label": "Epics", "page": "epic" },
14
15
  { "label": "Feature", "page": "feature" },
@@ -10,6 +10,7 @@
10
10
  "sticky": true,
11
11
  "title": "Nano Workforce",
12
12
  "items": [
13
+ { "label": "Overview", "page": "overview" },
13
14
  { "label": "Convergence", "page": "home" },
14
15
  { "label": "Epics", "page": "epic" },
15
16
  { "label": "Feature", "page": "feature" },
@@ -10,6 +10,7 @@
10
10
  "sticky": true,
11
11
  "title": "Nano Workforce",
12
12
  "items": [
13
+ { "label": "Overview", "page": "overview" },
13
14
  { "label": "Convergence", "page": "home" },
14
15
  { "label": "Epics", "page": "epic" },
15
16
  { "label": "Feature", "page": "feature" },
@@ -10,6 +10,7 @@
10
10
  "sticky": true,
11
11
  "title": "Nano Workforce",
12
12
  "items": [
13
+ { "label": "Overview", "page": "overview" },
13
14
  { "label": "Convergence", "page": "home" },
14
15
  { "label": "Epics", "page": "epic" },
15
16
  { "label": "Feature", "page": "feature" },
@@ -9,6 +9,10 @@
9
9
  "variant": "bar",
10
10
  "title": "Nano Workforce",
11
11
  "items": [
12
+ {
13
+ "label": "Overview",
14
+ "page": "overview"
15
+ },
12
16
  {
13
17
  "label": "Convergence",
14
18
  "page": "home"
@@ -0,0 +1,137 @@
1
+ {
2
+ "schemaVersion": "1.0",
3
+ "title": "Overview",
4
+ "nodes": [
5
+ {
6
+ "type": "nav",
7
+ "id": "nav",
8
+ "props": {
9
+ "variant": "bar",
10
+ "sticky": true,
11
+ "title": "Nano Workforce",
12
+ "items": [
13
+ { "label": "Overview", "page": "overview" },
14
+ { "label": "Convergence", "page": "home" },
15
+ { "label": "Epics", "page": "epic" },
16
+ { "label": "Feature", "page": "feature" },
17
+ { "label": "Cockpit", "page": "cockpit" }
18
+ ]
19
+ }
20
+ },
21
+ {
22
+ "type": "text",
23
+ "id": "title",
24
+ "props": { "text": "Overview", "variant": "heading" }
25
+ },
26
+ {
27
+ "type": "text",
28
+ "id": "subtitle",
29
+ "props": {
30
+ "text": "Everything currently in flight across the three dispatch surfaces \u2014 PR convergences, Epics, and Feature runs. Each section lists only its active items, remembers whether you collapsed it, and shows a live count in its header. Follow any row to its detail page.",
31
+ "variant": "sub"
32
+ }
33
+ },
34
+ {
35
+ "type": "dataGrid",
36
+ "id": "overview-prs",
37
+ "props": {
38
+ "title": "Active PR convergences",
39
+ "collapsible": true,
40
+ "defaultCollapsed": false,
41
+ "showCount": true,
42
+ "rowKey": "pr_key",
43
+ "refreshMs": 5000,
44
+ "data": {
45
+ "kind": "datasource",
46
+ "source": "app",
47
+ "table": "pull_requests",
48
+ "orderBy": { "field": "updated_at", "dir": "desc" },
49
+ "filter": [
50
+ {
51
+ "field": "status",
52
+ "in": [
53
+ "converging",
54
+ "waiting_review",
55
+ "escalated",
56
+ "waiting_deps",
57
+ "waiting_merge",
58
+ "queued",
59
+ "merging"
60
+ ]
61
+ }
62
+ ]
63
+ },
64
+ "columns": [
65
+ { "field": "pr_key", "header": "PR", "link": { "kind": "page", "page": "home", "keyField": "pr_key" } },
66
+ { "field": "status", "header": "Status", "link": { "kind": "processExplorer", "keyField": "process_key" } },
67
+ {
68
+ "field": "incident_message",
69
+ "header": "Incident",
70
+ "badge": { "tone": "danger", "label": "1" }
71
+ },
72
+ { "field": "current_round", "header": "Round" },
73
+ { "field": "active_worker", "header": "Agent" },
74
+ { "field": "updated_at", "header": "Updated" }
75
+ ]
76
+ }
77
+ },
78
+ {
79
+ "type": "dataGrid",
80
+ "id": "overview-epics",
81
+ "props": {
82
+ "title": "Active Epics",
83
+ "collapsible": true,
84
+ "defaultCollapsed": false,
85
+ "showCount": true,
86
+ "rowKey": "plan_key",
87
+ "refreshMs": 5000,
88
+ "data": {
89
+ "kind": "datasource",
90
+ "source": "app",
91
+ "table": "plans",
92
+ "orderBy": { "field": "updated_at", "dir": "desc" },
93
+ "filter": [{ "field": "status", "in": ["planning", "dispatched"] }]
94
+ },
95
+ "columns": [
96
+ { "field": "plan_key", "header": "Epic", "link": { "kind": "page", "page": "epic-detail", "keyField": "plan_key" } },
97
+ { "field": "status", "header": "Status", "link": { "kind": "processExplorer", "keyField": "process_key" } },
98
+ { "field": "delivery", "header": "Delivery" },
99
+ { "field": "base_branch", "header": "Base branch" },
100
+ { "field": "wave_label", "header": "Wave" },
101
+ { "field": "task_count", "header": "Tasks" },
102
+ { "field": "issue_number", "header": "Issue", "linkField": "issue_url" },
103
+ { "field": "updated_at", "header": "Updated" }
104
+ ]
105
+ }
106
+ },
107
+ {
108
+ "type": "dataGrid",
109
+ "id": "overview-features",
110
+ "props": {
111
+ "title": "Active Features",
112
+ "collapsible": true,
113
+ "defaultCollapsed": false,
114
+ "showCount": true,
115
+ "rowKey": "feature_key",
116
+ "refreshMs": 5000,
117
+ "data": {
118
+ "kind": "datasource",
119
+ "source": "app",
120
+ "table": "feature_runs",
121
+ "orderBy": { "field": "updated_at", "dir": "desc" },
122
+ "filter": [{ "field": "status", "in": ["running", "awaiting_operator"] }]
123
+ },
124
+ "columns": [
125
+ { "field": "feature_key", "header": "Feature", "link": { "kind": "page", "page": "feature", "keyField": "feature_key" } },
126
+ { "field": "status", "header": "Status", "link": { "kind": "processExplorer", "keyField": "process_key" } },
127
+ { "field": "base_branch", "header": "Base branch" },
128
+ { "field": "pr_key", "header": "PR", "link": { "kind": "page", "page": "home", "keyField": "pr_key" } },
129
+ { "field": "converge", "header": "Converge" },
130
+ { "field": "auto_merge", "header": "Auto-merge" },
131
+ { "field": "outcome", "header": "Outcome" },
132
+ { "field": "updated_at", "header": "Updated" }
133
+ ]
134
+ }
135
+ }
136
+ ]
137
+ }
@@ -22,16 +22,44 @@ You have `gh` / git authenticated for the target repository.
22
22
 
23
23
  Always use the branch **`feat/<task.id>`**. Because a resumed run gets a fresh
24
24
  process with no memory of your last run, the branch name MUST be derivable from
25
- `task.id` alone. On start, check whether it already exists on the remote
26
- (`git ls-remote --heads origin feat/<task.id>` or
27
- `gh pr list --head feat/<task.id> --state all`):
25
+ `task.id` alone. On start, check whether it already exists on the remote with
26
+ `git ls-remote --heads origin feat/<task.id>` — a non-empty result means the
27
+ branch exists. Use this ref check as the authoritative first-run/resume signal:
28
+ a remote branch can exist without any PR, so `gh pr list --head feat/<task.id> --state all`
29
+ is reliable only as a *supplementary* PR lookup once `git ls-remote` has
30
+ established the branch exists, never as the existence check itself:
28
31
 
29
32
  - **It does not exist** → this is a first run. Branch off the base branch (see
30
33
  the note below — usually the repository default branch, but an epic may pin an
31
- integration branch in your appended task context).
34
+ integration branch in your appended task context). **Claim the issue now** — see
35
+ below.
32
36
  - **It exists** → this is a **resume**. `git fetch` and check it out, read its diff
33
37
  and any open (draft) PR, and **continue from there** — do not restart from
34
- scratch. Fold in `variables.answer` as the guidance you were waiting on.
38
+ scratch. Fold in `variables.answer` as the guidance you were waiting on. Do
39
+ **not** re-claim — you already announced this run on your first pass.
40
+
41
+ ## Claim your issue on a first run (only when `variables.claimIssue` is true)
42
+
43
+ So humans and other agents can see the work has been picked up, announce yourself
44
+ on the issue **before implementing** — but ONLY on a first run (your
45
+ `feat/<task.id>` branch did not yet exist, per the check above) AND only when
46
+ **`variables.claimIssue` is true**:
47
+
48
+ ```sh
49
+ gh issue comment <variables.issue> --body-file - <<'BODY'
50
+ 🤖 Starting work on this issue — branch `feat/<task.id>`.
51
+ BODY
52
+ ```
53
+
54
+ - `variables.claimIssue` is set **only for single-issue feature runs**, where
55
+ `variables.issue` IS the issue you implement and close, so claiming it is
56
+ correct. **Epic slices leave it unset** — their `variables.issue` is the shared
57
+ *parent epic*, which must never be claimed per-slice; when `claimIssue` is not
58
+ true, **skip this step entirely**.
59
+ - The branch-existence check above is what makes this idempotent: a resumed run
60
+ (branch already exists) never re-claims, so the issue gets exactly one claim.
61
+ - It is a courtesy claim, **not a gate** — if the comment fails (e.g. a transient
62
+ `gh` error), carry on and implement anyway.
35
63
 
36
64
  ## Your base branch (default branch, unless the epic pins one)
37
65
 
@@ -44,14 +72,16 @@ against the wrong base will not be merged into the epic.
44
72
 
45
73
  ## What to do
46
74
 
47
- 1. Clone / check out your base branch (first run the default branch, or the
75
+ 1. Claim your issue if `variables.claimIssue` is true and this is a first run (see
76
+ above).
77
+ 2. Clone / check out your base branch (first run — the default branch, or the
48
78
  pinned epic branch if your context names one) or your existing
49
79
  `feat/<task.id>` branch (resume — see above).
50
- 2. Implement `task.prompt`. Keep the change scoped to this slice only.
51
- 3. Commit (sign off — this repo family enforces DCO: `git commit -s`), push the
80
+ 3. Implement `task.prompt`. Keep the change scoped to this slice only.
81
+ 4. Commit (sign off — this repo family enforces DCO: `git commit -s`), push the
52
82
  branch, and open a pull request with `gh pr create` describing the slice and
53
83
  linking the parent issue (`Depends-on:`/`Closes` as appropriate).
54
- 4. Clean up any scratch clone/worktree you created outside the commit.
84
+ 5. Clean up any scratch clone/worktree you created outside the commit.
55
85
 
56
86
  > **Do not request the Copilot review yourself.** When you open a *ready* PR the
57
87
  > app enrolls it into the review-convergence loop and requests the initial
@@ -230,3 +230,57 @@ test("issue #87: plan_reviews is surfaced on the per-epic detail page", async ()
230
230
  }
231
231
  }
232
232
  });
233
+
234
+ test("issue #205: overview is the landing page and first nav item", async () => {
235
+ const overview = JSON.parse(readFileSync(`${ROOT}pages/overview.page.json`, "utf8"));
236
+
237
+ // The overview must be the pages-surface home so it's the default destination.
238
+ const app = JSON.parse(readFileSync(`${ROOT}nano.app.json`, "utf8"));
239
+ assert(
240
+ app?.surfaces?.pages?.homePage === "overview",
241
+ "nano.app.json surfaces.pages.homePage must be \"overview\" (the landing page)",
242
+ );
243
+
244
+ // Every page's nav must lead with Overview so it's the first tab everywhere.
245
+ for (const e of readdirSync(`${ROOT}pages`, { withFileTypes: true })) {
246
+ if (!e.isFile() || !e.name.endsWith(".page.json")) continue;
247
+ const page = JSON.parse(readFileSync(`${ROOT}pages/${e.name}`, "utf8"));
248
+ const nav = (page.nodes ?? []).find((n: Json) => n.type === "nav");
249
+ if (!nav) continue;
250
+ const first = nav.props?.items?.[0];
251
+ assert(
252
+ first?.page === "overview" && first?.label === "Overview",
253
+ `${e.name}: nav must lead with the Overview tab (first item page "overview")`,
254
+ );
255
+ }
256
+
257
+ // Three collapsible active-work sections, one per dispatch surface, each with a
258
+ // live count in its header (showCount) and a persisted collapse toggle (collapsible).
259
+ const expected: Record<string, string[]> = {
260
+ pull_requests: [
261
+ "converging",
262
+ "waiting_review",
263
+ "escalated",
264
+ "waiting_deps",
265
+ "waiting_merge",
266
+ "queued",
267
+ "merging",
268
+ ],
269
+ plans: ["planning", "dispatched"],
270
+ feature_runs: ["running", "awaiting_operator"],
271
+ };
272
+ const grids = (overview.nodes ?? []).filter((n: Json) => n.type === "dataGrid");
273
+ for (const [table, statuses] of Object.entries(expected)) {
274
+ const grid = grids.find((g: Json) => g.props?.data?.table === table);
275
+ assert(grid, `overview.page.json must have a section bound to "${table}"`);
276
+ assert(grid.props.collapsible === true, `overview "${table}" section must be collapsible`);
277
+ assert(grid.props.showCount === true, `overview "${table}" section must show a live count`);
278
+ const filter = grid.props?.data?.filter?.find((f: Json) => f.field === "status");
279
+ assert(filter, `overview "${table}" section must filter on status`);
280
+ assert(
281
+ JSON.stringify([...filter.in].sort()) === JSON.stringify([...statuses].sort()),
282
+ `overview "${table}" section must filter to the active statuses ${JSON.stringify(statuses)}`,
283
+ );
284
+ }
285
+ });
286
+