@nanobpm/nano-workforce 0.159.1 → 0.161.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/openapi.yaml CHANGED
@@ -35,6 +35,37 @@ components:
35
35
  properties:
36
36
  error:
37
37
  type: string
38
+ ValidationError:
39
+ description: >-
40
+ The uniform validation-error contract: a human-readable `error` plus a path-qualified
41
+ `issues[]` naming each offending input. Returned by endpoints that reject a bad query/body
42
+ param (e.g. `GET /agent/guide` with an unknown `section` id).
43
+ type: object
44
+ additionalProperties: false
45
+ required:
46
+ - error
47
+ - issues
48
+ properties:
49
+ error:
50
+ type: string
51
+ description: A human-readable summary of the rejection.
52
+ issues:
53
+ type: array
54
+ minItems: 1
55
+ items:
56
+ type: object
57
+ additionalProperties: false
58
+ required:
59
+ - path
60
+ - message
61
+ properties:
62
+ path:
63
+ type: string
64
+ description: JSON-path pointer at the offending input (e.g. `section`).
65
+ message:
66
+ type: string
67
+ description: Human-actionable description of the failure.
68
+ description: The path-qualified `{ path, message }` failures (at least one).
38
69
  ActivePr:
39
70
  type: object
40
71
  description: A tracked PR that is not in a terminal (converged/abandoned) state.
@@ -881,6 +912,82 @@ components:
881
912
  skill:
882
913
  type: string
883
914
  description: The full operator skill (SKILL.md) as markdown, including its YAML frontmatter.
