@nanobpm/nano-workforce 0.109.0 → 0.111.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 +14 -0
- package/app/contracts.ts +9 -0
- package/app/deliveryGraph.test.ts +357 -0
- package/app/deliveryGraph.ts +463 -0
- package/app/github.ts +22 -0
- package/app/readiness.test.ts +160 -0
- package/app/readiness.ts +163 -3
- package/openapi.yaml +248 -3
- package/package.json +1 -1
- package/resources/processes/readiness-gate.bpmn +3 -0
package/app/readiness.ts
CHANGED
|
@@ -18,14 +18,28 @@
|
|
|
18
18
|
// any credential is read at execution time from the typed env-contract (`credentialEnv` names a
|
|
19
19
|
// declared {@link EnvKey}; ADR 0004 pinned decision 2) and is redacted from every log line.
|
|
20
20
|
import { isEnvKey, readEnv, readEnvOr } from "./contracts.ts";
|
|
21
|
+
import { allCheckNames, classifyMergeability, failingCheckNames, type PrState, pendingCheckNames } from "./github.ts";
|
|
21
22
|
import { isoDuration, isoDurationToMs } from "./reviewWait.ts";
|
|
22
23
|
|
|
23
24
|
/** The built-in readiness sources. `command` is the escape hatch that subsumes the long tail
|
|
24
25
|
* (`gh`, `curl`, `docker manifest inspect`, a custom probe) — adding a first-class kind later is
|
|
25
26
|
* an additive matcher, not a schema change. `capability` is the first such additive kind (#274):
|
|
26
27
|
* it resolves "which published version first carries capability C?" from the publish-provenance
|
|
27
|
-
* substrate and binds the discovered `pkg@version` back through the gate (see {@link matchCapability}).
|
|
28
|
-
|
|
28
|
+
* substrate and binds the discovered `pkg@version` back through the gate (see {@link matchCapability}).
|
|
29
|
+
* `pr` (ADR 0005 §2) is the merge-state kind: it lifts the PR-liveness/mergeability READ out of the
|
|
30
|
+
* merge loop (`app/mergeProtocol.ts` / `app/github.ts`) into a first-class probe so "watch an
|
|
31
|
+
* in-flight PR reach a declared state" is a graph edge, not logic buried in the merge-loop node
|
|
32
|
+
* body — the ACTION (landing the PR) stays in that node body; this kind only OBSERVES. */
|
|
33
|
+
export type ProbeKind = "http" | "command" | "npm" | "github-check" | "capability" | "pr";
|
|
34
|
+
|
|
35
|
+
/** The declared PR state a `pr` probe waits for (ADR 0005 §2). Each is a discovered fact about an
|
|
36
|
+
* in-flight PR, read from its live GitHub state and evaluated by {@link matchPr}:
|
|
37
|
+
* • `ready` — the PR is out of draft (the draft→ready transition is observable).
|
|
38
|
+
* • `merged` — the PR has landed; binds `mergedSha` (the merge commit) as an output.
|
|
39
|
+
* • `mergeable` — GitHub reports the PR as landable now ({@link classifyMergeability} `ready`:
|
|
40
|
+
* CLEAN/HAS_HOOKS/UNSTABLE/BEHIND — required review + checks satisfied).
|
|
41
|
+
* • `checks-green` — every head check run is complete with none failing (required checks green). */
|
|
42
|
+
export type PrCondition = "ready" | "merged" | "mergeable" | "checks-green";
|
|
29
43
|
|
|
30
44
|
/** What the gate does when the bounded wait times out (the engine timer arm fires). */
|
|
31
45
|
export type OnTimeout = "escalate" | "fail" | "continue";
|
|
@@ -33,9 +47,10 @@ export type OnTimeout = "escalate" | "fail" | "continue";
|
|
|
33
47
|
/** Backoff policy between poll attempts. */
|
|
34
48
|
export type Backoff = "fixed" | "exponential";
|
|
35
49
|
|
|
36
|
-
const PROBE_KINDS: readonly ProbeKind[] = ["http", "command", "npm", "github-check", "capability"];
|
|
50
|
+
const PROBE_KINDS: readonly ProbeKind[] = ["http", "command", "npm", "github-check", "capability", "pr"];
|
|
37
51
|
const ON_TIMEOUTS: readonly OnTimeout[] = ["escalate", "fail", "continue"];
|
|
38
52
|
const BACKOFFS: readonly Backoff[] = ["fixed", "exponential"];
|
|
53
|
+
const PR_CONDITIONS: readonly PrCondition[] = ["ready", "merged", "mergeable", "checks-green"];
|
|
39
54
|
|
|
40
55
|
/** The per-kind readiness predicate. Every field is optional; each kind reads only the ones it
|
|
41
56
|
* understands and applies a sensible default when a field is absent (see the matchers below). */
|
|
@@ -66,6 +81,9 @@ export interface ProbeMatch {
|
|
|
66
81
|
* unset, the capability edge is deterministic-only. The resolved `pkg@version` and bare version
|
|
67
82
|
* are exposed to the command as `RESOLVED_ARTIFACT` / `RESOLVED_VERSION`. */
|
|
68
83
|
readonly verifyCommand?: string;
|
|
84
|
+
/** pr: the declared PR state the probe waits for (default `merged`). One of {@link PrCondition} —
|
|
85
|
+
* `ready` (out of draft), `merged`, `mergeable`, or `checks-green`. */
|
|
86
|
+
readonly prState?: PrCondition;
|
|
69
87
|
}
|
|
70
88
|
|
|
71
89
|
/** The poll cadence: how often to re-probe, how long to keep trying, and the backoff shape. */
|
|
@@ -207,6 +225,13 @@ export function parseProbe(raw: unknown): ReadinessProbe {
|
|
|
207
225
|
throw new Error("readiness probe (capability): 'match.package' is required (provenance is per-package scoped)");
|
|
208
226
|
}
|
|
209
227
|
}
|
|
228
|
+
// A pr edge whose target names no numeric PR id can never resolve — fail loudly at parse (mirroring
|
|
229
|
+
// the capability ref guard) rather than surface it as a timeout much later. `owner/repo#123`.
|
|
230
|
+
if (kind === "pr" && !parsePrTarget(target)) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`readiness probe (pr): 'target' ('${target}') must be an 'owner/repo#<number>' PR reference (e.g. 'nanobpm/nano-workforce#377')`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
210
235
|
const poll = isRecord(raw.poll) ? parsePoll(raw.poll) : undefined;
|
|
211
236
|
const credentialEnv = str(raw.credentialEnv).trim() || undefined;
|
|
212
237
|
if (credentialEnv !== undefined && !isEnvKey(credentialEnv)) {
|
|
@@ -259,9 +284,27 @@ function parseMatch(raw: Record<string, unknown>): ProbeMatch {
|
|
|
259
284
|
capabilityRef: str(raw.capabilityRef).trim() || undefined,
|
|
260
285
|
package: str(raw.package).trim() || undefined,
|
|
261
286
|
verifyCommand: str(raw.verifyCommand).trim() || undefined,
|
|
287
|
+
prState: parsePrCondition(raw.prState),
|
|
262
288
|
};
|
|
263
289
|
}
|
|
264
290
|
|
|
291
|
+
/** Narrow a raw `match.prState` to a {@link PrCondition}, throwing on a non-empty unknown value so a
|
|
292
|
+
* mistyped state (`"landed"` for `"merged"`) fails loudly at parse rather than waiting forever. An
|
|
293
|
+
* absent/blank value yields undefined — `matchPr` then applies the `merged` default. */
|
|
294
|
+
function parsePrCondition(raw: unknown): PrCondition | undefined {
|
|
295
|
+
const s = str(raw).trim();
|
|
296
|
+
if (s === "") return undefined;
|
|
297
|
+
if (!isPrCondition(s)) {
|
|
298
|
+
throw new Error(`readiness probe (pr): invalid match.prState '${s}' (expected one of ${PR_CONDITIONS.join(", ")})`);
|
|
299
|
+
}
|
|
300
|
+
return s;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function isPrCondition(v: string): v is PrCondition {
|
|
304
|
+
for (const c of PR_CONDITIONS) if (c === v) return true;
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
|
|
265
308
|
function parsePoll(raw: Record<string, unknown>): ProbePoll {
|
|
266
309
|
const backoffRaw = str(raw.backoff).trim();
|
|
267
310
|
if (backoffRaw !== "" && !isBackoff(backoffRaw)) {
|
|
@@ -485,6 +528,116 @@ export function githubReleasesCommand(repo: string): string {
|
|
|
485
528
|
return `gh api --paginate --slurp ${shellQuote(`repos/${repo}/releases?per_page=100`)} -H ${shellQuote("Accept: application/vnd.github+json")}`;
|
|
486
529
|
}
|
|
487
530
|
|
|
531
|
+
// ── PR / merge-state probe (ADR 0005 §2 — lift the merge-loop READ into a first-class probe) ─────
|
|
532
|
+
|
|
533
|
+
/** A live PR observation, reduced to the fields {@link matchPr} reads. It extends the shared
|
|
534
|
+
* {@link PrState} (so `classifyMergeability` and the merge loop's liveness vocabulary are reused
|
|
535
|
+
* verbatim, never re-derived) and adds `mergedSha`, the merge commit oid a `merged` match binds
|
|
536
|
+
* downstream. Kept separate from I/O — {@link parsePrView} builds it from an already-fetched
|
|
537
|
+
* `gh pr view --json …` payload — so the matcher stays pure/unit-testable, exactly like
|
|
538
|
+
* {@link GithubRelease}. */
|
|
539
|
+
export interface PrObservation extends PrState {
|
|
540
|
+
/** The merge commit oid once landed (`gh pr view --json mergeCommit`), else null. */
|
|
541
|
+
readonly mergedSha: string | null;
|
|
542
|
+
/** Count of head checks still in flight (queued/in progress), so a `checks-green` gate never
|
|
543
|
+
* reports green while a run hasn't concluded. Derived via `pendingCheckNames`. */
|
|
544
|
+
readonly pendingChecks: number;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** Parse a raw `gh pr view --json state,mergedAt,mergeStateStatus,statusCheckRollup,isDraft,headRefOid,mergeCommit`
|
|
548
|
+
* payload (already JSON-decoded) into a {@link PrObservation}. Reuses the SAME check-rollup readers
|
|
549
|
+
* as `fetchPrState` (`failingCheckNames`/`allCheckNames`) so a superseded/cancelled run is collapsed
|
|
550
|
+
* identically. Tolerant: a malformed/empty payload yields an all-open, no-checks observation, so a
|
|
551
|
+
* transient read degrades to "not ready" (keep waiting), never a throw. */
|
|
552
|
+
export function parsePrView(payload: unknown): PrObservation {
|
|
553
|
+
const j = isRecord(payload) ? payload : {};
|
|
554
|
+
const rollup = Array.isArray(j.statusCheckRollup) ? j.statusCheckRollup : [];
|
|
555
|
+
const names = failingCheckNames(rollup);
|
|
556
|
+
const pending = pendingCheckNames(rollup);
|
|
557
|
+
const merged = str(j.state).toUpperCase() === "MERGED" || str(j.mergedAt).trim() !== "";
|
|
558
|
+
const mergeCommit = isRecord(j.mergeCommit) ? j.mergeCommit : undefined;
|
|
559
|
+
const mergedSha = mergeCommit && str(mergeCommit.oid).trim() !== "" ? str(mergeCommit.oid).trim() : null;
|
|
560
|
+
return {
|
|
561
|
+
merged,
|
|
562
|
+
state: merged ? "merged" : str(j.state).toUpperCase() === "CLOSED" ? "closed" : "open",
|
|
563
|
+
mergeStateStatus: (str(j.mergeStateStatus) || "UNKNOWN").toUpperCase(),
|
|
564
|
+
failingChecks: names.length,
|
|
565
|
+
failingCheckNames: names,
|
|
566
|
+
totalChecks: rollup.length,
|
|
567
|
+
presentCheckNames: allCheckNames(rollup),
|
|
568
|
+
isDraft: j.isDraft === true,
|
|
569
|
+
headRefOid: str(j.headRefOid).trim() || null,
|
|
570
|
+
mergedSha,
|
|
571
|
+
pendingChecks: pending.length,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** pr readiness (ADR 0005 §2): does the observed PR satisfy the declared `match.prState`
|
|
576
|
+
* (default `merged`)? PURE — it operates on an already-fetched {@link PrObservation} and NEVER
|
|
577
|
+
* throws, so a transient/garbled read is simply "not ready yet". A `merged` match binds the merge
|
|
578
|
+
* commit as `{ mergedSha }` (mirroring the `capability` kind's `resolvedArtifact` bind) so a
|
|
579
|
+
* downstream edge can pin the exact landed commit. The merge ACTION stays in the merge-loop node
|
|
580
|
+
* body — this kind only OBSERVES. */
|
|
581
|
+
export function matchPr(match: ProbeMatch | undefined, pr: PrObservation): ProbeResult {
|
|
582
|
+
const want: PrCondition = match?.prState ?? "merged";
|
|
583
|
+
switch (want) {
|
|
584
|
+
case "ready": {
|
|
585
|
+
// draft→ready: a non-draft PR (already-merged PRs are non-draft too, so they also satisfy it).
|
|
586
|
+
const ready = !pr.isDraft;
|
|
587
|
+
return { ready, detail: `pr ${ready ? "ready (not draft)" : "still draft"}` };
|
|
588
|
+
}
|
|
589
|
+
case "merged": {
|
|
590
|
+
if (!pr.merged) return { ready: false, detail: "pr not merged yet" };
|
|
591
|
+
const bind = pr.mergedSha ? { mergedSha: pr.mergedSha } : undefined;
|
|
592
|
+
return { ready: true, detail: `pr merged${pr.mergedSha ? ` (${pr.mergedSha})` : ""}`, bind };
|
|
593
|
+
}
|
|
594
|
+
case "mergeable": {
|
|
595
|
+
const m = classifyMergeability(pr);
|
|
596
|
+
const ready = m === "ready";
|
|
597
|
+
return { ready, detail: `pr mergeability ${m}` };
|
|
598
|
+
}
|
|
599
|
+
case "checks-green": {
|
|
600
|
+
// Required checks green: at least one head run exists, none failing, AND none still in flight.
|
|
601
|
+
// A queued/in-progress run has no failing conclusion, so counting only `failingChecks` would
|
|
602
|
+
// report green while checks are still running — `pendingChecks` closes that gap. `failingChecks
|
|
603
|
+
// < 0` is token mode (checks unenumerable) — stay conservative (not ready), never falsely green.
|
|
604
|
+
const ready = pr.failingChecks === 0 && pr.pendingChecks === 0 && pr.totalChecks > 0;
|
|
605
|
+
const detail =
|
|
606
|
+
pr.totalChecks < 0
|
|
607
|
+
? "pr checks unenumerable (not ready)"
|
|
608
|
+
: pr.totalChecks === 0
|
|
609
|
+
? "pr no checks yet"
|
|
610
|
+
: pr.failingChecks > 0
|
|
611
|
+
? `pr checks ${pr.failingChecks} failing`
|
|
612
|
+
: pr.pendingChecks > 0
|
|
613
|
+
? `pr checks ${pr.pendingChecks} pending`
|
|
614
|
+
: "pr checks green";
|
|
615
|
+
return { ready, detail };
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/** Split an `owner/repo#123` PR reference into its repo + numeric PR number, or `null` when it
|
|
621
|
+
* carries no numeric id (so `parseProbe` can reject a never-resolvable target loudly). The `#`
|
|
622
|
+
* separator is the canonical — and only — PR handle: an `@N` form is deliberately NOT accepted, as
|
|
623
|
+
* `owner/repo@<ref>` is the repo-ref syntax used elsewhere (`parseRepoRef`), so a numeric `@N` there
|
|
624
|
+
* would ambiguously mis-parse a git ref as a PR number. Matches the OpenAPI contract + `parseProbe`
|
|
625
|
+
* error, both of which document `owner/repo#N` only. */
|
|
626
|
+
export function parsePrTarget(target: string): { repo: string; number: string } | null {
|
|
627
|
+
const t = target.trim();
|
|
628
|
+
const m = t.match(/^(.+?)#(\d+)$/);
|
|
629
|
+
if (!m) return null;
|
|
630
|
+
const repo = m[1].trim();
|
|
631
|
+
if (repo === "") return null;
|
|
632
|
+
return { repo, number: m[2] };
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/** Build the `gh pr view` command that reads a PR's merge-state fields. `gh` reads its token from the
|
|
636
|
+
* ambient env (like `github-check`/`capability`) — no `credentialEnv`. */
|
|
637
|
+
export function prViewCommand(repo: string, number: string): string {
|
|
638
|
+
return `gh pr view ${shellQuote(number)} --repo ${shellQuote(repo)} --json ${shellQuote("state,mergedAt,mergeStateStatus,statusCheckRollup,isDraft,headRefOid,mergeCommit")}`;
|
|
639
|
+
}
|
|
640
|
+
|
|
488
641
|
|
|
489
642
|
// ── Single probe attempt (does I/O via the injected {@link ProbeExec}) ──────────────────────────
|
|
490
643
|
|
|
@@ -520,6 +673,13 @@ export async function probeOnce(
|
|
|
520
673
|
if (out.code !== 0) return { ready: false, detail: "capability: gh api failed (not ready)" };
|
|
521
674
|
return matchCapability(probe.match, parseReleases(parseJson(out.stdout)));
|
|
522
675
|
}
|
|
676
|
+
case "pr": {
|
|
677
|
+
const ref = parsePrTarget(probe.target);
|
|
678
|
+
if (!ref) return { ready: false, detail: "pr: unparseable target (not ready)" };
|
|
679
|
+
const out = await exec.run(prViewCommand(ref.repo, ref.number), env);
|
|
680
|
+
if (out.code !== 0) return { ready: false, detail: "pr: gh pr view failed (not ready)" };
|
|
681
|
+
return matchPr(probe.match, parsePrView(parseJson(out.stdout)));
|
|
682
|
+
}
|
|
523
683
|
}
|
|
524
684
|
}
|
|
525
685
|
|
package/openapi.yaml
CHANGED
|
@@ -1217,12 +1217,12 @@ components:
|
|
|
1217
1217
|
properties:
|
|
1218
1218
|
kind:
|
|
1219
1219
|
type: string
|
|
1220
|
-
enum: [http, command, npm, github-check, capability]
|
|
1221
|
-
description: The readiness source. `command` is the escape hatch; `capability` resolves a cross-repo published-artifact edge.
|
|
1220
|
+
enum: [http, command, npm, github-check, capability, pr]
|
|
1221
|
+
description: The readiness source. `command` is the escape hatch; `capability` resolves a cross-repo published-artifact edge; `pr` watches an in-flight PR's merge state (ADR 0005 §2).
|
|
1222
1222
|
target:
|
|
1223
1223
|
type: string
|
|
1224
1224
|
minLength: 1
|
|
1225
|
-
description: The kind-specific target (a URL, a shell command, a `pkg@version`, an `owner/repo@ref`,
|
|
1225
|
+
description: The kind-specific target (a URL, a shell command, a `pkg@version`, an `owner/repo@ref`, `github-releases:owner/repo`, or an `owner/repo#123` PR reference for the `pr` kind).
|
|
1226
1226
|
onTimeout:
|
|
1227
1227
|
type: string
|
|
1228
1228
|
enum: [escalate, fail, continue]
|
|
@@ -1245,6 +1245,7 @@ components:
|
|
|
1245
1245
|
capabilityRef: { type: string, description: "capability: the upstream issue/PR handle the resolved version must carry." }
|
|
1246
1246
|
package: { type: string, description: "capability: the package whose releases are scanned for provenance." }
|
|
1247
1247
|
verifyCommand: { type: string, description: "capability: optional empirical verifier run once at the gate boundary." }
|
|
1248
|
+
prState: { type: string, enum: [ready, merged, mergeable, checks-green], description: "pr: the declared PR state to wait for (default merged)." }
|
|
1248
1249
|
poll:
|
|
1249
1250
|
type: object
|
|
1250
1251
|
additionalProperties: false
|
|
@@ -1253,6 +1254,250 @@ components:
|
|
|
1253
1254
|
everyMs: { type: integer, description: Interval between poll attempts (ms). }
|
|
1254
1255
|
timeoutMs: { type: integer, description: Bounded budget (ms) before the gate escalates. }
|
|
1255
1256
|
backoff: { type: string, enum: [fixed, exponential], description: Backoff shape between attempts. }
|
|
1257
|
+
DeliveryGraph:
|
|
1258
|
+
description: >-
|
|
1259
|
+
An agent-authored delivery graph (ADR 0005) — the SINGLE agent-facing artifact for a
|
|
1260
|
+
heterogeneous, partly-human, cross-repo delivery runbook. It is DATA, never an executable
|
|
1261
|
+
artifact: a JSON DAG whose nodes each name a `kind` from a CLOSED allowlist
|
|
1262
|
+
(`agent`/`wait`/`human`/`connector` — Decision 1/2, the trust boundary) and whose `edges`
|
|
1263
|
+
name DISCOVERED facts (Decision 3). Ingest validates the SHAPE here and the SEMANTICS
|
|
1264
|
+
(acyclicity, edge integrity, fact resolution) in the pure `validateDeliveryGraph`
|
|
1265
|
+
(`app/deliveryGraph.ts`). This slice (S0) defines the vocabulary + validation surface ONLY —
|
|
1266
|
+
no compiler, dispatch, or execution (those land in later slices).
|
|
1267
|
+
type: object
|
|
1268
|
+
additionalProperties: false
|
|
1269
|
+
required:
|
|
1270
|
+
- nodes
|
|
1271
|
+
properties:
|
|
1272
|
+
name:
|
|
1273
|
+
type: string
|
|
1274
|
+
maxLength: 255
|
|
1275
|
+
description: OPTIONAL human-readable label for the graph (shown in the rendered preview).
|
|
1276
|
+
nodes:
|
|
1277
|
+
type: array
|
|
1278
|
+
minItems: 1
|
|
1279
|
+
maxItems: 256
|
|
1280
|
+
items:
|
|
1281
|
+
$ref: "#/components/schemas/DeliveryNode"
|
|
1282
|
+
description: >-
|
|
1283
|
+
The graph's nodes. Each carries a unique `id` and a `kind` from the closed allowlist,
|
|
1284
|
+
plus its per-kind config and its typed `emits[]` declaration. Node ids must be unique
|
|
1285
|
+
across the graph (enforced by `validateDeliveryGraph`).
|
|
1286
|
+
edges:
|
|
1287
|
+
type: array
|
|
1288
|
+
maxItems: 1024
|
|
1289
|
+
items:
|
|
1290
|
+
$ref: "#/components/schemas/DeliveryEdge"
|
|
1291
|
+
description: >-
|
|
1292
|
+
The dependency edges — the graph's discovered-fact topology (Decision 3). Each edge means
|
|
1293
|
+
"`to` proceeds once fact `from` about the upstream node is observable". `from` is either a
|
|
1294
|
+
bare `<nodeId>` (the degenerate "wait for the upstream node's completion" fact) or a
|
|
1295
|
+
qualified `<nodeId>.<fact>` referencing one of that node's declared `emits`. Omit/`[]` for
|
|
1296
|
+
a set of independent (root) nodes. The edge set must be a DAG.
|
|
1297
|
+
DeliveryNode:
|
|
1298
|
+
description: >-
|
|
1299
|
+
One node in a delivery graph. A discriminated union on `kind` over the CLOSED allowlist; the
|
|
1300
|
+
matching per-kind config object (`agent`/`wait`/`connector`) is REQUIRED and names the
|
|
1301
|
+
engine-native body the node delegates to (Decision 2 — the graph schedules, it does not
|
|
1302
|
+
re-implement execution). The `human` config is the sole exception — it is OPTIONAL (a bare
|
|
1303
|
+
`human` node resolves to a generic emit-capturing form fallback in S3).
|
|
1304
|
+
oneOf:
|
|
1305
|
+
- $ref: "#/components/schemas/DeliveryNodeAgent"
|
|
1306
|
+
- $ref: "#/components/schemas/DeliveryNodeWait"
|
|
1307
|
+
- $ref: "#/components/schemas/DeliveryNodeHuman"
|
|
1308
|
+
- $ref: "#/components/schemas/DeliveryNodeConnector"
|
|
1309
|
+
discriminator:
|
|
1310
|
+
propertyName: kind
|
|
1311
|
+
mapping:
|
|
1312
|
+
agent: "#/components/schemas/DeliveryNodeAgent"
|
|
1313
|
+
wait: "#/components/schemas/DeliveryNodeWait"
|
|
1314
|
+
human: "#/components/schemas/DeliveryNodeHuman"
|
|
1315
|
+
connector: "#/components/schemas/DeliveryNodeConnector"
|
|
1316
|
+
DeliveryFact:
|
|
1317
|
+
description: >-
|
|
1318
|
+
A typed output a node declares it will EMIT (ADR 0005 Decision 3/4 — emitted-fact typing).
|
|
1319
|
+
A downstream edge references it as `from: "<nodeId>.<fact>"`, so a bind is validated against
|
|
1320
|
+
this declaration, not stringly. A "click done" human node or a pass-through node declares no
|
|
1321
|
+
facts (`emits` absent/empty) — the degenerate no-emit case.
|
|
1322
|
+
type: object
|
|
1323
|
+
additionalProperties: false
|
|
1324
|
+
required:
|
|
1325
|
+
- name
|
|
1326
|
+
- type
|
|
1327
|
+
properties:
|
|
1328
|
+
name:
|
|
1329
|
+
type: string
|
|
1330
|
+
minLength: 1
|
|
1331
|
+
maxLength: 128
|
|
1332
|
+
pattern: '^[A-Za-z_][A-Za-z0-9_]*$'
|
|
1333
|
+
description: The fact's identifier, referenced downstream as `<nodeId>.<name>`. Must be unique within the node.
|
|
1334
|
+
type:
|
|
1335
|
+
type: string
|
|
1336
|
+
enum: [string, number, boolean, artifact, version, url]
|
|
1337
|
+
description: >-
|
|
1338
|
+
The fact's declared type. `artifact` is a `pkg@version` handle, `version` a bare version
|
|
1339
|
+
string, `url` a location — mirrors the values `capability`/`pr` probes late-bind.
|
|
1340
|
+
description:
|
|
1341
|
+
type: string
|
|
1342
|
+
maxLength: 512
|
|
1343
|
+
description: OPTIONAL human note describing what the fact carries.
|
|
1344
|
+
DeliveryNodeCommon:
|
|
1345
|
+
type: object
|
|
1346
|
+
properties:
|
|
1347
|
+
id:
|
|
1348
|
+
type: string
|
|
1349
|
+
minLength: 1
|
|
1350
|
+
maxLength: 128
|
|
1351
|
+
pattern: '^[A-Za-z_][A-Za-z0-9_.-]*$'
|
|
1352
|
+
description: The node's identifier, unique within the graph and referenced by edges.
|
|
1353
|
+
emits:
|
|
1354
|
+
type: array
|
|
1355
|
+
maxItems: 32
|
|
1356
|
+
items:
|
|
1357
|
+
$ref: "#/components/schemas/DeliveryFact"
|
|
1358
|
+
description: >-
|
|
1359
|
+
The typed facts this node hands forward when it completes (Decision 3/4). Absent/empty for
|
|
1360
|
+
a node that emits nothing. Downstream edges bind these via `from: "<nodeId>.<fact>"`.
|
|
1361
|
+
DeliveryNodeAgent:
|
|
1362
|
+
description: >-
|
|
1363
|
+
An `agent` node — a worker executes an agent job type (the existing fan-out body). Bounded
|
|
1364
|
+
(timeout → escalate) and resumable like every node.
|
|
1365
|
+
allOf:
|
|
1366
|
+
- $ref: "#/components/schemas/DeliveryNodeCommon"
|
|
1367
|
+
- type: object
|
|
1368
|
+
additionalProperties: false
|
|
1369
|
+
required:
|
|
1370
|
+
- id
|
|
1371
|
+
- kind
|
|
1372
|
+
- agent
|
|
1373
|
+
properties:
|
|
1374
|
+
id: { type: string }
|
|
1375
|
+
kind: { type: string, enum: [agent] }
|
|
1376
|
+
emits: { type: array, items: { $ref: "#/components/schemas/DeliveryFact" } }
|
|
1377
|
+
agent:
|
|
1378
|
+
type: object
|
|
1379
|
+
additionalProperties: false
|
|
1380
|
+
required:
|
|
1381
|
+
- jobType
|
|
1382
|
+
properties:
|
|
1383
|
+
jobType:
|
|
1384
|
+
type: string
|
|
1385
|
+
minLength: 1
|
|
1386
|
+
description: The agent job type a worker executes for this node (e.g. `senior:feature`).
|
|
1387
|
+
prompt:
|
|
1388
|
+
type: string
|
|
1389
|
+
maxLength: 20000
|
|
1390
|
+
description: OPTIONAL steering prompt appended to the node's job brief.
|
|
1391
|
+
DeliveryNodeWait:
|
|
1392
|
+
description: >-
|
|
1393
|
+
A `wait` node — a durable `ReadinessProbe` (ADR 0001 §2) watching an external fact. Reuses the
|
|
1394
|
+
existing `ReadinessProbe` shape verbatim (Decision 3 — never a second wait loop); the `pr`
|
|
1395
|
+
merge-state kind is added to that shape by slice S2 and flows in here automatically.
|
|
1396
|
+
allOf:
|
|
1397
|
+
- $ref: "#/components/schemas/DeliveryNodeCommon"
|
|
1398
|
+
- type: object
|
|
1399
|
+
additionalProperties: false
|
|
1400
|
+
required:
|
|
1401
|
+
- id
|
|
1402
|
+
- kind
|
|
1403
|
+
- wait
|
|
1404
|
+
properties:
|
|
1405
|
+
id: { type: string }
|
|
1406
|
+
kind: { type: string, enum: [wait] }
|
|
1407
|
+
emits: { type: array, items: { $ref: "#/components/schemas/DeliveryFact" } }
|
|
1408
|
+
wait:
|
|
1409
|
+
$ref: "#/components/schemas/ReadinessProbe"
|
|
1410
|
+
DeliveryNodeHuman:
|
|
1411
|
+
description: >-
|
|
1412
|
+
A `human` node — a scheduled user task + form (ADR 0002 machinery promoted from exception to
|
|
1413
|
+
node, Decision 4). Surfaces "now do X" on the Tasks inbox, blocks dependents, is answerable by
|
|
1414
|
+
a human OR an agent, is SLA-bounded, and can EMIT a typed fact its form captures.
|
|
1415
|
+
allOf:
|
|
1416
|
+
- $ref: "#/components/schemas/DeliveryNodeCommon"
|
|
1417
|
+
- type: object
|
|
1418
|
+
additionalProperties: false
|
|
1419
|
+
required:
|
|
1420
|
+
- id
|
|
1421
|
+
- kind
|
|
1422
|
+
properties:
|
|
1423
|
+
id: { type: string }
|
|
1424
|
+
kind: { type: string, enum: [human] }
|
|
1425
|
+
emits: { type: array, items: { $ref: "#/components/schemas/DeliveryFact" } }
|
|
1426
|
+
human:
|
|
1427
|
+
type: object
|
|
1428
|
+
additionalProperties: false
|
|
1429
|
+
description: >-
|
|
1430
|
+
OPTIONAL human-node config. `formKey` explicitly attaches a form (else a form is
|
|
1431
|
+
selected by node category, else a generic emit-capturing fallback — resolved in S3).
|
|
1432
|
+
The node's typed output is declared via the node-level `emits[]`.
|
|
1433
|
+
properties:
|
|
1434
|
+
formKey:
|
|
1435
|
+
type: string
|
|
1436
|
+
minLength: 1
|
|
1437
|
+
description: OPTIONAL explicit form to attach at authoring time (specific-else-generic resolution, S3).
|
|
1438
|
+
prompt:
|
|
1439
|
+
type: string
|
|
1440
|
+
maxLength: 20000
|
|
1441
|
+
description: OPTIONAL instruction shown to the human/agent completing the task ("now do X").
|
|
1442
|
+
DeliveryNodeConnector:
|
|
1443
|
+
description: >-
|
|
1444
|
+
A `connector` node — an automated, side-effecting outbound action (the connector I/O surface).
|
|
1445
|
+
Side-effecting, so it carries a `dedupeKey` and tolerates at-least-once execution. The
|
|
1446
|
+
`payload` schema is a minimal forward-declared stub in this slice (ADR 0005 non-goal — the
|
|
1447
|
+
concrete connector I/O lands later).
|
|
1448
|
+
allOf:
|
|
1449
|
+
- $ref: "#/components/schemas/DeliveryNodeCommon"
|
|
1450
|
+
- type: object
|
|
1451
|
+
additionalProperties: false
|
|
1452
|
+
required:
|
|
1453
|
+
- id
|
|
1454
|
+
- kind
|
|
1455
|
+
- connector
|
|
1456
|
+
properties:
|
|
1457
|
+
id: { type: string }
|
|
1458
|
+
kind: { type: string, enum: [connector] }
|
|
1459
|
+
emits: { type: array, items: { $ref: "#/components/schemas/DeliveryFact" } }
|
|
1460
|
+
connector:
|
|
1461
|
+
type: object
|
|
1462
|
+
additionalProperties: false
|
|
1463
|
+
required:
|
|
1464
|
+
- target
|
|
1465
|
+
properties:
|
|
1466
|
+
target:
|
|
1467
|
+
type: string
|
|
1468
|
+
minLength: 1
|
|
1469
|
+
description: The connector action target (forward-declared — the concrete scheme lands in a later slice).
|
|
1470
|
+
dedupeKey:
|
|
1471
|
+
type: string
|
|
1472
|
+
minLength: 1
|
|
1473
|
+
description: >-
|
|
1474
|
+
OPTIONAL idempotency key so an at-least-once resume cannot double-fire this
|
|
1475
|
+
side-effecting node (ADR 0005 Decision 7). Author-supplied or graph-derived.
|
|
1476
|
+
payload:
|
|
1477
|
+
type: object
|
|
1478
|
+
additionalProperties: true
|
|
1479
|
+
description: Minimal forward-declared payload stub — the concrete connector payload schema is deferred (ADR non-goal).
|
|
1480
|
+
DeliveryEdge:
|
|
1481
|
+
description: >-
|
|
1482
|
+
A dependency edge — "`to` proceeds once fact `from` is observable" (ADR 0005 Decision 3).
|
|
1483
|
+
`from` is either a bare `<nodeId>` (wait for the upstream node's completion fact) or a
|
|
1484
|
+
qualified `<nodeId>.<fact>` referencing a declared `emits` fact of that node. Both endpoints
|
|
1485
|
+
must resolve to a node in the graph, the referenced fact must be declared, and the whole edge
|
|
1486
|
+
set must be a DAG — all enforced by `validateDeliveryGraph`.
|
|
1487
|
+
type: object
|
|
1488
|
+
additionalProperties: false
|
|
1489
|
+
required:
|
|
1490
|
+
- from
|
|
1491
|
+
- to
|
|
1492
|
+
properties:
|
|
1493
|
+
from:
|
|
1494
|
+
type: string
|
|
1495
|
+
minLength: 1
|
|
1496
|
+
description: The upstream endpoint — `<nodeId>` (completion) or `<nodeId>.<fact>` (a declared emitted fact).
|
|
1497
|
+
to:
|
|
1498
|
+
type: string
|
|
1499
|
+
minLength: 1
|
|
1500
|
+
description: The dependent node's id — proceeds once `from` is observed.
|
|
1256
1501
|
FeatureStart:
|
|
1257
1502
|
description: The start-feature request body — a SINGLE-issue feature run. Names the target issue
|
|
1258
1503
|
by EXACTLY ONE of `issue` (an `owner/repo#123` reference) or `url` (a bare issue URL), plus a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.111.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",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
<nano:extend name="capabilityRef" type="string" optional="true" />
|
|
23
23
|
<nano:extend name="package" type="string" optional="true" />
|
|
24
24
|
<nano:extend name="verifyCommand" type="string" optional="true" />
|
|
25
|
+
<nano:extend name="prState" type="string" optional="true" />
|
|
25
26
|
</nano:shape>
|
|
26
27
|
<nano:shape id="ReadinessProbePoll" name="Readiness probe — poll policy">
|
|
27
28
|
<nano:extend name="everyMs" type="integer" optional="true" />
|
|
@@ -46,11 +47,13 @@
|
|
|
46
47
|
<nano:extend name="ready" type="boolean" />
|
|
47
48
|
<nano:extend name="detail" type="string" optional="true" />
|
|
48
49
|
<nano:extend name="resolvedArtifact" type="string" optional="true" />
|
|
50
|
+
<nano:extend name="mergedSha" type="string" optional="true" />
|
|
49
51
|
</nano:shape>
|
|
50
52
|
<nano:shape id="ReadinessReady" name="readiness-ready message payload">
|
|
51
53
|
<nano:extend name="ready" type="boolean" />
|
|
52
54
|
<nano:extend name="detail" type="string" optional="true" />
|
|
53
55
|
<nano:extend name="resolvedArtifact" type="string" optional="true" />
|
|
56
|
+
<nano:extend name="mergedSha" type="string" optional="true" />
|
|
54
57
|
</nano:shape>
|
|
55
58
|
</nano:shapes>
|
|
56
59
|
</bpmn:extensionElements>
|