@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/CHANGELOG.md +12 -0
- package/app/agentGuide.test.ts +115 -0
- package/app/agentGuide.ts +109 -0
- package/app/deliveryGraphVocabulary.test.ts +102 -0
- package/app/deliveryGraphVocabulary.ts +319 -0
- package/app/readiness.ts +10 -5
- package/docs/agent-guide.md +15 -0
- package/docs/mcp-runbook.md +16 -0
- package/e2e/addressable-guide.e2e.ts +89 -0
- package/openapi.yaml +410 -0
- package/operations/getAgentGuide.test.ts +100 -0
- package/operations/getAgentGuide.ts +92 -0
- package/operations/getDeliveryGraphVocabulary.ts +25 -0
- package/package.json +1 -1
- package/test/deliveryGraphVocabulary-mcp.test.ts +62 -0
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.
|
|
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",
|