@nanobpm/nano-workforce 0.180.0 → 0.181.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 +6 -0
- package/app/agentGuide.test.ts +87 -0
- package/app/agentGuide.ts +61 -0
- package/app/deliveryGraph.ts +1 -1
- package/app/deliveryGraphProposals.test.ts +60 -0
- package/app/deliveryGraphProposals.ts +61 -3
- package/app/deliveryGraphStage.ts +11 -1
- package/app/sequenceIssues.test.ts +131 -0
- package/app/sequenceIssues.ts +238 -29
- package/e2e/addressable-guide.e2e.ts +43 -0
- package/openapi.yaml +219 -21
- package/operations/compileDeliveryGraph.test.ts +4 -0
- package/operations/getAgentGuide.test.ts +13 -0
- package/operations/getAgentGuide.ts +40 -5
- package/package.json +1 -1
package/openapi.yaml
CHANGED
|
@@ -1248,7 +1248,20 @@ components:
|
|
|
1248
1248
|
- markdown
|
|
1249
1249
|
instructions:
|
|
1250
1250
|
type: string
|
|
1251
|
-
description: The section's markdown, with example commands keyed to this instance.
|
|
1251
|
+
description: The section's markdown (or a bounded page of it when paginating), with example commands keyed to this instance.
|
|
1252
|
+
start:
|
|
1253
|
+
type: integer
|
|
1254
|
+
description: The CHARACTER offset this page starts at (present only when paginating, issue #740).
|
|
1255
|
+
length:
|
|
1256
|
+
type: integer
|
|
1257
|
+
description: The number of characters returned in this page (present only when paginating).
|
|
1258
|
+
totalLength:
|
|
1259
|
+
type: integer
|
|
1260
|
+
description: The total number of characters in the fully-rendered section (present only when paginating).
|
|
1261
|
+
nextStart:
|
|
1262
|
+
type: integer
|
|
1263
|
+
nullable: true
|
|
1264
|
+
description: The `start` to pass for the next page, or null when this is the last page (present only when paginating).
|
|
1252
1265
|
SubmitResult:
|
|
1253
1266
|
type: object
|
|
1254
1267
|
required:
|
|
@@ -2832,6 +2845,95 @@ components:
|
|
|
2832
2845
|
description: >-
|
|
2833
2846
|
A NAVIGATIONAL cockpit deep-link to the staged proposal (helps the agent hand the human a
|
|
2834
2847
|
link). It is a pointer only — NOT a dispatch handle; nothing in this response can start a run.
|
|
2848
|
+
superseded:
|
|
2849
|
+
type: array
|
|
2850
|
+
items:
|
|
2851
|
+
type: string
|
|
2852
|
+
description: >-
|
|
2853
|
+
Digests of prior staged proposals sharing this graph's LOGICAL key (derived from its `name`)
|
|
2854
|
+
that this stage replaced — flipped to `superseded` (issue #740). Empty on a first stage. Lets
|
|
2855
|
+
the agent tell the operator precisely which proposals it retired.
|
|
2856
|
+
siblingsStaged:
|
|
2857
|
+
type: integer
|
|
2858
|
+
description: >-
|
|
2859
|
+
How many OTHER live staged proposals remain after this stage (a DIFFERENT logical key from
|
|
2860
|
+
this one) — i.e. proposals this stage did NOT supersede (issue #740). A non-zero count flags
|
|
2861
|
+
potential ORPHANED SIBLINGS (e.g. an earlier stage of the "same" runbook under a different
|
|
2862
|
+
`name`, which supersede does not catch) cluttering the operator's Delivery Graphs list, so
|
|
2863
|
+
the agent can name them for cleanup.
|
|
2864
|
+
SequenceGate:
|
|
2865
|
+
description: >-
|
|
2866
|
+
An interleaved `wait` GATE (issue #740) that must go green before a given issue's agent starts
|
|
2867
|
+
— reusing the exact probe schema `wait` nodes accept (`npm`/`github-check`/`http`/`command`/
|
|
2868
|
+
`capability`/`pr`/`epic`). It lets an author splice e.g. "wait for `@nanobpm/agentic@0.13.0` to
|
|
2869
|
+
publish" between two sequence steps without hand-authoring raw node/edge JSON.
|
|
2870
|
+
type: object
|
|
2871
|
+
additionalProperties: false
|
|
2872
|
+
required:
|
|
2873
|
+
- kind
|
|
2874
|
+
- target
|
|
2875
|
+
properties:
|
|
2876
|
+
kind:
|
|
2877
|
+
type: string
|
|
2878
|
+
minLength: 1
|
|
2879
|
+
description: >-
|
|
2880
|
+
The wait-probe kind — one of the S3 vocabulary kinds (`npm`, `github-check`, `http`,
|
|
2881
|
+
`command`, `capability`, `pr`, `epic`). An unknown kind is a 400.
|
|
2882
|
+
target:
|
|
2883
|
+
type: string
|
|
2884
|
+
minLength: 1
|
|
2885
|
+
description: >-
|
|
2886
|
+
The probe target — kind-specific: `pkg@version` for `npm`, `owner/repo@ref` for
|
|
2887
|
+
`github-check`, a URL for `http`, an `owner/repo#N` PR for `pr`, etc.
|
|
2888
|
+
match:
|
|
2889
|
+
type: object
|
|
2890
|
+
description: >-
|
|
2891
|
+
OPTIONAL kind-specific readiness match fields (e.g. `{ version }` for `npm`,
|
|
2892
|
+
`{ conclusion, checkName }` for `github-check`).
|
|
2893
|
+
poll:
|
|
2894
|
+
type: object
|
|
2895
|
+
additionalProperties: false
|
|
2896
|
+
description: >-
|
|
2897
|
+
OPTIONAL poll budget. Defaults to the bounded merge-gate budget (re-probe every 5 minutes,
|
|
2898
|
+
budget 3 days) so a gate never falls into the 30-minute default trap.
|
|
2899
|
+
properties:
|
|
2900
|
+
everyMs:
|
|
2901
|
+
type: integer
|
|
2902
|
+
minimum: 1
|
|
2903
|
+
description: How often to re-probe (milliseconds).
|
|
2904
|
+
timeoutMs:
|
|
2905
|
+
type: integer
|
|
2906
|
+
minimum: 1
|
|
2907
|
+
description: The total budget before the gate escalates/continues (milliseconds).
|
|
2908
|
+
onTimeout:
|
|
2909
|
+
type: string
|
|
2910
|
+
enum:
|
|
2911
|
+
- escalate
|
|
2912
|
+
- continue
|
|
2913
|
+
description: >-
|
|
2914
|
+
What to do when the gate never goes green within its budget — `escalate` (default) parks on
|
|
2915
|
+
a human; `continue` proceeds anyway. `fail` is not supported on a `wait` node.
|
|
2916
|
+
credentialEnv:
|
|
2917
|
+
type: string
|
|
2918
|
+
minLength: 1
|
|
2919
|
+
description: OPTIONAL env-key name supplying a credential for the probe (`http`/`capability`).
|
|
2920
|
+
SequenceIssueEntry:
|
|
2921
|
+
description: >-
|
|
2922
|
+
One `issues[]` entry with an OPTIONAL leading gate (issue #740) — an object form of a sequence
|
|
2923
|
+
step. The `issue` is implemented + converged + merged like a bare-string entry; the optional
|
|
2924
|
+
`gate` must go green (in addition to the prior issue merging) before this issue's agent starts.
|
|
2925
|
+
type: object
|
|
2926
|
+
additionalProperties: false
|
|
2927
|
+
required:
|
|
2928
|
+
- issue
|
|
2929
|
+
properties:
|
|
2930
|
+
gate:
|
|
2931
|
+
$ref: "#/components/schemas/SequenceGate"
|
|
2932
|
+
issue:
|
|
2933
|
+
type: string
|
|
2934
|
+
minLength: 1
|
|
2935
|
+
maxLength: 255
|
|
2936
|
+
description: An `owner/repo#N` issue reference to implement + converge + merge, in sequence.
|
|
2835
2937
|
SequenceIssuesIntent:
|
|
2836
2938
|
description: >-
|
|
2837
2939
|
The `sequenceIssues` INTENT (epic nano-workforce#605, S4) — a high-level shape that GENERATES
|
|
@@ -2843,15 +2945,21 @@ components:
|
|
|
2843
2945
|
`compileDeliveryGraph` — it never dispatches (dispatch is an operator-only cockpit action, ADR
|
|
2844
2946
|
0005 Decision 7). For each issue it emits `agent` (`senior:feature`, emits a `pr` fact) →
|
|
2845
2947
|
`connector` (`converge-merge`, late-binding that `pr`) → `wait[pr, merged]` (a realistic
|
|
2846
|
-
`poll.timeoutMs`), threading the `pr` fact per §9.4.
|
|
2847
|
-
|
|
2948
|
+
`poll.timeoutMs`), threading the `pr` fact per §9.4. An `issues[]` entry may be a bare
|
|
2949
|
+
`owner/repo#N` string OR a `{ gate?, issue }` object that interleaves a `wait` gate (the
|
|
2950
|
+
`npm`/`github-check`/… vocabulary) before that issue's agent (issue #740). Invalid input (empty
|
|
2951
|
+
`issues`, an unparseable ref, an unknown gate kind) is a 400 carrying `issues: [{ path, message
|
|
2952
|
+
}]`; nothing is staged.
|
|
2848
2953
|
type: object
|
|
2849
2954
|
additionalProperties: false
|
|
2850
2955
|
example:
|
|
2851
|
-
behind: nanobpm/nano-ide#488
|
|
2852
2956
|
issues:
|
|
2853
|
-
- nanobpm/nano-
|
|
2854
|
-
-
|
|
2957
|
+
- nanobpm/nano-ide#557
|
|
2958
|
+
- gate:
|
|
2959
|
+
kind: npm
|
|
2960
|
+
target: "@nanobpm/agentic@0.13.0"
|
|
2961
|
+
issue: jwulf/c8ctl-plugin-nano#186
|
|
2962
|
+
- nanobpm/nano-workforce#738
|
|
2855
2963
|
required:
|
|
2856
2964
|
- issues
|
|
2857
2965
|
properties:
|
|
@@ -2868,15 +2976,22 @@ components:
|
|
|
2868
2976
|
minItems: 1
|
|
2869
2977
|
maxItems: 64
|
|
2870
2978
|
items:
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2979
|
+
oneOf:
|
|
2980
|
+
- type: string
|
|
2981
|
+
minLength: 1
|
|
2982
|
+
maxLength: 255
|
|
2983
|
+
description: A bare `owner/repo#N` issue reference (no gate) — today's behaviour, byte-for-byte.
|
|
2984
|
+
- $ref: "#/components/schemas/SequenceIssueEntry"
|
|
2985
|
+
description: >-
|
|
2986
|
+
A sequence step — a bare `owner/repo#N` string, or a `{ gate?, issue }` object that
|
|
2987
|
+
interleaves a `wait` gate before that issue's agent (issue #740).
|
|
2875
2988
|
description: >-
|
|
2876
2989
|
The ordered issues to sequence — each is implemented by a `senior:feature` agent that opens
|
|
2877
2990
|
a PR, driven to convergence + merge, and the NEXT issue's implementation starts only once
|
|
2878
|
-
the prior issue has merged. At least one; at most
|
|
2879
|
-
compiler's node ceiling
|
|
2991
|
+
the prior issue has merged (and any interleaved gate has gone green). At least one; at most
|
|
2992
|
+
64. The compiler's node ceiling is enforced SEPARATELY by a node-budget guard (interleaved
|
|
2993
|
+
gates each add nodes, so a fully-gated 64-issue sequence can exceed the ceiling and is
|
|
2994
|
+
rejected there) — `maxItems: 64` bounds the entry count, not the generated node count.
|
|
2880
2995
|
SequenceIssuesRejected:
|
|
2881
2996
|
description: >-
|
|
2882
2997
|
A rejected `sequenceIssues` intent — the input failed validation (empty/oversized `issues`, an
|
|
@@ -3847,6 +3962,31 @@ paths:
|
|
|
3847
3962
|
with no argument), e.g. `orient`, `submit-pr`, `submit-epic`, `escalations`, `lifecycle`,
|
|
3848
3963
|
`debug`, `debug-models`, `unstick`, `raise-issue`, `delivery-graphs`, `tool-crosswalk`. Omit it to get the
|
|
3849
3964
|
table of contents. An unknown id yields a 400 listing the valid ids.
|
|
3965
|
+
- name: start
|
|
3966
|
+
in: query
|
|
3967
|
+
required: false
|
|
3968
|
+
schema:
|
|
3969
|
+
type: integer
|
|
3970
|
+
minimum: 0
|
|
3971
|
+
example: 0
|
|
3972
|
+
description: >-
|
|
3973
|
+
OPTIONAL pagination cursor (issue #740) — the CHARACTER offset into a `section`'s markdown to
|
|
3974
|
+
start at. Passing `start` and/or `length` engages bounded-chunk retrieval so a large section
|
|
3975
|
+
(e.g. `delivery-graphs`) can be paged through without overflowing a tool-result limit; the
|
|
3976
|
+
response echoes `start`, `length`, `totalLength`, and a `nextStart` cursor (null on the last
|
|
3977
|
+
page). Ignored when no `section` is given. Omit both `start` and `length` to get the whole
|
|
3978
|
+
section unchanged.
|
|
3979
|
+
- name: length
|
|
3980
|
+
in: query
|
|
3981
|
+
required: false
|
|
3982
|
+
schema:
|
|
3983
|
+
type: integer
|
|
3984
|
+
minimum: 1
|
|
3985
|
+
example: 12000
|
|
3986
|
+
description: >-
|
|
3987
|
+
OPTIONAL pagination window (issue #740) — the maximum number of CHARACTERS to return for a
|
|
3988
|
+
`section`. Defaults to a bounded page size when `start` is given without `length`. Pair with
|
|
3989
|
+
`start`/`nextStart` to page through an oversized section.
|
|
3850
3990
|
responses:
|
|
3851
3991
|
"200":
|
|
3852
3992
|
description: Either the table of contents (no `section`) or a single section's markdown.
|
|
@@ -4161,14 +4301,17 @@ paths:
|
|
|
4161
4301
|
application/json:
|
|
4162
4302
|
schema:
|
|
4163
4303
|
# BEGIN generated:mcp-body source=#/components/schemas/SequenceIssuesIntent (scripts/inline-mcp-bodies.ts — do not hand-edit)
|
|
4164
|
-
description: 'The `sequenceIssues` INTENT (epic nano-workforce#605, S4) — a high-level shape that GENERATES the canonical "implement issue → converge → merge" delivery graph (operator-guide §9.4) instead of making an agent hand-author its node/edge JSON. It names an ordered list of `issues` to sequence (each issue''s implementation starts once the PRIOR issue has merged) and an OPTIONAL leading `behind` gate (wait for that issue/epic/feature to be fully merged first, §9.5). The door GENERATES, then STAGES the graph through the same compile+stage flow as `compileDeliveryGraph` — it never dispatches (dispatch is an operator-only cockpit action, ADR 0005 Decision 7). For each issue it emits `agent` (`senior:feature`, emits a `pr` fact) → `connector` (`converge-merge`, late-binding that `pr`) → `wait[pr, merged]` (a realistic `poll.timeoutMs`), threading the `pr` fact per §9.4. Invalid input (empty `issues`, an unparseable ref) is a 400 carrying `issues: [{ path, message }]`; nothing is staged.'
|
|
4304
|
+
description: 'The `sequenceIssues` INTENT (epic nano-workforce#605, S4) — a high-level shape that GENERATES the canonical "implement issue → converge → merge" delivery graph (operator-guide §9.4) instead of making an agent hand-author its node/edge JSON. It names an ordered list of `issues` to sequence (each issue''s implementation starts once the PRIOR issue has merged) and an OPTIONAL leading `behind` gate (wait for that issue/epic/feature to be fully merged first, §9.5). The door GENERATES, then STAGES the graph through the same compile+stage flow as `compileDeliveryGraph` — it never dispatches (dispatch is an operator-only cockpit action, ADR 0005 Decision 7). For each issue it emits `agent` (`senior:feature`, emits a `pr` fact) → `connector` (`converge-merge`, late-binding that `pr`) → `wait[pr, merged]` (a realistic `poll.timeoutMs`), threading the `pr` fact per §9.4. An `issues[]` entry may be a bare `owner/repo#N` string OR a `{ gate?, issue }` object that interleaves a `wait` gate (the `npm`/`github-check`/… vocabulary) before that issue''s agent (issue #740). Invalid input (empty `issues`, an unparseable ref, an unknown gate kind) is a 400 carrying `issues: [{ path, message }]`; nothing is staged.'
|
|
4165
4305
|
type: object
|
|
4166
4306
|
additionalProperties: false
|
|
4167
4307
|
example:
|
|
4168
|
-
behind: nanobpm/nano-ide#488
|
|
4169
4308
|
issues:
|
|
4170
|
-
- nanobpm/nano-
|
|
4171
|
-
-
|
|
4309
|
+
- nanobpm/nano-ide#557
|
|
4310
|
+
- gate:
|
|
4311
|
+
kind: npm
|
|
4312
|
+
target: '@nanobpm/agentic@0.13.0'
|
|
4313
|
+
issue: jwulf/c8ctl-plugin-nano#186
|
|
4314
|
+
- nanobpm/nano-workforce#738
|
|
4172
4315
|
required:
|
|
4173
4316
|
- issues
|
|
4174
4317
|
properties:
|
|
@@ -4182,11 +4325,66 @@ paths:
|
|
|
4182
4325
|
minItems: 1
|
|
4183
4326
|
maxItems: 64
|
|
4184
4327
|
items:
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4328
|
+
oneOf:
|
|
4329
|
+
- type: string
|
|
4330
|
+
minLength: 1
|
|
4331
|
+
maxLength: 255
|
|
4332
|
+
description: A bare `owner/repo#N` issue reference (no gate) — today's behaviour, byte-for-byte.
|
|
4333
|
+
- description: "One `issues[]` entry with an OPTIONAL leading gate (issue #740) — an object form of a sequence step. The `issue` is implemented + converged + merged like a bare-string entry; the optional `gate` must go green (in addition to the prior issue merging) before this issue's agent starts."
|
|
4334
|
+
type: object
|
|
4335
|
+
additionalProperties: false
|
|
4336
|
+
required:
|
|
4337
|
+
- issue
|
|
4338
|
+
properties:
|
|
4339
|
+
gate:
|
|
4340
|
+
description: 'An interleaved `wait` GATE (issue #740) that must go green before a given issue''s agent starts — reusing the exact probe schema `wait` nodes accept (`npm`/`github-check`/`http`/`command`/ `capability`/`pr`/`epic`). It lets an author splice e.g. "wait for `@nanobpm/agentic@0.13.0` to publish" between two sequence steps without hand-authoring raw node/edge JSON.'
|
|
4341
|
+
type: object
|
|
4342
|
+
additionalProperties: false
|
|
4343
|
+
required:
|
|
4344
|
+
- kind
|
|
4345
|
+
- target
|
|
4346
|
+
properties:
|
|
4347
|
+
kind:
|
|
4348
|
+
type: string
|
|
4349
|
+
minLength: 1
|
|
4350
|
+
description: The wait-probe kind — one of the S3 vocabulary kinds (`npm`, `github-check`, `http`, `command`, `capability`, `pr`, `epic`). An unknown kind is a 400.
|
|
4351
|
+
target:
|
|
4352
|
+
type: string
|
|
4353
|
+
minLength: 1
|
|
4354
|
+
description: 'The probe target — kind-specific: `pkg@version` for `npm`, `owner/repo@ref` for `github-check`, a URL for `http`, an `owner/repo#N` PR for `pr`, etc.'
|
|
4355
|
+
match:
|
|
4356
|
+
type: object
|
|
4357
|
+
description: OPTIONAL kind-specific readiness match fields (e.g. `{ version }` for `npm`, `{ conclusion, checkName }` for `github-check`).
|
|
4358
|
+
poll:
|
|
4359
|
+
type: object
|
|
4360
|
+
additionalProperties: false
|
|
4361
|
+
description: OPTIONAL poll budget. Defaults to the bounded merge-gate budget (re-probe every 5 minutes, budget 3 days) so a gate never falls into the 30-minute default trap.
|
|
4362
|
+
properties:
|
|
4363
|
+
everyMs:
|
|
4364
|
+
type: integer
|
|
4365
|
+
minimum: 1
|
|
4366
|
+
description: How often to re-probe (milliseconds).
|
|
4367
|
+
timeoutMs:
|
|
4368
|
+
type: integer
|
|
4369
|
+
minimum: 1
|
|
4370
|
+
description: The total budget before the gate escalates/continues (milliseconds).
|
|
4371
|
+
onTimeout:
|
|
4372
|
+
type: string
|
|
4373
|
+
enum:
|
|
4374
|
+
- escalate
|
|
4375
|
+
- continue
|
|
4376
|
+
description: What to do when the gate never goes green within its budget — `escalate` (default) parks on a human; `continue` proceeds anyway. `fail` is not supported on a `wait` node.
|
|
4377
|
+
credentialEnv:
|
|
4378
|
+
type: string
|
|
4379
|
+
minLength: 1
|
|
4380
|
+
description: OPTIONAL env-key name supplying a credential for the probe (`http`/`capability`).
|
|
4381
|
+
issue:
|
|
4382
|
+
type: string
|
|
4383
|
+
minLength: 1
|
|
4384
|
+
maxLength: 255
|
|
4385
|
+
description: An `owner/repo#N` issue reference to implement + converge + merge, in sequence.
|
|
4386
|
+
description: "A sequence step — a bare `owner/repo#N` string, or a `{ gate?, issue }` object that interleaves a `wait` gate before that issue's agent (issue #740)."
|
|
4387
|
+
description: "The ordered issues to sequence — each is implemented by a `senior:feature` agent that opens a PR, driven to convergence + merge, and the NEXT issue's implementation starts only once the prior issue has merged (and any interleaved gate has gone green). At least one; at most 64. The compiler's node ceiling is enforced SEPARATELY by a node-budget guard (interleaved gates each add nodes, so a fully-gated 64-issue sequence can exceed the ceiling and is rejected there) — `maxItems: 64` bounds the entry count, not the generated node count."
|
|
4190
4388
|
# END generated:mcp-body
|
|
4191
4389
|
responses:
|
|
4192
4390
|
"200":
|
|
@@ -127,6 +127,10 @@ test("compile-delivery-graph: a changed graph with the same name SUPERSEDES the
|
|
|
127
127
|
assert(a.body.digest !== b.body.digest, "the changed graph has a new digest");
|
|
128
128
|
assertEquals((await deliveryGraphProposals(data).get(a.body.digest))?.status, "superseded");
|
|
129
129
|
assertEquals((await deliveryGraphProposals(data).get(b.body.digest))?.status, "staged");
|
|
130
|
+
// The supersede is surfaced in the compile result body so the operator sees the retirement (#740).
|
|
131
|
+
assertEquals(b.body.superseded, [a.body.digest]);
|
|
132
|
+
assertEquals(b.body.siblingsStaged, 0);
|
|
133
|
+
assertEquals(a.body.superseded, []); // the first stage retired nothing
|
|
130
134
|
});
|
|
131
135
|
});
|
|
132
136
|
|
|
@@ -82,6 +82,19 @@ test("blank/whitespace section is treated as no section (TOC)", async () => {
|
|
|
82
82
|
assertEquals(r.body.kind, "toc");
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
+
test("a pagination cursor beyond MAX_SAFE_INTEGER → 400 (unsafe integers lose precision)", async () => {
|
|
86
|
+
// 9007199254740993 === 9007199254740992 in IEEE-754 double, so `Number.isInteger` accepts it
|
|
87
|
+
// while it no longer represents the caller's requested character offset. It must be rejected.
|
|
88
|
+
const unsafe = "9007199254740993";
|
|
89
|
+
const rStart = (await handler(input({ section: "delivery-graphs", start: unsafe }), app)) as any;
|
|
90
|
+
assertEquals(rStart.status, 400);
|
|
91
|
+
assert(Array.isArray(rStart.body.issues) && rStart.body.issues.some((i: any) => i.path === "start"));
|
|
92
|
+
|
|
93
|
+
const rLength = (await handler(input({ section: "delivery-graphs", length: unsafe }), app)) as any;
|
|
94
|
+
assertEquals(rLength.status, 400);
|
|
95
|
+
assert(Array.isArray(rLength.body.issues) && rLength.body.issues.some((i: any) => i.path === "length"));
|
|
96
|
+
});
|
|
97
|
+
|
|
85
98
|
test("shared-secret guard: rejects when the secret is set and header is wrong", async () => {
|
|
86
99
|
const prev = process.env.NANO_PR_WEBHOOK_SECRET;
|
|
87
100
|
process.env.NANO_PR_WEBHOOK_SECRET = "s3cr3t";
|
|
@@ -15,13 +15,29 @@
|
|
|
15
15
|
//
|
|
16
16
|
// The optional shared-secret guard mirrors /agent and /version: enforced HERE only when
|
|
17
17
|
// NANO_PR_WEBHOOK_SECRET is set (the runtime does not enforce OpenAPI `security`).
|
|
18
|
-
import { guideToc, renderGuideSection, resolveEngineBase } from "../app/agentGuide.ts";
|
|
18
|
+
import { GUIDE_SECTION_PAGE_DEFAULT, guideToc, renderGuideSection, renderGuideSectionChunk, resolveEngineBase } from "../app/agentGuide.ts";
|
|
19
19
|
import { resolveApiBase } from "../app/resolveApiBase.ts";
|
|
20
20
|
import { buildVersionInfo, envVar } from "../app/version.ts";
|
|
21
21
|
import { defineOperation } from "../nano-generated/operations.ts";
|
|
22
22
|
|
|
23
23
|
const SECRET = envVar("NANO_PR_WEBHOOK_SECRET") ?? "";
|
|
24
24
|
|
|
25
|
+
/** Parse a `start`/`length` pagination query param: a present value must be a non-negative SAFE
|
|
26
|
+
* integer (`length` additionally >= 1). `Number.isSafeInteger` (not `Number.isInteger`) is required
|
|
27
|
+
* because these values are used as character cursors: an integer beyond `Number.MAX_SAFE_INTEGER`
|
|
28
|
+
* (e.g. `9007199254740993`) loses precision, so accepting it would silently misinterpret the
|
|
29
|
+
* caller's requested window. Returns the parsed number, `undefined` when absent, or a
|
|
30
|
+
* path-qualified validation issue. */
|
|
31
|
+
function parsePageArg(raw: unknown, path: string, min: number): { value?: number; issue?: { path: string; message: string } } {
|
|
32
|
+
if (raw === undefined || raw === null || (typeof raw === "string" && raw.trim() === "")) return {};
|
|
33
|
+
const s = typeof raw === "string" ? raw.trim() : String(raw);
|
|
34
|
+
const n = Number(s);
|
|
35
|
+
if (!Number.isSafeInteger(n) || n < min) {
|
|
36
|
+
return { issue: { path, message: `\`${path}\` must be an integer >= ${min} (character offset); got "${s}"` } };
|
|
37
|
+
}
|
|
38
|
+
return { value: n };
|
|
39
|
+
}
|
|
40
|
+
|
|
25
41
|
export default defineOperation("getAgentGuide", ({ query, req }, app) => {
|
|
26
42
|
if (SECRET && req.headers.get("x-hook-secret") !== SECRET) {
|
|
27
43
|
app.log.warn("getAgentGuide rejected: missing/invalid shared secret");
|
|
@@ -46,9 +62,23 @@ export default defineOperation("getAgentGuide", ({ query, req }, app) => {
|
|
|
46
62
|
};
|
|
47
63
|
}
|
|
48
64
|
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
65
|
+
// Pagination window (issue #740): a caller engages it by passing `start` and/or `length` (character
|
|
66
|
+
// offsets). Absent both, the whole section is returned unchanged (byte-for-byte identical to before).
|
|
67
|
+
const startArg = parsePageArg(query.start, "start", 0);
|
|
68
|
+
const lengthArg = parsePageArg(query.length, "length", 1);
|
|
69
|
+
const pageIssues = [startArg.issue, lengthArg.issue].filter((i): i is { path: string; message: string } => i !== undefined);
|
|
70
|
+
if (pageIssues.length > 0) {
|
|
71
|
+
app.log.warn("getAgentGuide rejected: invalid pagination args", { issues: pageIssues.length });
|
|
72
|
+
return { status: 400, body: { error: "invalid pagination arguments", issues: pageIssues } };
|
|
73
|
+
}
|
|
74
|
+
const paginate = startArg.value !== undefined || lengthArg.value !== undefined;
|
|
75
|
+
|
|
76
|
+
// A section id → just that section (or a bounded page of it), or a 400 that names the valid ids.
|
|
77
|
+
const chunk = paginate ? renderGuideSectionChunk(section, baseUrl, startArg.value ?? 0, lengthArg.value ?? GUIDE_SECTION_PAGE_DEFAULT) : undefined;
|
|
78
|
+
// The resolved body text: a page's slice when paginating, else the whole section. `undefined` from
|
|
79
|
+
// either path means the section id is unknown → the uniform 400 below.
|
|
80
|
+
const resolved = chunk ? chunk.instructions : paginate ? undefined : renderGuideSection(section, baseUrl);
|
|
81
|
+
if (resolved === undefined) {
|
|
52
82
|
// Derive the valid ids from the PARSED table of contents — the sections this deployment can
|
|
53
83
|
// actually serve — not the static registry. When the guide doc is unreadable (RAW_GUIDE
|
|
54
84
|
// fallback, no `##` headings) the TOC is empty and NO id is retrievable, so say so explicitly
|
|
@@ -85,7 +115,12 @@ export default defineOperation("getAgentGuide", ({ query, req }, app) => {
|
|
|
85
115
|
id: section,
|
|
86
116
|
title,
|
|
87
117
|
format: "markdown",
|
|
88
|
-
instructions,
|
|
118
|
+
instructions: resolved,
|
|
119
|
+
// Pagination cursor state — present ONLY when the caller engaged a window, so an un-paginated
|
|
120
|
+
// `section=<id>` call's body stays byte-for-byte identical (issue #740).
|
|
121
|
+
...(chunk
|
|
122
|
+
? { start: chunk.start, length: chunk.length, totalLength: chunk.totalLength, nextStart: chunk.nextStart }
|
|
123
|
+
: {}),
|
|
89
124
|
},
|
|
90
125
|
},
|
|
91
126
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.181.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",
|