915
+ AgentGuideResponse:
916
+ type: object
917
+ description: The addressable operator-guide response served by `getAgentGuide`. Two shapes,
918
+ discriminated by `kind`. `kind:"toc"` (no `section` argument) carries `sections` — the compact
919
+ table of contents, one entry per stable section id. `kind:"section"` (a `section` id given)
920
+ carries `section` — that one section's markdown, with its examples keyed to this instance.
921
+ additionalProperties: false
922
+ required:
923
+ - kind
924
+ - appVersion
925
+ - generatedAt
926
+ - baseUrl
927
+ properties:
928
+ kind:
929
+ type: string
930
+ description: '"toc" when listing sections (no `section` argument); "section" when returning one.'
931
+ enum:
932
+ - toc
933
+ - section
934
+ appVersion:
935
+ type: string
936
+ nullable: true
937
+ description: The running app version this guide matches (null when unreadable).
938
+ generatedAt:
939
+ type: string
940
+ description: When this response was rendered (ISO-8601).
941
+ baseUrl:
942
+ type: string
943
+ description: The app control-API base the examples target (e.g. https://host/app/api).
944
+ engineBase:
945
+ type: string
946
+ description: The engine's Camunda-8 v2 REST base this app talks to (present on a section response).
947
+ sections:
948
+ type: array
949
+ description: The table of contents — present when `kind` is "toc". One entry per addressable section.
950
+ items:
951
+ type: object
952
+ additionalProperties: false
953
+ required:
954
+ - id
955
+ - title
956
+ - summary
957
+ properties:
958
+ id:
959
+ type: string
960
+ description: The stable section id to pass back as `getAgentGuide(section)`.
961
+ title:
962
+ type: string
963
+ description: The section's heading text (e.g. "9. Author and run a delivery graph (ADR 0005)").
964
+ summary:
965
+ type: string
966
+ description: A one-line summary of what the section covers.
967
+ section:
968
+ type: object
969
+ description: The requested section — present when `kind` is "section".
970
+ additionalProperties: false
971
+ required:
972
+ - id
973
+ - title
974
+ - format
975
+ - instructions
976
+ properties:
977
+ id:
978
+ type: string
979
+ description: The stable section id that was requested.
980
+ title:
981
+ type: string
982
+ description: The section's heading text.
983
+ format:
984
+ type: string
985
+ description: The `instructions` media format. Always "markdown".
986
+ enum:
987
+ - markdown
988
+ instructions:
989
+ type: string
990
+ description: The section's markdown, with example commands keyed to this instance.
884
991
  SubmitResult:
885
992
  type: object
886
993
  required:
@@ -3080,6 +3187,53 @@ paths:
3080
3187
  application/json:
3081
3188
  schema:
3082
3189
  $ref: "#/components/schemas/ErrorBody"
3190
+ /agent/guide:
3191
+ get:
3192
+ operationId: getAgentGuide
3193
+ summary: The operator guide, ADDRESSABLE — fetch one section instead of the whole ~43KB blob.
3194
+ Read-only, pure, idempotent. Call with NO `section` to get a compact table of contents (every
3195
+ stable section id + a one-line summary); call with `section` set to a TOC id (e.g.
3196
+ `delivery-graphs`) to get ONLY that section's markdown, small enough to fit a typical
3197
+ tool-result limit. This is the MCP-friendly companion to `getAgentInstructions`, which still
3198
+ returns the full guide unchanged for non-MCP callers. Typical flow — first call
3199
+ `getAgentGuide` (no arg) to see the ids, then `getAgentGuide(section=<id>)` for the one you
3200
+ need. An unknown id is rejected with `issues[{path,message}]` that lists the valid ids.
3201
+ security:
3202
+ - hookSecret: []
3203
+ - {}
3204
+ parameters:
3205
+ # Self-contained tool input (epic #605 S0 convention): a single inline `type: string` query
3206
+ # param — no `$ref`, an explicit type and example — so the projected MCP tool schema is
3207
+ # client-usable as-is (no request body; the inline-mcp-bodies generator does not apply here).
3208
+ - name: section
3209
+ in: query
3210
+ required: false
3211
+ schema:
3212
+ type: string
3213
+ example: delivery-graphs
3214
+ description: OPTIONAL stable section id (from the table of contents `getAgentGuide` returns
3215
+ with no argument), e.g. `orient`, `submit-pr`, `submit-epic`, `escalations`, `lifecycle`,
3216
+ `debug`, `debug-models`, `unstick`, `raise-issue`, `delivery-graphs`. Omit it to get the
3217
+ table of contents. An unknown id yields a 400 listing the valid ids.
3218
+ responses:
3219
+ "200":
3220
+ description: Either the table of contents (no `section`) or a single section's markdown.
3221
+ content:
3222
+ application/json:
3223
+ schema:
3224
+ $ref: "#/components/schemas/AgentGuideResponse"
3225
+ "400":
3226
+ description: The `section` id is not a known section; `issues` lists the valid ids.
3227
+ content:
3228
+ application/json:
3229
+ schema:
3230
+ $ref: "#/components/schemas/ValidationError"
3231
+ "401":
3232
+ description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
3233
+ content:
3234
+ application/json:
3235
+ schema:
3236
+ $ref: "#/components/schemas/ErrorBody"
3083
3237
  /actions/start/convergence-loop:
3084
3238
  post:
3085
3239
  operationId: startConvergenceLoop
@@ -4064,6 +4218,262 @@ paths:
4064
4218
  application/json:
4065
4219
  schema:
4066
4220
  $ref: "#/components/schemas/ErrorBody"
4221
+ /delivery-graph/vocabulary:
4222
+ get:
4223
+ operationId: getDeliveryGraphVocabulary
4224
+ summary: The closed delivery-graph vocabulary + wait-probe semantics as structured JSON (ADR 0005).
4225
+ description: >-
4226
+ Read tool (projected onto the MCP surface like `getAgentInstructions`). Returns the CLOSED
4227
+ delivery-graph vocabulary and the non-obvious wait/poll/fact-threading semantics as structured
4228
+ JSON, so you can discover them from the surface instead of reading source. Covers: the four node
4229
+ kinds (`agent`/`wait`/`human`/`connector`) with their per-kind body contracts; every wait probe
4230
+ kind with its `match` fields and — crucially — WHAT it OBSERVES (e.g. `epic` resolves a lineage
4231
+ thread by `rootRequestKey` REGARDLESS of the thread's kind, so it gates plan-fanout epics AND
4232
+ single-PR feature runs alike, ready on `stage:"merged" && active:false`); which connector targets
4233
+ are real (`converge`/`converge-merge`/`merge-main`) vs. forward-declared stubs; the `onTimeout`
4234
+ options; the poll-budget rule (always set a realistic `poll.timeoutMs` on merge/epic gates — the
4235
+ 30-minute default is a trap); and the edge/fact-threading rules (a `node.fact` must be threaded by
4236
+ an edge to every consumer, else `unbound-pr`). Derived from the implementing code (a drift test
4237
+ fails the build if a probe kind / connector target is added without a vocabulary entry). Pure,
4238
+ read-only, idempotent — no side effects. Pairs with `compileDeliveryGraph`/`previewDeliveryGraph`:
4239
+ call this first to learn the vocabulary, then author a `DeliveryGraph` and compile it.
4240
+ security:
4241
+ - hookSecret: []
4242
+ - {}
4243
+ responses:
4244
+ "200":
4245
+ description: The full delivery-graph vocabulary.
4246
+ content:
4247
+ application/json:
4248
+ schema:
4249
+ type: object
4250
+ additionalProperties: false
4251
+ description: The closed delivery-graph vocabulary + wait-probe semantics, derived from the compiler/runner code.
4252
+ required:
4253
+ - adr
4254
+ - summary
4255
+ - nodeKinds
4256
+ - factTypes
4257
+ - guardScalarTypes
4258
+ - waitProbeKinds
4259
+ - connectorTargets
4260
+ - onTimeout
4261
+ - pollBudget
4262
+ - factThreading
4263
+ properties:
4264
+ adr:
4265
+ type: string
4266
+ description: The governing ADR (agent-authored delivery graphs).
4267
+ summary:
4268
+ type: string
4269
+ description: One-paragraph orientation on the graph shape and the propose→compile→stage surface.
4270
+ nodeKinds:
4271
+ type: array
4272
+ description: The closed node-kind allowlist with each kind's config key and body contract.
4273
+ items:
4274
+ type: object
4275
+ additionalProperties: false
4276
+ required: [kind, configKey, requiredFields, optionalFields, sideEffecting, mayEmit, summary]
4277
+ properties:
4278
+ kind:
4279
+ type: string
4280
+ description: The node kind (one of agent | wait | human | connector).
4281
+ configKey:
4282
+ type: string
4283
+ description: The per-kind config object key the node must carry.
4284
+ requiredFields:
4285
+ type: array
4286
+ items: { type: string }
4287
+ description: Required non-empty fields inside the per-kind config.
4288
+ optionalFields:
4289
+ type: array
4290
+ items: { type: string }
4291
+ description: Optional fields inside the per-kind config.
4292
+ sideEffecting:
4293
+ type: boolean
4294
+ description: Whether the node performs a side effect (agent/connector) vs. read-only (wait/human).
4295
+ mayEmit:
4296
+ type: boolean
4297
+ description: Whether the node may declare typed emits.
4298
+ summary:
4299
+ type: string
4300
+ description: The body contract / semantics of the kind.
4301
+ factTypes:
4302
+ type: array
4303
+ items: { type: string }
4304
+ description: The closed emitted-fact type allowlist.
4305
+ guardScalarTypes:
4306
+ type: array
4307
+ items: { type: string }
4308
+ description: The scalar fact types an edge `when` guard may reference.
4309
+ waitProbeKinds:
4310
+ type: array
4311
+ description: Every wait probe kind, its match fields, and what it observes / when it is ready.
4312
+ items:
4313
+ type: object
4314
+ additionalProperties: false
4315
+ required: [kind, target, matchFields, observes, ready]
4316
+ properties:
4317
+ kind:
4318
+ type: string
4319
+ description: The probe kind (http | command | npm | github-check | capability | pr | epic).
4320
+ target:
4321
+ type: string
4322
+ description: What the probe's `target` names.
4323
+ matchFields:
4324
+ type: array
4325
+ items: { type: string }
4326
+ description: The `match` fields this kind reads.
4327
+ conditions:
4328
+ type: array
4329
+ items: { type: string }
4330
+ description: The closed condition set for pr/epic kinds (else absent).
4331
+ observes:
4332
+ type: string
4333
+ description: The read that decides readiness (what the probe actually observes).
4334
+ ready:
4335
+ type: string
4336
+ description: The condition under which the probe reports ready.
4337
+ binds:
4338
+ type: array
4339
+ items: { type: string }
4340
+ description: Output facts the probe binds on a ready match.
4341
+ connectorTargets:
4342
+ type: array
4343
+ description: Which connector targets are real (converge-enrollment) vs. forward-declared stubs.
4344
+ items:
4345
+ type: object
4346
+ additionalProperties: false
4347
+ required: [target, status, summary]
4348
+ properties:
4349
+ target:
4350
+ type: string
4351
+ description: The connector target literal (or a sentinel for any other target).
4352
+ status:
4353
+ type: string
4354
+ enum: [real, forward-declared]
4355
+ description: real ⇒ dispatches a real side effect; forward-declared ⇒ a no-op stub.
4356
+ convergeOnlyDefault:
4357
+ type: boolean
4358
+ description: The default `convergeOnly` for a real converge target.
4359
+ summary:
4360
+ type: string
4361
+ description: What the target does.
4362
+ onTimeout:
4363
+ type: array
4364
+ description: The `onTimeout` options for a bounded wait and what each does.
4365
+ items:
4366
+ type: object
4367
+ additionalProperties: false
4368
+ required: [value, meaning]
4369
+ properties:
4370
+ value: { type: string }
4371
+ meaning: { type: string }
4372
+ pollBudget:
4373
+ type: object
4374
+ additionalProperties: false
4375
+ required: [defaultTimeoutMs, defaultTimeoutIso, defaultEveryMs, rule]
4376
+ description: The poll-budget defaults and the "always set poll.timeoutMs on merge/epic gates" rule.
4377
+ properties:
4378
+ defaultTimeoutMs: { type: number }
4379
+ defaultTimeoutIso: { type: string }
4380
+ defaultEveryMs: { type: number }
4381
+ rule: { type: string }
4382
+ factThreading:
4383
+ type: object
4384
+ additionalProperties: false
4385
+ required: [rule, details]
4386
+ description: The edge/fact-threading rules — a node.fact reaches a consumer only via an edge.
4387
+ properties:
4388
+ rule: { type: string }
4389
+ details:
4390
+ type: array
4391
+ items: { type: string }
4392
+ guideSection:
4393
+ type: string
4394
+ description: The operator-guide section this data mirrors (docs/agent-guide.md §9).
4395
+ example:
4396
+ adr: "ADR 0005 — agent-authored delivery graphs"
4397
+ summary: "A delivery graph is a JSON DAG an agent authors as DATA; the surface ends at propose → compile → stage."
4398
+ nodeKinds:
4399
+ - kind: agent
4400
+ configKey: agent
4401
+ requiredFields: [jobType]
4402
+ optionalFields: [prompt, converge, merge]
4403
+ sideEffecting: true
4404
+ mayEmit: true
4405
+ summary: "A worker runs an agent job type; an agent that opens a PR emits it as a `pr` fact."
4406
+ - kind: wait
4407
+ configKey: wait
4408
+ requiredFields: [kind, target]
4409
+ optionalFields: [match, poll, onTimeout, credentialEnv]
4410
+ sideEffecting: false
4411
+ mayEmit: true
4412
+ summary: "A durable, bounded, read-only readiness probe (a ReadinessProbe verbatim)."
4413
+ - kind: connector
4414
+ configKey: connector
4415
+ requiredFields: [target]
4416
+ optionalFields: [dedupeKey, payload]
4417
+ sideEffecting: true
4418
+ mayEmit: true
4419
+ summary: "An automated outbound action; payload for a converge target is { pr, convergeOnly?, dependsOn? }."
4420
+ factTypes: [string, number, boolean, artifact, version, url, pr]
4421
+ guardScalarTypes: [string, number, boolean]
4422
+ waitProbeKinds:
4423
+ - kind: pr
4424
+ target: "an owner/repo#N PR (or a <node>.pr fact reference)"
4425
+ matchFields: [prState]
4426
+ conditions: [ready, merged, mergeable, checks-green]
4427
+ observes: "the live GitHub state of one in-flight PR; only OBSERVES, level-triggered."
4428
+ ready: "the PR reaches match.prState (default merged)."
4429
+ binds: [mergedSha]
4430
+ - kind: epic
4431
+ target: "the epic's durable planKey (owner/repo#NN, the epic issue)"
4432
+ matchFields: [epicState]
4433
+ conditions: [merged, done]
4434
+ observes: "the app's lineage read-model, resolved by rootRequestKey REGARDLESS of thread kind (feature | epic | pr | delivery) — so it gates a single-PR FEATURE RUN just as well as a plan-fanout epic."
4435
+ ready: 'the lineage thread reaches stage:"merged" && active:false (every opened slice/PR landed).'
4436
+ binds: [prCount]
4437
+ connectorTargets:
4438
+ - target: converge
4439
+ status: real
4440
+ convergeOnlyDefault: true
4441
+ summary: "Converge-only: drive review convergence and stop at converged."
4442
+ - target: converge-merge
4443
+ status: real
4444
+ convergeOnlyDefault: false
4445
+ summary: "Unit-level land: converge AND merge onto the PR's own base branch."
4446
+ - target: merge-main
4447
+ status: real
4448
+ convergeOnlyDefault: false
4449
+ summary: "Graph-level top-level land onto main (two-level merge)."
4450
+ - target: "<any other target>"
4451
+ status: forward-declared
4452
+ summary: "Forward-declared stub — returns a deterministic acknowledgement, fires no real I/O."
4453
+ onTimeout:
4454
+ - value: escalate
4455
+ meaning: "park a human escalation when the bounded wait elapses."
4456
+ - value: fail
4457
+ meaning: "terminate the gate as failed (NOT yet supported on a wait node — rejected by the compiler)."
4458
+ - value: continue
4459
+ meaning: "proceed as if ready when the wait elapses (a soft gate)."
4460
+ pollBudget:
4461
+ defaultTimeoutMs: 1800000
4462
+ defaultTimeoutIso: PT30M
4463
+ defaultEveryMs: 15000
4464
+ rule: "An omitted poll.timeoutMs inherits the 30-minute default — a trap for wait[pr, merged]/wait[epic] which wait hours/days. Always set poll.timeoutMs explicitly on a merge/epic gate."
4465
+ factThreading:
4466
+ rule: "A node's emitted fact reaches a consumer ONLY via an edge (<nodeId>.<fact>); an unthreaded reference is rejected (unbound-pr)."
4467
+ details:
4468
+ - "The referenced fact must be declared in the producer's emits[] with the right type."
4469
+ - "A connector payload may omit pr to auto-bind the single incoming pr fact."
4470
+ guideSection: "docs/agent-guide.md §9 (Author and run a delivery graph)"
4471
+ "401":
4472
+ description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
4473
+ content:
4474
+ application/json:
4475
+ schema:
4476
+ $ref: "#/components/schemas/ErrorBody"
4067
4477
  /actions/delivery-graph/library/save:
4068
4478
  post:
4069
4479
  operationId: saveToLibrary
@@ -0,0 +1,100 @@
1
+ // Tests for GET /app/api/agent/guide → operation `getAgentGuide` (epic #605 slice S5, issue #611):
2
+ // the addressable operator guide. No `section` → a compact table of contents; `section=<id>` → just
3
+ // that section; an unknown id → 400 with `issues[{path,message}]`. Mirrors the getAgentInstructions
4
+ // test's request shape and shared-secret guard pattern.
5
+ import { test } from "node:test";
6
+ import { assert, assertEquals } from "#test-assert";
7
+ import type { AppApi } from "@nanobpm/urban";
8
+ import { noopLog } from "../test/log.ts";
9
+ import { GUIDE_SECTIONS } from "../app/agentGuide.ts";
10
+ import handler from "./getAgentGuide.ts";
11
+
12
+ const app = { log: noopLog() } as any as AppApi;
13
+
14
+ function input(query: Record<string, string> = {}, headers: Record<string, string> = {}) {
15
+ return {
16
+ req: {
17
+ method: "GET",
18
+ path: "/app/api/agent/guide",
19
+ query: new URLSearchParams(query),
20
+ headers: new Headers(headers),
21
+ text: async () => "",
22
+ } as any,
23
+ params: {},
24
+ query,
25
+ body: undefined,
26
+ };
27
+ }
28
+
29
+ test("no section → the table of contents, one entry per registry section", async () => {
30
+ const r = (await handler(input(), app)) as any;
31
+ assertEquals(r.status, 200);
32
+ assertEquals(r.body.kind, "toc");
33
+ assert(typeof r.body.baseUrl === "string" && r.body.baseUrl.length > 0);
34
+ assert(Array.isArray(r.body.sections));
35
+ assertEquals(r.body.sections.length, GUIDE_SECTIONS.length);
36
+ const ids = r.body.sections.map((s: any) => s.id);
37
+ for (const s of GUIDE_SECTIONS) assert(ids.includes(s.id), `TOC must list "${s.id}"`);
38
+ for (const s of r.body.sections) {
39
+ assert(typeof s.title === "string" && s.title.length > 0);
40
+ assert(typeof s.summary === "string" && s.summary.length > 0);
41
+ }
42
+ });
43
+
44
+ test("the TOC is far smaller than the full guide (fits a tool-result limit)", async () => {
45
+ const r = (await handler(input(), app)) as any;
46
+ assert(JSON.stringify(r.body).length < 4000, "the TOC response must stay compact");
47
+ });
48
+
49
+ test("section=delivery-graphs → just that section's markdown, base-keyed", async () => {
50
+ const r = (await handler(input({ section: "delivery-graphs" }), app)) as any;
51
+ assertEquals(r.status, 200);
52
+ assertEquals(r.body.kind, "section");
53
+ assertEquals(r.body.section.id, "delivery-graphs");
54
+ assertEquals(r.body.section.format, "markdown");
55
+ assert(r.body.section.instructions.length > 200);
56
+ assert(!r.body.section.instructions.includes("__BASE__"), "placeholders must be substituted");
57
+ assert(typeof r.body.engineBase === "string" && r.body.engineBase.length > 0);
58
+ });
59
+
60
+ test("a section is much smaller than the whole guide", async () => {
61
+ const toc = (await handler(input(), app)) as any;
62
+ const section = (await handler(input({ section: "orient" }), app)) as any;
63
+ assert(
64
+ JSON.stringify(section.body).length < 30000,
65
+ "a single section must comfortably fit a typical tool-result limit",
66
+ );
67
+ assertEquals(toc.body.kind, "toc");
68
+ });
69
+
70
+ test("an unknown section id → 400 with issues[{path,message}] listing valid ids", async () => {
71
+ const r = (await handler(input({ section: "nope" }), app)) as any;
72
+ assertEquals(r.status, 400);
73
+ assert(typeof r.body.error === "string");
74
+ assert(Array.isArray(r.body.issues) && r.body.issues.length === 1);
75
+ assertEquals(r.body.issues[0].path, "section");
76
+ assert(r.body.issues[0].message.includes("delivery-graphs"), "the 400 must name the valid ids");
77
+ });
78
+
79
+ test("blank/whitespace section is treated as no section (TOC)", async () => {
80
+ const r = (await handler(input({ section: " " }), app)) as any;
81
+ assertEquals(r.status, 200);
82
+ assertEquals(r.body.kind, "toc");
83
+ });
84
+
85
+ test("shared-secret guard: rejects when the secret is set and header is wrong", async () => {
86
+ const prev = process.env.NANO_PR_WEBHOOK_SECRET;
87
+ process.env.NANO_PR_WEBHOOK_SECRET = "s3cr3t";
88
+ try {
89
+ // Re-import with the secret set so the module-level SECRET picks it up.
90
+ const mod = await import(`./getAgentGuide.ts?secret=${Date.now()}`);
91
+ const guarded = mod.default;
92
+ const rejected = (await guarded(input({}, {}), app)) as any;
93
+ assertEquals(rejected.status, 401);
94
+ const ok = (await guarded(input({}, { "x-hook-secret": "s3cr3t" }), app)) as any;
95
+ assertEquals(ok.status, 200);
96
+ } finally {
97
+ if (prev === undefined) delete process.env.NANO_PR_WEBHOOK_SECRET;
98
+ else process.env.NANO_PR_WEBHOOK_SECRET = prev;
99
+ }
100
+ });
@@ -0,0 +1,92 @@
1
+ // GET /app/api/agent/guide → operationId `getAgentGuide` (epic nano-workforce#605, slice S5,
2
+ // issue #611). The ADDRESSABLE operator guide: fetch one section instead of the whole ~43KB blob
3
+ // that `getAgentInstructions` returns (which can exceed an agent's tool-result limit, forcing it to
4
+ // persist the blob and carve out a section out-of-band).
5
+ //
6
+ // • No `section` query param → a compact table of contents: every stable section id + a one-line
7
+ // summary (`kind: "toc"`). Small by construction — safe under any tool-result limit.
8
+ // • `section=<id>` → ONLY that section's markdown (`kind: "section"`), examples keyed to THIS
9
+ // instance's control-API base + engine base, exactly as the full guide keys them.
10
+ // • An unknown id → 400 with `issues: [{ path: "section", message }]` listing the valid ids
11
+ // (the uniform validation-error contract).
12
+ //
13
+ // This is the MCP-friendly companion to `getAgentInstructions`; the full-guide doors
14
+ // (`GET /agent`, `GET /agent/skill`) are untouched and byte-identical. Read-only, pure, idempotent.
15
+ //
16
+ // The optional shared-secret guard mirrors /agent and /version: enforced HERE only when
17
+ // NANO_PR_WEBHOOK_SECRET is set (the runtime does not enforce OpenAPI `security`).
18
+ import { guideToc, renderGuideSection, resolveEngineBase } from "../app/agentGuide.ts";
19
+ import { resolveApiBase } from "../app/resolveApiBase.ts";
20
+ import { buildVersionInfo, envVar } from "../app/version.ts";
21
+ import { defineOperation } from "../nano-generated/operations.ts";
22
+
23
+ const SECRET = envVar("NANO_PR_WEBHOOK_SECRET") ?? "";
24
+
25
+ export default defineOperation("getAgentGuide", ({ query, req }, app) => {
26
+ if (SECRET && req.headers.get("x-hook-secret") !== SECRET) {
27
+ app.log.warn("getAgentGuide rejected: missing/invalid shared secret");
28
+ return { status: 401, body: { error: "unauthorized" } };
29
+ }
30
+
31
+ const baseUrl = resolveApiBase(req, "agent/guide");
32
+ const rawSection = query.section;
33
+ const section = typeof rawSection === "string" ? rawSection.trim() : "";
34
+
35
+ // No section → the table of contents.
36
+ if (!section) {
37
+ return {
38
+ status: 200,
39
+ body: {
40
+ kind: "toc",
41
+ appVersion: buildVersionInfo().version,
42
+ generatedAt: new Date().toISOString(),
43
+ baseUrl,
44
+ sections: guideToc(),
45
+ },
46
+ };
47
+ }
48
+
49
+ // A section id → just that section, or a 400 that names the valid ids.
50
+ const instructions = renderGuideSection(section, baseUrl);
51
+ if (instructions === undefined) {
52
+ // Derive the valid ids from the PARSED table of contents — the sections this deployment can
53
+ // actually serve — not the static registry. When the guide doc is unreadable (RAW_GUIDE
54
+ // fallback, no `##` headings) the TOC is empty and NO id is retrievable, so say so explicitly
55
+ // rather than list registry ids that would themselves 400.
56
+ const validIds = guideToc().map((s) => s.id);
57
+ const detail =
58
+ validIds.length > 0
59
+ ? `valid ids: ${validIds.join(", ")}`
60
+ : "no sections are available in this deployment";
61
+ return {
62
+ status: 400,
63
+ body: {
64
+ error: `unknown guide section "${section}"`,
65
+ issues: [
66
+ {
67
+ path: "section",
68
+ message: `unknown section id "${section}"; ${detail}`,
69
+ },
70
+ ],
71
+ },
72
+ };
73
+ }
74
+
75
+ const title = guideToc().find((s) => s.id === section)?.title ?? section;
76
+ return {
77
+ status: 200,
78
+ body: {
79
+ kind: "section",
80
+ appVersion: buildVersionInfo().version,
81
+ generatedAt: new Date().toISOString(),
82
+ baseUrl,
83
+ engineBase: resolveEngineBase(),
84
+ section: {
85
+ id: section,
86
+ title,
87
+ format: "markdown",
88
+ instructions,
89
+ },
90
+ },
91
+ };
92
+ });
@@ -0,0 +1,25 @@
1
+ // GET /app/api/delivery-graph/vocabulary → operationId `getDeliveryGraphVocabulary` (epic
2
+ // nano-workforce#605, S3/#609). A read tool — projected onto the MCP surface like
3
+ // `getAgentInstructions` — that returns the closed delivery-graph vocabulary + wait-probe semantics
4
+ // as STRUCTURED JSON, so an agent can discover the node/probe/connector vocabulary and the non-obvious
5
+ // wait/poll/fact-threading rules from the surface instead of reading source (ADR 0005).
6
+ //
7
+ // The payload is derived from the implementing code (`app/deliveryGraphVocabulary.ts`) — every closed
8
+ // set is imported from its owning module, and a drift test fails the build if a probe kind / connector
9
+ // target lands in the compiler without a vocabulary entry. Cross-linked from docs/agent-guide.md §9.
10
+ //
11
+ // Read-only. The optional shared-secret guard mirrors /agent and /version: enforced HERE only when
12
+ // NANO_PR_WEBHOOK_SECRET is set (the runtime does not enforce OpenAPI `security`).
13
+ import { deliveryGraphVocabulary } from "../app/deliveryGraphVocabulary.ts";
14
+ import { envVar } from "../app/version.ts";
15
+ import { defineOperation } from "../nano-generated/operations.ts";
16
+
17
+ const SECRET = envVar("NANO_PR_WEBHOOK_SECRET") ?? "";
18
+
19
+ export default defineOperation("getDeliveryGraphVocabulary", ({ req }, app) => {
20
+ if (SECRET && req.headers.get("x-hook-secret") !== SECRET) {
21
+ app.log.warn("getDeliveryGraphVocabulary rejected: missing/invalid shared secret");
22
+ return { status: 401, body: { error: "unauthorized" } };
23
+ }
24
+ return { status: 200, body: deliveryGraphVocabulary() };
25
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.159.1",
3
+ "version": "0.161.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",