@nanobpm/nano-workforce 0.108.0 → 0.109.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 +7 -0
- package/app/agentSkill.ts +54 -0
- package/app/resolveApiBase.test.ts +43 -0
- package/app/resolveApiBase.ts +28 -0
- package/docs/adr/0005-agent-authored-delivery-graphs.md +221 -0
- package/openapi.yaml +54 -0
- package/operations/getAgentInstructions.ts +2 -16
- package/operations/getAgentSkill.test.ts +72 -0
- package/operations/getAgentSkill.ts +36 -0
- package/package.json +1 -1
- package/pages/home.page.json +0 -14
- package/pages/overview.page.json +14 -0
- package/skills/README.md +50 -0
- package/skills/nano-workforce/SKILL.md +126 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [0.109.0](https://github.com/nanobpm/nano-workforce/compare/v0.108.0...v0.109.0) (2026-08-20)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **skill:** add nano-workforce operator bootstrap skill ([#382](https://github.com/nanobpm/nano-workforce/issues/382)) ([ef6701f](https://github.com/nanobpm/nano-workforce/commit/ef6701f4df76bcfc762ddec20d745bf5edd0ec1b))
|
|
7
|
+
|
|
1
8
|
# [0.108.0](https://github.com/nanobpm/nano-workforce/compare/v0.107.2...v0.108.0) (2026-08-20)
|
|
2
9
|
|
|
3
10
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// The Nano Workforce operator *skill* served by GET /app/api/agent/skill (operationId
|
|
2
|
+
// `getAgentSkill`). Companion to the agent guide (app/agentGuide.ts): where the guide is the full,
|
|
3
|
+
// instance-keyed playbook, the skill is a small, portable *bootstrap* an agent runtime (Copilot
|
|
4
|
+
// CLI, Claude) loads on demand — it resolves which instance to drive and then fetches the live
|
|
5
|
+
// guide. The user clicks "Agent Instructions" on the Overview tab, copies the prompt, and the agent
|
|
6
|
+
// loads THIS skill from THIS instance.
|
|
7
|
+
//
|
|
8
|
+
// The skill is authored as plain markdown in `skills/nano-workforce/SKILL.md` (kept OUT of
|
|
9
|
+
// `resources/` so the deploy-by-convention walk does NOT treat it as a deployable model, ADR 0062)
|
|
10
|
+
// and read from the checkout at module load — same approach as agentGuide.ts / version.ts. A
|
|
11
|
+
// `__BASE__` placeholder, if present, is substituted per request so any embedded example is
|
|
12
|
+
// copy-pasteable against THIS instance.
|
|
13
|
+
//
|
|
14
|
+
// Reading is best-effort: a missing file yields a short built-in fallback rather than throwing, so
|
|
15
|
+
// the endpoint never 500s just because the skill is absent from a stripped-down deploy.
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
const REPO_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
21
|
+
const SKILL_PATH = join(REPO_ROOT, "skills", "nano-workforce", "SKILL.md");
|
|
22
|
+
|
|
23
|
+
// Read the raw skill once, at module load. Frozen for the life of the process.
|
|
24
|
+
const RAW_SKILL: string = (() => {
|
|
25
|
+
try {
|
|
26
|
+
return readFileSync(SKILL_PATH, "utf8");
|
|
27
|
+
} catch {
|
|
28
|
+
return [
|
|
29
|
+
"---",
|
|
30
|
+
"name: nano-workforce",
|
|
31
|
+
"description: Drive and debug a running Nano Workforce instance.",
|
|
32
|
+
"---",
|
|
33
|
+
"",
|
|
34
|
+
"# Nano Workforce operator skill",
|
|
35
|
+
"",
|
|
36
|
+
"This skill could not be read from this deployment. Fetch the live operator guide and follow",
|
|
37
|
+
"it instead — it is the authoritative, version-matched playbook:",
|
|
38
|
+
"",
|
|
39
|
+
" curl -sS __BASE__/agent",
|
|
40
|
+
"",
|
|
41
|
+
"If the instance is secured with a shared secret (NANO_PR_WEBHOOK_SECRET), add its value as an",
|
|
42
|
+
"`x-hook-secret` header. Confirm which instance you are driving before any side-effecting call.",
|
|
43
|
+
"",
|
|
44
|
+
].join("\n");
|
|
45
|
+
}
|
|
46
|
+
})();
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Render the skill for a given app control-API base (e.g. "https://host/app/api"). Substitutes every
|
|
50
|
+
* `__BASE__` occurrence so any embedded example targets THIS instance.
|
|
51
|
+
*/
|
|
52
|
+
export function renderAgentSkill(apiBase: string): string {
|
|
53
|
+
return RAW_SKILL.replaceAll("__BASE__", apiBase.replace(/\/+$/, ""));
|
|
54
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Tests for app/resolveApiBase.ts — the single canonical control-API base reconstruction shared by
|
|
2
|
+
// getAgentInstructions and getAgentSkill. Covers proxy-header handling, scheme restriction,
|
|
3
|
+
// host-absent fallback, and mount-suffix stripping for both mount depths.
|
|
4
|
+
import { test } from "node:test";
|
|
5
|
+
import { assertEquals } from "#test-assert";
|
|
6
|
+
import { resolveApiBase } from "./resolveApiBase.ts";
|
|
7
|
+
|
|
8
|
+
function req(headers: Record<string, string>, path: string) {
|
|
9
|
+
return { path, headers: new Headers(headers) };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
test("strips the single-segment mount suffix to recover the base", () => {
|
|
13
|
+
assertEquals(resolveApiBase(req({ host: "wf.example.com" }, "/app/api/agent"), "agent"), "http://wf.example.com/app/api");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("strips the nested mount suffix to recover the base", () => {
|
|
17
|
+
assertEquals(
|
|
18
|
+
resolveApiBase(req({ host: "wf.example.com" }, "/app/api/agent/skill"), "agent/skill"),
|
|
19
|
+
"http://wf.example.com/app/api",
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("honours x-forwarded-proto and x-forwarded-host", () => {
|
|
24
|
+
const r = req({ host: "internal", "x-forwarded-host": "wf.example.com", "x-forwarded-proto": "https" }, "/app/api/agent");
|
|
25
|
+
assertEquals(resolveApiBase(r, "agent"), "https://wf.example.com/app/api");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("restricts x-forwarded-proto to http/https", () => {
|
|
29
|
+
const r = req({ host: "wf.example.com", "x-forwarded-proto": "javascript" }, "/app/api/agent/skill");
|
|
30
|
+
assertEquals(resolveApiBase(r, "agent/skill"), "http://wf.example.com/app/api");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("falls back to a localhost default when the Host header is absent", () => {
|
|
34
|
+
assertEquals(resolveApiBase(req({}, "/app/api/agent"), "agent"), "http://localhost:3000/app/api");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("tolerates a leading slash on the mount suffix", () => {
|
|
38
|
+
assertEquals(resolveApiBase(req({ host: "h" }, "/app/api/agent"), "/agent"), "http://h/app/api");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("strips multiple trailing slashes after the mount suffix", () => {
|
|
42
|
+
assertEquals(resolveApiBase(req({ host: "h" }, "/app/api/agent/skill///"), "agent/skill"), "http://h/app/api");
|
|
43
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Canonical reconstruction of the app control-API base a caller reached us on (e.g.
|
|
2
|
+
// "https://host/app/api"), so an operation can rewrite its embedded examples to THIS instance and
|
|
3
|
+
// keep them copy-pasteable. One implementation shared by every /app/api operation that keys output
|
|
4
|
+
// to the request base (getAgentInstructions, getAgentSkill, …) — per AGENTS.md "Derivation over
|
|
5
|
+
// duplication: no drift surfaces", proxy-header handling and base-path stripping must not fork.
|
|
6
|
+
//
|
|
7
|
+
// Honour reverse-proxy forwarding headers; fall back to a localhost default when the Host header is
|
|
8
|
+
// absent (e.g. a raw unit-test request).
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Recover the control-API base from a request, stripping the operation's own mount suffix.
|
|
12
|
+
*
|
|
13
|
+
* @param req the request (path + headers)
|
|
14
|
+
* @param mountSuffix the operation's path suffix to strip to recover the base, e.g. "agent" or
|
|
15
|
+
* "agent/skill" (with or without a leading slash). The base defaults to
|
|
16
|
+
* "/app/api" when the path is nothing but the suffix.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveApiBase(req: { path: string; headers: Headers }, mountSuffix: string): string {
|
|
19
|
+
const rawProto = (req.headers.get("x-forwarded-proto") ?? "http").split(",")[0].trim().toLowerCase();
|
|
20
|
+
// x-forwarded-proto is user-controlled behind some proxies; only trust http/https.
|
|
21
|
+
const proto = rawProto === "http" || rawProto === "https" ? rawProto : "http";
|
|
22
|
+
const host = (req.headers.get("x-forwarded-host") ?? req.headers.get("host") ?? "").split(",")[0].trim();
|
|
23
|
+
// The op is mounted at "<base>/<mountSuffix>"; strip the trailing segments to recover the base path.
|
|
24
|
+
const suffix = mountSuffix.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
25
|
+
const stripRe = new RegExp(`/${suffix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/*$`);
|
|
26
|
+
const basePath = req.path.replace(stripRe, "") || "/app/api";
|
|
27
|
+
return host ? `${proto}://${host}${basePath}` : `http://localhost:3000${basePath}`;
|
|
28
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# ADR 0005 — Agent-authored delivery graphs (data-over-a-closed-vocabulary, human-in-the-loop)
|
|
2
|
+
|
|
3
|
+
Status: **Proposed.**
|
|
4
|
+
Date: 2026-08-20.
|
|
5
|
+
|
|
6
|
+
> **Scope note.** This is a **nano-workforce-local** ADR — it governs how *this app* runs
|
|
7
|
+
> heterogeneous cross-repo delivery work that mixes automated and human steps. Platform-wide ADRs live
|
|
8
|
+
> in `Magikcraft/nano-bpm/docs/adr` (referenced by number + repo, e.g. "nano-bpm ADR 0051").
|
|
9
|
+
> nano-workforce's own series continues here after ADR 0004.
|
|
10
|
+
|
|
11
|
+
Relates to:
|
|
12
|
+
nano-workforce **ADR 0001** (cross-repo epics + the generic `ReadinessProbe` wait-gate — this ADR
|
|
13
|
+
generalizes §4's deferred "release DAG" and its §2 gate into an arbitrary graph),
|
|
14
|
+
nano-workforce **ADR 0002** (escalations are user tasks + forms — the human-node machinery this ADR
|
|
15
|
+
promotes from *exception* to *scheduled node*),
|
|
16
|
+
nano-workforce **ADR 0003** (epic base-branch admission — the guardrail an `agent`/merge node inherits),
|
|
17
|
+
nano-workforce **ADR 0004** (shared-contract coordination — the discipline a cross-repo edge rides on),
|
|
18
|
+
nano-bpm **ADR 0026** (Urban human surfaces + `taskInbox` — where human nodes render),
|
|
19
|
+
nano-bpm **ADR 0046** (agent-as-worker vs agent-in-the-node — why an agent can answer a human node's
|
|
20
|
+
form, and why a node's *body* can itself be an agent),
|
|
21
|
+
nano-bpm **ADR 0051** (nano-workforce — the crew orchestrator whose `plan-fanout` interpreter is the
|
|
22
|
+
prior art this ADR widens),
|
|
23
|
+
nano-bpm **ADR 0056** (the Nano agentic protocol — the live-steering plane, complementary to this
|
|
24
|
+
durable lane),
|
|
25
|
+
nano-bpm **ADR 0059** (the app-hosted OpenAPI hook surface these graphs are submitted and signalled
|
|
26
|
+
through),
|
|
27
|
+
and issues **#263** (capability edges / publish provenance — the emit-vs-poll dual this ADR resolves),
|
|
28
|
+
**#289** (capability edge wired into dispatch — the `readiness-gate` call-activity pattern reused here),
|
|
29
|
+
**#242** (the pre-plan classifier — the *derive-by-default* front-end deferred here).
|
|
30
|
+
|
|
31
|
+
## Context
|
|
32
|
+
|
|
33
|
+
nano-workforce today runs exactly two shapes of work, each as a **static BPMN process interpreting a
|
|
34
|
+
data graph**: `convergence-loop.bpmn` (one PR) and `plan-fanout.bpmn` (an epic — a
|
|
35
|
+
`RecordPlanTask[] + dependsOn[]` DAG fanned out over multi-instance, with waves, trial-merges, and
|
|
36
|
+
escalations). Both are **specialised to one node shape**: "an agent implements a slice → opens a PR."
|
|
37
|
+
|
|
38
|
+
But real delivery in this ecosystem is a **heterogeneous, cross-repo, partly-human graph**. A concrete
|
|
39
|
+
case from one session:
|
|
40
|
+
|
|
41
|
+
1. PR **#A** must merge in repo 1.
|
|
42
|
+
2. *then* draft PR **#B** in repo 2 can be taken out of draft and merged.
|
|
43
|
+
3. *then* a **human** must do a **manual OTP publish** and **set up OIDC trusted publishing** — no
|
|
44
|
+
automation can cross this step.
|
|
45
|
+
4. *then* PR **#C** in repo 3 can consume the just-published version.
|
|
46
|
+
|
|
47
|
+
This is a dependency graph whose **nodes are a mix of automated tasks, in-flight-PR/merge watches, and
|
|
48
|
+
human actions**, and whose **edges span repos**. nwf has every *primitive* this needs, but no way to
|
|
49
|
+
*compose* them into one arbitrary graph:
|
|
50
|
+
|
|
51
|
+
- **Automated execution** — agent job types + the supervisor/worker fleet.
|
|
52
|
+
- **"Watch the world"** — the `ReadinessProbe` gate (ADR 0001 §2): durable, bounded (timeout →
|
|
53
|
+
escalate), resumable, with `http`/`command`/`npm`/`github-check`/`capability` kinds.
|
|
54
|
+
- **Cross-repo release edges** — capability edges + publish provenance (#263/#274), wired into dispatch
|
|
55
|
+
as a `readiness-gate` call activity (#289).
|
|
56
|
+
- **Human decision points** — user tasks + forms (ADR 0002), answerable by a human **or** an agent,
|
|
57
|
+
with SLA nudges.
|
|
58
|
+
- **Visible phase across a graph** — derived `epic_phase` (#261) + the cockpit/overview.
|
|
59
|
+
|
|
60
|
+
The gap is purely **composition**: a way to feed an *arbitrary* graph of these node kinds — including
|
|
61
|
+
human nodes as **scheduled stops that hand a value forward** — into one runner. ADR 0001 §4 sketched a
|
|
62
|
+
narrow "release DAG" and deferred it; the manual-publish-in-the-middle case is the general form that
|
|
63
|
+
finally justifies building it.
|
|
64
|
+
|
|
65
|
+
The obvious-but-wrong answer is "let an agent design a one-shot process definition per graph" — i.e.
|
|
66
|
+
generate BPMN (or process-builder code) with an LLM and deploy it. That fails on two axes at once:
|
|
67
|
+
**reliability** (an LLM emitting deployable BPMN/builder-code is authoring an artifact that must compile
|
|
68
|
+
and deploy before you learn it's wrong) and **trust** (agents deploying arbitrary executable process
|
|
69
|
+
definitions — or worse, arbitrary code — into your engine is an unbounded surface: any job type, any
|
|
70
|
+
service task, any script). Both objections point at the same fix: **the agent must never author the
|
|
71
|
+
executable artifact.**
|
|
72
|
+
|
|
73
|
+
## Decision
|
|
74
|
+
|
|
75
|
+
### 1. The contract is a validated JSON graph over a *closed* node vocabulary — never an agent-authored executable artifact
|
|
76
|
+
|
|
77
|
+
A delivery graph is submitted as **data**: a JSON DAG whose nodes each name a `kind` from a **fixed
|
|
78
|
+
allowlist** and whose edges name **facts** (below). This JSON — not BPMN, not builder-code — is the
|
|
79
|
+
durable, agent-facing contract, expressed as a **nano-app-schema** type and **published through the
|
|
80
|
+
agent guide** (`GET /app/api/agent`) so a co-designing agent reads exactly what it may build. Ingest
|
|
81
|
+
**validates** against the schema and rejects with **actionable** errors, closing the same
|
|
82
|
+
author→validate→fix loop the capability edges use.
|
|
83
|
+
|
|
84
|
+
This is decisive on both failing axes:
|
|
85
|
+
|
|
86
|
+
- **Reliability** — LLMs emit validated JSON reliably; deployable BPMN/builder-code they do not. The
|
|
87
|
+
whole "co-design → submit" UX depends on the artifact being cheap to validate and safe to produce.
|
|
88
|
+
- **Trust** — a graph can only compose allowlisted `kind`s; it **cannot** express an arbitrary service
|
|
89
|
+
task or run arbitrary code. Safe by construction, exactly as `plan-fanout`'s data model is today.
|
|
90
|
+
|
|
91
|
+
### 2. Node vocabulary — four kinds, each delegating to an engine-native body
|
|
92
|
+
|
|
93
|
+
The closed set (extensible only by a deliberate ADR/PR, never by graph authors):
|
|
94
|
+
|
|
95
|
+
- **`agent`** — a worker executes an agent job type (the existing fan-out body).
|
|
96
|
+
- **`wait`** — a `ReadinessProbe` (ADR 0001 §2), watching an external fact: `github-check`, `npm`,
|
|
97
|
+
`capability` (#263), `http`, `command`, and a new **`pr`/merge-state** kind (draft→ready→merged,
|
|
98
|
+
required checks, mergeable) lifted out of `mergeProtocol.ts` into a first-class probe kind.
|
|
99
|
+
- **`human`** — a scheduled user task + form (§4).
|
|
100
|
+
- **`connector`** — an automated, side-effecting outbound action (the connector I/O surface).
|
|
101
|
+
|
|
102
|
+
Crucially, **execution stays engine-native**: each node kind is a real, already-deployed
|
|
103
|
+
sub-process / call activity (`readiness-gate`, a user task, the implementation task, a connector
|
|
104
|
+
invocation). The graph layer owns **scheduling** (which nodes' edges are satisfied → dispatch), not a
|
|
105
|
+
re-implementation of execution.
|
|
106
|
+
|
|
107
|
+
### 3. Edges are *facts*, discovered — not declared values (extends ADR 0001 §4)
|
|
108
|
+
|
|
109
|
+
Every edge means *"B proceeds once fact X about A is observable."* The fact vocabulary is uniform —
|
|
110
|
+
*PR merged*, *check green*, *version on npm carrying capability C*, *human confirmed done*,
|
|
111
|
+
*connector action acknowledged* — and the satisfying state is **discovered**, never pre-declared
|
|
112
|
+
(ADR 0001 §4's discover-don't-declare, generalized from "capability published" to the whole graph). An
|
|
113
|
+
edge is therefore *always* a `wait`-shaped observation; a plain `dependsOn` between two internal nodes
|
|
114
|
+
is the degenerate "wait for the upstream node's completion fact."
|
|
115
|
+
|
|
116
|
+
### 4. Human nodes are first-class scheduled user tasks that can *emit* a typed fact
|
|
117
|
+
|
|
118
|
+
A `human` node is ADR 0002's user-task+form machinery promoted from **exception** (something broke) to
|
|
119
|
+
**scheduled node** (a planned stop). It surfaces *"now do X"* on the app's own **Tasks** page/inbox, blocks its
|
|
120
|
+
dependents, is **answerable by a human or an agent** (ADR 0046), and is **SLA-bounded** so it nags and
|
|
121
|
+
cannot silently wedge the graph.
|
|
122
|
+
|
|
123
|
+
- **It can hand a value forward.** A human node's form captures a **typed output** that **late-binds
|
|
124
|
+
downstream** — e.g. the manual-publish node emits `resolvedArtifact` (`@nanobpm/urban@0.54.0`), which
|
|
125
|
+
a downstream `capability`/`npm` edge binds and pins. A "click done" gate is the degenerate case that
|
|
126
|
+
emits nothing. This is the **emit-side** of #263's emit-vs-poll dual — and a *human* emitter is the
|
|
127
|
+
same shape as an automated one, which is what unifies human and automated steps in one graph.
|
|
128
|
+
- **Form resolution is specific-else-generic:** (1) an explicit `formKey` on the node; else (2) a form
|
|
129
|
+
**selected** by node category; else (3) a **generic** fallback form that still captures a typed
|
|
130
|
+
emitted fact (so *every* human node can emit downstream even with no bespoke form). Forms are
|
|
131
|
+
preferably **attached at authoring time** (deterministic, visible in preview); a **runtime
|
|
132
|
+
agent-form-router is a gated exception** — it fires only when a node activates with no resolvable
|
|
133
|
+
form, never in every human node's critical path (same "deterministic default, agent judgment as the
|
|
134
|
+
escape hatch" grain as the capability probe's empirical verifier).
|
|
135
|
+
|
|
136
|
+
### 5. A trusted, deterministic compiler turns the JSON into something the engine runs — exposed as an agent tool
|
|
137
|
+
|
|
138
|
+
The *only* thing that turns graph-data into an executable is a **deterministic, human-written, tested
|
|
139
|
+
compiler**. It is exposed to the co-designing agent as a **tool** — `validate/compile(JSON) →
|
|
140
|
+
{ ok, diagram, errors }` — which doubles as the **preview/dry-run**: the agent iterates JSON → tool →
|
|
141
|
+
fix, and the rendered graph is what a human approves before anything runs. Because the compiler only
|
|
142
|
+
ever instantiates allowlisted node kinds, it inherits Decision 1's trust bound.
|
|
143
|
+
|
|
144
|
+
### 6. Execution strategy is swappable behind the JSON contract; the axis is static-vs-dynamic topology
|
|
145
|
+
|
|
146
|
+
The compiler MAY target either execution strategy, and because the JSON is the contract, the choice is
|
|
147
|
+
an implementation detail the agent never sees:
|
|
148
|
+
|
|
149
|
+
- **Compile-to-native** — emit a **one-shot native BPMN definition** (native parallel/event gateways do
|
|
150
|
+
the scheduling; you get a real diagram for free) and deploy it. **Best when the graph is known at
|
|
151
|
+
authoring time.**
|
|
152
|
+
- **Interpret** — feed the JSON to one generic deployed process that evaluates the ready-set at
|
|
153
|
+
runtime. **Best when the graph is discovered or mutated at runtime**, and the strategy that makes
|
|
154
|
+
**mid-flight amendment** tractable (edit a variable, not migrate a deployed definition).
|
|
155
|
+
|
|
156
|
+
The discriminator is **author-time-static vs runtime-dynamic topology**. Delivery runbooks (the
|
|
157
|
+
motivating case — a *pre-known* release choreography) are static → **start with compile-to-native**.
|
|
158
|
+
`plan-fanout` stays **interpret** (its graph is agent-discovered and its waves adapt to results). The
|
|
159
|
+
shared JSON contract means either can be swapped in later without touching the agent UX.
|
|
160
|
+
|
|
161
|
+
### 7. Submission is propose → preview → approve → dispatch, idempotent, over the self-describing endpoint
|
|
162
|
+
|
|
163
|
+
Graphs are submitted exactly as epics are today — via a **new (proposed)** `POST
|
|
164
|
+
/actions/start/delivery-graph` endpoint (paths are relative to the agent guide's `__BASE__` prefix,
|
|
165
|
+
matching the guide's style) with the JSON body, discovered via the agent guide (which already
|
|
166
|
+
documents `POST /actions/start/plan-fanout` and `POST /actions/complete-user-task`). This endpoint
|
|
167
|
+
does not yet exist in `openapi.yaml` — it is introduced by this Proposed ADR. Three ways in, **one validated contract**: agent-ergonomic
|
|
168
|
+
(co-design → POST), raw REST, and a **UI JSON-paste fallback**. Because these graphs *merge PRs and
|
|
169
|
+
publish packages*, submission **defaults to propose-preview-approve** — the resolved graph (what it will
|
|
170
|
+
do, where it stops for humans) is rendered and a human approves before dispatch; that approval is itself
|
|
171
|
+
just the first human node. Submission carries an **idempotency key** so a re-POST cannot double-launch,
|
|
172
|
+
and every **side-effecting node (`connector`, merge, publish) carries a dedupe key** and tolerates
|
|
173
|
+
at-least-once execution (mirroring the release workflow's `npx semantic-release`
|
|
174
|
+
"skip already-published" discipline — `.github/workflows/release.yml`) so a
|
|
175
|
+
resume cannot double-fire.
|
|
176
|
+
|
|
177
|
+
## Consequences
|
|
178
|
+
|
|
179
|
+
- nwf gains a **generic delivery-graph runner** that composes its existing primitives; the motivating
|
|
180
|
+
human-in-the-middle cross-repo case (merge → un-draft → manual publish+OIDC → consume) becomes a
|
|
181
|
+
single submitted graph rather than hand-carried human coordination.
|
|
182
|
+
- **#263's deferred "publish node emits"** is subsumed: an emitting node (human *or* automated) that
|
|
183
|
+
hands a version to a downstream edge is Decision 4 + Decision 3.
|
|
184
|
+
- The **connector I/O surface** finds its orchestration home: a connector is just an automated emitting
|
|
185
|
+
node kind (Decision 2).
|
|
186
|
+
- New surface to own: the JSON graph schema, the deterministic compiler, and the `pr`/merge-state probe
|
|
187
|
+
kind. All bounded — the schema is validated at ingest, the compiler is deterministic and tested, and
|
|
188
|
+
every node inherits the ADR 0001 §2 timeout+escalation bound, so a malformed or hanging graph cannot
|
|
189
|
+
stall silently.
|
|
190
|
+
- The **agent never deploys executable artifacts**; the trust boundary is the closed vocabulary + the
|
|
191
|
+
human-written compiler, not agent output.
|
|
192
|
+
- Amendment is cheap **only** under the interpret strategy; compile-to-native graphs are amended by
|
|
193
|
+
cancel + resubmit (an accepted cost for pre-known runbooks).
|
|
194
|
+
|
|
195
|
+
## Non-goals / deferred
|
|
196
|
+
|
|
197
|
+
- **Derive-by-default graph authoring.** Populating edges automatically from intake (the #242
|
|
198
|
+
classifier facet, or PR-dependency inference) — this ADR ships the **declared** graph + the runner;
|
|
199
|
+
derivation is sugar layered on later.
|
|
200
|
+
- **Runtime-dynamic delivery graphs.** The first cut is compile-to-native for **static** runbooks; the
|
|
201
|
+
interpret strategy for graphs whose shape changes at runtime is kept behind the same contract but not
|
|
202
|
+
built now.
|
|
203
|
+
- **Mid-flight amendment of a running graph** beyond cancel+resubmit.
|
|
204
|
+
- **A second workflow engine.** Scheduling composes the nano engine's native constructs (or a thin
|
|
205
|
+
ready-set loop); execution is always engine-native sub-processes. Do not re-implement durability,
|
|
206
|
+
timers, or receive-tasks.
|
|
207
|
+
- **Non-npm emit facts** (OCI/github-release) and behavioural edges beyond the `command` escape hatch —
|
|
208
|
+
added when a real case lands.
|
|
209
|
+
|
|
210
|
+
## Open questions
|
|
211
|
+
|
|
212
|
+
- **Compiler target for the first cut** — confirm compile-to-native (diagram + native scheduling) vs a
|
|
213
|
+
minimal interpreter reusing `plan-fanout` patterns; the ADR leans native for static runbooks.
|
|
214
|
+
- **`pr`/merge-state probe kind boundary** — how much of `mergeProtocol.ts` (required checks, mergeable,
|
|
215
|
+
base guards) becomes probe-matcher config vs stays in the merge-loop node body.
|
|
216
|
+
- **Emitted-fact typing** — the schema for a node's typed output (version string, URL, artifact id) and
|
|
217
|
+
how downstream edges reference it (`from: <nodeId>.<fact>`), so binds are validated not stringly.
|
|
218
|
+
- **Definition lifecycle under compile-to-native** — naming, versioning, and GC of one-shot deployed
|
|
219
|
+
process definitions so the engine doesn't accumulate them.
|
|
220
|
+
- **Approval granularity** — one approval of the whole graph at submit, vs re-approval when a human node
|
|
221
|
+
amends the un-started tail (if amendment is ever allowed).
|
package/openapi.yaml
CHANGED
|
@@ -821,6 +821,38 @@ components:
|
|
|
821
821
|
instructions:
|
|
822
822
|
type: string
|
|
823
823
|
description: The full operator guide as markdown.
|
|
824
|
+
AgentSkill:
|
|
825
|
+
type: object
|
|
826
|
+
description: The portable Nano Workforce operator skill (SKILL.md) — a thin bootstrap an agent
|
|
827
|
+
runtime loads on demand. It resolves which instance to drive and then fetches the live
|
|
828
|
+
operator guide. The `skill` markdown has any embedded example keyed to this instance's
|
|
829
|
+
`baseUrl`.
|
|
830
|
+
additionalProperties: false
|
|
831
|
+
required:
|
|
832
|
+
- format
|
|
833
|
+
- appVersion
|
|
834
|
+
- generatedAt
|
|
835
|
+
- baseUrl
|
|
836
|
+
- skill
|
|
837
|
+
properties:
|
|
838
|
+
format:
|
|
839
|
+
type: string
|
|
840
|
+
description: The `skill` media format. Always "markdown".
|
|
841
|
+
enum:
|
|
842
|
+
- markdown
|
|
843
|
+
appVersion:
|
|
844
|
+
type: string
|
|
845
|
+
nullable: true
|
|
846
|
+
description: The running app version this skill was served from (null when unreadable).
|
|
847
|
+
generatedAt:
|
|
848
|
+
type: string
|
|
849
|
+
description: When this response was rendered (ISO-8601).
|
|
850
|
+
baseUrl:
|
|
851
|
+
type: string
|
|
852
|
+
description: The app control-API base the skill was fetched from (e.g. https://host/app/api).
|
|
853
|
+
skill:
|
|
854
|
+
type: string
|
|
855
|
+
description: The full operator skill (SKILL.md) as markdown, including its YAML frontmatter.
|
|
824
856
|
SubmitResult:
|
|
825
857
|
type: object
|
|
826
858
|
required:
|
|
@@ -1861,6 +1893,28 @@ paths:
|
|
|
1861
1893
|
application/json:
|
|
1862
1894
|
schema:
|
|
1863
1895
|
$ref: "#/components/schemas/ErrorBody"
|
|
1896
|
+
/agent/skill:
|
|
1897
|
+
get:
|
|
1898
|
+
operationId: getAgentSkill
|
|
1899
|
+
summary: The portable operator skill (markdown SKILL.md) an agent runtime loads on demand — a
|
|
1900
|
+
thin bootstrap that resolves which instance to drive, then fetches the live operator guide
|
|
1901
|
+
(GET /agent). Surfaced from the Overview tab's "Agent Instructions" prompt.
|
|
1902
|
+
security:
|
|
1903
|
+
- hookSecret: []
|
|
1904
|
+
- {}
|
|
1905
|
+
responses:
|
|
1906
|
+
"200":
|
|
1907
|
+
description: The operator skill, with any example keyed to this instance.
|
|
1908
|
+
content:
|
|
1909
|
+
application/json:
|
|
1910
|
+
schema:
|
|
1911
|
+
$ref: "#/components/schemas/AgentSkill"
|
|
1912
|
+
"401":
|
|
1913
|
+
description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
|
|
1914
|
+
content:
|
|
1915
|
+
application/json:
|
|
1916
|
+
schema:
|
|
1917
|
+
$ref: "#/components/schemas/ErrorBody"
|
|
1864
1918
|
/actions/start/convergence-loop:
|
|
1865
1919
|
post:
|
|
1866
1920
|
operationId: startConvergenceLoop
|
|
@@ -13,32 +13,18 @@
|
|
|
13
13
|
// NANO_PR_WEBHOOK_SECRET is set (the runtime does not enforce OpenAPI `security`).
|
|
14
14
|
|
|
15
15
|
import { renderAgentGuide, resolveEngineBase } from "../app/agentGuide.ts";
|
|
16
|
+
import { resolveApiBase } from "../app/resolveApiBase.ts";
|
|
16
17
|
import { buildVersionInfo, envVar } from "../app/version.ts";
|
|
17
18
|
import { defineOperation } from "../nano-generated/operations.ts";
|
|
18
19
|
|
|
19
20
|
const SECRET = envVar("NANO_PR_WEBHOOK_SECRET") ?? "";
|
|
20
21
|
|
|
21
|
-
/**
|
|
22
|
-
* Reconstruct the app control-API base the caller reached us on (e.g. "https://host/app/api"), so
|
|
23
|
-
* the guide's example commands are copy-pasteable. Honour reverse-proxy forwarding headers; fall
|
|
24
|
-
* back to a localhost default when the Host header is absent (e.g. a raw unit-test request).
|
|
25
|
-
*/
|
|
26
|
-
function resolveApiBase(req: { path: string; headers: Headers }): string {
|
|
27
|
-
const rawProto = (req.headers.get("x-forwarded-proto") ?? "http").split(",")[0].trim().toLowerCase();
|
|
28
|
-
// x-forwarded-proto is user-controlled behind some proxies; only trust http/https.
|
|
29
|
-
const proto = rawProto === "http" || rawProto === "https" ? rawProto : "http";
|
|
30
|
-
const host = (req.headers.get("x-forwarded-host") ?? req.headers.get("host") ?? "").split(",")[0].trim();
|
|
31
|
-
// The op is mounted at "<base>/agent"; strip the trailing segment to recover the base path.
|
|
32
|
-
const basePath = req.path.replace(/\/agent\/?$/, "") || "/app/api";
|
|
33
|
-
return host ? `${proto}://${host}${basePath}` : `http://localhost:3000${basePath}`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
22
|
export default defineOperation("getAgentInstructions", ({ req }, app) => {
|
|
37
23
|
if (SECRET && req.headers.get("x-hook-secret") !== SECRET) {
|
|
38
24
|
app.log.warn("getAgentInstructions rejected: missing/invalid shared secret");
|
|
39
25
|
return { status: 401, body: { error: "unauthorized" } };
|
|
40
26
|
}
|
|
41
|
-
const baseUrl = resolveApiBase(req);
|
|
27
|
+
const baseUrl = resolveApiBase(req, "agent");
|
|
42
28
|
return {
|
|
43
29
|
status: 200,
|
|
44
30
|
body: {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Tests for GET /app/api/agent/skill → operation `getAgentSkill` (ADR 0058 OpenAPI surface).
|
|
2
|
+
// The SKILL.md markdown is served as the `skill` field with any example keyed to the request's
|
|
3
|
+
// control-API base. Mirrors the getAgentInstructions test's request shape and shared-secret guard.
|
|
4
|
+
import { test } from "node:test";
|
|
5
|
+
import { assert, assertEquals } from "#test-assert";
|
|
6
|
+
import type { AppApi } from "@nanobpm/urban";
|
|
7
|
+
import { noopLog } from "../test/log.ts";
|
|
8
|
+
import handler from "./getAgentSkill.ts";
|
|
9
|
+
|
|
10
|
+
const app = { log: noopLog() } as any as AppApi;
|
|
11
|
+
|
|
12
|
+
function input(headers: Record<string, string> = {}, path = "/app/api/agent/skill") {
|
|
13
|
+
return {
|
|
14
|
+
req: {
|
|
15
|
+
method: "GET",
|
|
16
|
+
path,
|
|
17
|
+
query: new URLSearchParams(),
|
|
18
|
+
headers: new Headers(headers),
|
|
19
|
+
text: async () => "",
|
|
20
|
+
} as any,
|
|
21
|
+
params: {},
|
|
22
|
+
query: {},
|
|
23
|
+
body: undefined,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test("returns 200 with the SKILL.md markdown and metadata", async () => {
|
|
28
|
+
const r = (await handler(input(), app)) as any;
|
|
29
|
+
assertEquals(r.status, 200);
|
|
30
|
+
assertEquals(r.body.format, "markdown");
|
|
31
|
+
assert("appVersion" in r.body); // nullable, but always present
|
|
32
|
+
assert(typeof r.body.generatedAt === "string" && r.body.generatedAt.length > 0);
|
|
33
|
+
assert(typeof r.body.baseUrl === "string" && r.body.baseUrl.length > 0);
|
|
34
|
+
assert(typeof r.body.skill === "string" && r.body.skill.length > 200);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("the skill is the portable bootstrap: frontmatter + fetch-the-live-guide", async () => {
|
|
38
|
+
const md = ((await handler(input(), app)) as any).body.skill as string;
|
|
39
|
+
assert(md.includes("name: nano-workforce"), "carries the skill frontmatter");
|
|
40
|
+
assert(md.includes("/agent"), "bootstraps by fetching the live operator guide");
|
|
41
|
+
assert(md.includes("Confirm which instance") || md.includes("confirm"), "tells the agent to confirm the target instance");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("baseUrl is keyed to the request's control-API base; no placeholder leaks", async () => {
|
|
45
|
+
const forwarded = input({ host: "wf.example.com", "x-forwarded-proto": "https" });
|
|
46
|
+
const body = (await handler(forwarded, app)) as any;
|
|
47
|
+
assertEquals(body.body.baseUrl, "https://wf.example.com/app/api");
|
|
48
|
+
assert(!body.body.skill.includes("__BASE__"), "no unsubstituted __BASE__ placeholder");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("x-forwarded-proto is restricted to http/https", async () => {
|
|
52
|
+
const spoofed = input({ host: "wf.example.com", "x-forwarded-proto": "javascript" });
|
|
53
|
+
const body = (await handler(spoofed, app)) as any;
|
|
54
|
+
assertEquals(body.body.baseUrl, "http://wf.example.com/app/api", "unsafe scheme falls back to http");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("shared-secret guard rejects a missing/wrong secret when configured", async () => {
|
|
58
|
+
const prev = process.env["NANO_PR_WEBHOOK_SECRET"];
|
|
59
|
+
process.env["NANO_PR_WEBHOOK_SECRET"] = "s3cr3t";
|
|
60
|
+
try {
|
|
61
|
+
// SECRET is bound at import time, so import a cache-busted copy to observe the guard.
|
|
62
|
+
const mod = await import(`./getAgentSkill.ts?guard=${Date.now()}`);
|
|
63
|
+
const guarded = mod.default as typeof handler;
|
|
64
|
+
const bad = (await guarded(input(), app)) as any;
|
|
65
|
+
assertEquals(bad.status, 401);
|
|
66
|
+
const ok = (await guarded(input({ "x-hook-secret": "s3cr3t" }), app)) as any;
|
|
67
|
+
assertEquals(ok.status, 200);
|
|
68
|
+
} finally {
|
|
69
|
+
if (prev === undefined) delete process.env["NANO_PR_WEBHOOK_SECRET"];
|
|
70
|
+
else process.env["NANO_PR_WEBHOOK_SECRET"] = prev;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// GET /app/api/agent/skill → operationId `getAgentSkill` (ADR 0058/0059 OpenAPI surface, base
|
|
2
|
+
// /app/api). Serves the portable operator *skill* (SKILL.md) an agent runtime loads on demand: a
|
|
3
|
+
// thin bootstrap that resolves which instance to drive and then fetches the live operator guide
|
|
4
|
+
// (GET /app/api/agent). Companion to getAgentInstructions.
|
|
5
|
+
//
|
|
6
|
+
// The runtime serializes an operation body as JSON, so the markdown skill is returned as the
|
|
7
|
+
// `skill` string field (alongside the app version + the base URL it was fetched from). Any
|
|
8
|
+
// `__BASE__` example is rewritten to THIS instance's control-API base (derived from the request).
|
|
9
|
+
//
|
|
10
|
+
// Read-only. The optional shared-secret guard mirrors /version and /agent: enforced HERE only when
|
|
11
|
+
// NANO_PR_WEBHOOK_SECRET is set (the runtime does not enforce OpenAPI `security`).
|
|
12
|
+
|
|
13
|
+
import { renderAgentSkill } from "../app/agentSkill.ts";
|
|
14
|
+
import { resolveApiBase } from "../app/resolveApiBase.ts";
|
|
15
|
+
import { buildVersionInfo, envVar } from "../app/version.ts";
|
|
16
|
+
import { defineOperation } from "../nano-generated/operations.ts";
|
|
17
|
+
|
|
18
|
+
const SECRET = envVar("NANO_PR_WEBHOOK_SECRET") ?? "";
|
|
19
|
+
|
|
20
|
+
export default defineOperation("getAgentSkill", ({ req }, app) => {
|
|
21
|
+
if (SECRET && req.headers.get("x-hook-secret") !== SECRET) {
|
|
22
|
+
app.log.warn("getAgentSkill rejected: missing/invalid shared secret");
|
|
23
|
+
return { status: 401, body: { error: "unauthorized" } };
|
|
24
|
+
}
|
|
25
|
+
const baseUrl = resolveApiBase(req, "agent/skill");
|
|
26
|
+
return {
|
|
27
|
+
status: 200,
|
|
28
|
+
body: {
|
|
29
|
+
format: "markdown",
|
|
30
|
+
appVersion: buildVersionInfo().version,
|
|
31
|
+
generatedAt: new Date().toISOString(),
|
|
32
|
+
baseUrl,
|
|
33
|
+
skill: renderAgentSkill(baseUrl),
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.109.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",
|
package/pages/home.page.json
CHANGED
|
@@ -73,20 +73,6 @@
|
|
|
73
73
|
"variant": "sub"
|
|
74
74
|
}
|
|
75
75
|
},
|
|
76
|
-
{
|
|
77
|
-
"type": "button",
|
|
78
|
-
"id": "agent-instructions",
|
|
79
|
-
"props": {
|
|
80
|
-
"label": "\ud83e\udd16 Agent Instructions",
|
|
81
|
-
"variant": "ghost",
|
|
82
|
-
"modal": {
|
|
83
|
-
"title": "Point your agent at Nano Workforce",
|
|
84
|
-
"description": "Copy this prompt and paste it into your coding agent (Copilot, Claude, etc.). It tells the agent to read this workforce's live operator guide, then help you drive and debug it.",
|
|
85
|
-
"copyLabel": "Copy prompt",
|
|
86
|
-
"copyText": "You are helping me operate a running Nano Workforce instance \u2014 a durable orchestration app that drives pull requests to review convergence against an automated reviewer, merges them, and can take a whole issue and plan \u2192 implement \u2192 converge it across a fleet of coding agents.\n\nFirst, fetch and read its live operator guide. It tells you how to submit PRs and epics for convergence (including whether to go all the way to merge or stop at review consensus), how to find engine instances and relate them to PRs via the Nano/Camunda-8 REST API, how to inspect the models and prompts, and how to help me untangle escalations:\n\n curl -sS {{appBase}}app/api/agent\n\nIf this instance is secured with a shared secret (NANO_PR_WEBHOOK_SECRET), add its value as an x-hook-secret header, otherwise the request returns 401:\n\n curl -sS -H \"x-hook-secret: <secret>\" {{appBase}}app/api/agent\n\nThen follow that guide to help me drive and debug this workforce. If you find a bug or a stuck process, the guide explains how to raise an issue or a PR against nanobpm/nano-workforce."
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
},
|
|
90
76
|
{
|
|
91
77
|
"type": "actionForm",
|
|
92
78
|
"id": "submit",
|
package/pages/overview.page.json
CHANGED
|
@@ -70,6 +70,20 @@
|
|
|
70
70
|
"variant": "sub"
|
|
71
71
|
}
|
|
72
72
|
},
|
|
73
|
+
{
|
|
74
|
+
"type": "button",
|
|
75
|
+
"id": "agent-instructions",
|
|
76
|
+
"props": {
|
|
77
|
+
"label": "\ud83e\udd16 Agent Instructions",
|
|
78
|
+
"variant": "ghost",
|
|
79
|
+
"modal": {
|
|
80
|
+
"title": "Point your agent at Nano Workforce",
|
|
81
|
+
"description": "Copy this prompt and paste it into your coding agent (Copilot, Claude, etc.). It tells the agent to load this workforce's operator skill from this instance, then help you drive and debug it.",
|
|
82
|
+
"copyLabel": "Copy prompt",
|
|
83
|
+
"copyText": "Load the Nano Workforce operator skill from this running instance and then help me drive and debug my workforce \u2014 a durable orchestration app that drives pull requests to review convergence against an automated reviewer, merges them, and can take a whole issue and plan \u2192 implement \u2192 converge it across a fleet of coding agents.\n\nThis is the instance to operate; its control-API base is {{appBase}}app/api. Fetch the skill and follow it (the response is JSON with a `skill` markdown field):\n\n curl -sS {{appBase}}app/api/agent/skill\n\nIf this instance is secured with a shared secret (NANO_PR_WEBHOOK_SECRET), add its value as an x-hook-secret header, otherwise the request returns 401:\n\n curl -sS -H \"x-hook-secret: <secret>\" {{appBase}}app/api/agent/skill\n\nThe skill is a thin bootstrap: it has you fetch this instance's live operator guide (at {{appBase}}app/api/agent) and then drive the workforce \u2014 submit PRs and epics for convergence, answer escalations, and debug stuck processes. If you find a bug or a stuck process, the guide explains how to raise an issue or a PR against nanobpm/nano-workforce."
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
73
87
|
{
|
|
74
88
|
"type": "dataGrid",
|
|
75
89
|
"id": "overview-prs",
|
package/skills/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Agent skills
|
|
2
|
+
|
|
3
|
+
Portable agent skills that ship with Nano Workforce. A skill is a `SKILL.md` with
|
|
4
|
+
YAML frontmatter (`name`, `description`) that an agent runtime (Copilot CLI, Claude)
|
|
5
|
+
loads on demand when its `description` matches the task.
|
|
6
|
+
|
|
7
|
+
## `nano-workforce`
|
|
8
|
+
|
|
9
|
+
A **thin bootstrap** that teaches any agent to operate a running Nano Workforce
|
|
10
|
+
instance: it resolves the instance base URL and fetches the instance's *live*
|
|
11
|
+
operator guide (`GET /app/api/agent`), then follows it. It deliberately holds no
|
|
12
|
+
endpoint detail of its own — the live, version-matched guide is the source of truth.
|
|
13
|
+
|
|
14
|
+
### Install
|
|
15
|
+
|
|
16
|
+
Copy or symlink the skill into your agent's skills directory. For Copilot CLI:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# symlink so it tracks this repo
|
|
20
|
+
ln -s "$(pwd)/skills/nano-workforce" ~/.copilot/skills/nano-workforce
|
|
21
|
+
# …or copy it
|
|
22
|
+
cp -r skills/nano-workforce ~/.copilot/skills/nano-workforce
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then, from an agent session against your instance:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
Load the nano-workforce skill and drive my workforce.
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Set `NANO_WORKFORCE_URL` (and `NANO_PR_WEBHOOK_SECRET`, if your instance guards the
|
|
32
|
+
agent endpoints) so the bootstrap can reach your instance without prompting.
|
|
33
|
+
|
|
34
|
+
### Multiple instances
|
|
35
|
+
|
|
36
|
+
If you run more than one instance (a local copy, one on the LAN, a tunnel when
|
|
37
|
+
you're off-LAN), register them by name so the skill can offer a choice and probe
|
|
38
|
+
which is live. Set `NANO_WORKFORCE_INSTANCES`, or write
|
|
39
|
+
`~/.config/nano-workforce/instances.json`:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"local": "http://localhost:3000/app/api",
|
|
44
|
+
"merlin": "http://merlin.local:3000/app/api",
|
|
45
|
+
"remote": "https://<subdomain>.ngrok.app/app/api"
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then just say *"drive merlin"* — or let the skill probe reachability and ask which
|
|
50
|
+
to use (off the LAN, `merlin.local` won't resolve, so it steers you to `remote`).
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nano-workforce
|
|
3
|
+
description: Drive and debug a running Nano Workforce instance — submit PRs for review convergence, submit issues/epics for plan→implement→converge, submit agent-authored delivery graphs (ADR 0005), answer escalations, and unstick stuck instances. Use when the user asks to operate, drive, submit work to, or debug their Nano Workforce.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Nano Workforce operator skill
|
|
7
|
+
|
|
8
|
+
Nano Workforce (nwf) is a durable orchestration app that drives pull requests to
|
|
9
|
+
**review convergence** and merges them, takes whole issues and **plans →
|
|
10
|
+
implements → converges** them across a fleet of coding agents, and runs
|
|
11
|
+
**agent-authored delivery graphs** (heterogeneous cross-repo, human-in-the-loop
|
|
12
|
+
DAGs — ADR 0005).
|
|
13
|
+
|
|
14
|
+
**This skill is a thin bootstrap by design.** It does not describe the endpoints.
|
|
15
|
+
Every running nwf instance serves its own operator guide, *live*, keyed to that
|
|
16
|
+
instance's URLs and matched to its deployed version. Your job is to fetch that
|
|
17
|
+
guide and follow it — never to work from a cached copy, which drifts across
|
|
18
|
+
versions and instances.
|
|
19
|
+
|
|
20
|
+
## 1. Confirm which instance you are driving — always
|
|
21
|
+
|
|
22
|
+
A user typically runs **several** Nano Workforce instances — e.g. a local dev copy,
|
|
23
|
+
one on the LAN (`http://merlin.local:3000/app/api`), and a public tunnel
|
|
24
|
+
(an ngrok URL) when off the LAN. Every action here is **side-effecting** —
|
|
25
|
+
submitting work, answering escalations, merging PRs — so targeting the wrong
|
|
26
|
+
instance is a real mistake, not a harmless one. **Never silently default to a
|
|
27
|
+
base URL.**
|
|
28
|
+
|
|
29
|
+
### Sources of candidate instances
|
|
30
|
+
|
|
31
|
+
Gather candidates from, in order:
|
|
32
|
+
|
|
33
|
+
1. **A named-instance registry** the user maintains — first of these that exists:
|
|
34
|
+
`$NANO_WORKFORCE_INSTANCES` (JSON object of `name → base URL`), or
|
|
35
|
+
`~/.config/nano-workforce/instances.json` (same shape). Example:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{ "local": "http://localhost:3000/app/api",
|
|
39
|
+
"merlin": "http://merlin.local:3000/app/api",
|
|
40
|
+
"remote": "https://<subdomain>.ngrok.app/app/api" }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
2. `$NANO_WORKFORCE_URL`, if set (a single default; accept as-is, append `/app/api`
|
|
44
|
+
only if it is a bare origin).
|
|
45
|
+
3. Any URL the user names in the conversation.
|
|
46
|
+
4. Local fallback: `http://localhost:3000/app/api` (port `PR_REVIEW_PORT`, default `3000`).
|
|
47
|
+
|
|
48
|
+
### Choosing
|
|
49
|
+
|
|
50
|
+
- If the user **named an instance** (by name from the registry, or by URL), use it.
|
|
51
|
+
- Otherwise, **probe the candidates for reachability** and ask the user which to
|
|
52
|
+
use, offering the candidates as choices and marking which are live. Reachability
|
|
53
|
+
disambiguates the common case — off the LAN, `merlin.local` won't resolve, so the
|
|
54
|
+
tunnel instance is the live one:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# For each candidate base, a fast liveness + identity check:
|
|
58
|
+
curl -sS --max-time 3 \
|
|
59
|
+
${NANO_PR_WEBHOOK_SECRET:+-H "x-hook-secret: $NANO_PR_WEBHOOK_SECRET"} \
|
|
60
|
+
"$BASE/version" | jq '{appVersion, gitSha, uptimeSeconds}'
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- Only skip the question when exactly **one** candidate exists and is reachable —
|
|
64
|
+
and even then, **name the instance you're about to drive** before acting.
|
|
65
|
+
|
|
66
|
+
Some instances guard the agent endpoints with a shared secret. If the user has
|
|
67
|
+
`$NANO_PR_WEBHOOK_SECRET` set, send it as `x-hook-secret` on every request below.
|
|
68
|
+
|
|
69
|
+
## 2. Fetch the live guide — this is your real playbook
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
curl -sS ${NANO_PR_WEBHOOK_SECRET:+-H "x-hook-secret: $NANO_PR_WEBHOOK_SECRET"} \
|
|
73
|
+
"$BASE/agent" | jq -r '.instructions'
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`GET /app/api/agent` (`getAgentInstructions`) returns
|
|
77
|
+
`{ format, appVersion, generatedAt, baseUrl, engineBase, instructions }`. The
|
|
78
|
+
`instructions` markdown is the authoritative, version-matched operator guide, with
|
|
79
|
+
every example already keyed to this instance's `baseUrl`/`engineBase`. Read it in
|
|
80
|
+
full and follow it for everything that follows — orientation, submitting work,
|
|
81
|
+
answering escalations, and debugging.
|
|
82
|
+
|
|
83
|
+
**Always re-fetch the guide at the start of a session.** It is the source of truth;
|
|
84
|
+
this skill only tells you how to find it.
|
|
85
|
+
|
|
86
|
+
## 3. Orient before acting
|
|
87
|
+
|
|
88
|
+
The guide's first steps confirm what is live and what is in flight:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
curl -sS "$BASE/version" | jq # app/urban version, git sha, uptime
|
|
92
|
+
curl -sS "$BASE/status" | jq # every PR/instance in flight + open escalations
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`/status` is the primary situational-awareness endpoint — check it before you
|
|
96
|
+
submit or unstick anything.
|
|
97
|
+
|
|
98
|
+
## 4. What you can drive (all detailed in the live guide)
|
|
99
|
+
|
|
100
|
+
- **Submit a PR** for review convergence — `POST $BASE/actions/start/convergence-loop`.
|
|
101
|
+
- **Submit an issue/epic** for plan → implement → converge across the fleet —
|
|
102
|
+
`POST $BASE/actions/start/plan-fanout`.
|
|
103
|
+
- **Submit a delivery graph** (ADR 0005) — propose → preview → approve → dispatch.
|
|
104
|
+
Compile/preview is a pure, side-effect-free tool; only the start door dispatches.
|
|
105
|
+
The live guide documents the exact operations once the instance exposes them.
|
|
106
|
+
- **Answer an escalation** (a durable user task the workforce parked on) —
|
|
107
|
+
`POST $BASE/actions/complete-user-task`, or the agent hook
|
|
108
|
+
`POST $BASE/hooks/agent-complete` (`agentCompleteEscalation`).
|
|
109
|
+
- **Debug**: relate an in-flight PR to its engine process instance via `processKey`
|
|
110
|
+
from `/status`, then use the engine REST base (`engineBase` from the guide) to
|
|
111
|
+
inspect and unstick it.
|
|
112
|
+
|
|
113
|
+
## Principles
|
|
114
|
+
|
|
115
|
+
- **Discover, don't declare.** Prefer the live guide and live `/status` over any
|
|
116
|
+
assumption baked into this file. If this skill and the guide disagree, the guide
|
|
117
|
+
wins.
|
|
118
|
+
- **Confirm the target instance.** Never run a side-effecting call against an
|
|
119
|
+
assumed base URL. Know — and when ambiguous, ask — which instance you're driving.
|
|
120
|
+
- **Preview before dispatch.** For delivery graphs and any bulk action, use the
|
|
121
|
+
pure preview/validate path first and show the user the plan before the
|
|
122
|
+
side-effecting start call.
|
|
123
|
+
- **Idempotency.** Submissions carry dedupe keys; re-submitting the same work must
|
|
124
|
+
not double-dispatch. The guide documents the keys — honour them.
|
|
125
|
+
- **Escalations are for humans.** When the workforce parks on a human node, surface
|
|
126
|
+
it to the user with options; don't silently auto-answer design/product decisions.
|