pi-crew 0.11.4 → 0.11.6

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/README.md +2 -2
  3. package/dist/index.mjs +498 -492
  4. package/docs/actions-reference.md +22 -0
  5. package/docs/dynamic-workflows.md +37 -0
  6. package/index.ts +9 -5
  7. package/package.json +18 -14
  8. package/scripts/analyze-run.mjs +101 -14
  9. package/skills/model-routing-context/SKILL.md +4 -3
  10. package/skills/real-test-pi-crew/SKILL.md +10 -7
  11. package/src/config/env-vars.ts +6 -0
  12. package/src/extension/crew-vibes/index.ts +29 -10
  13. package/src/extension/pi-api.ts +1 -0
  14. package/src/extension/registration/lifecycle-handlers.ts +65 -3
  15. package/src/extension/team-tool/dispatch/index.ts +4 -3
  16. package/src/extension/team-tool/dispatch/status.ts +7 -3
  17. package/src/extension/team-tool/inspect.ts +90 -0
  18. package/src/extension/team-tool/respond.ts +54 -0
  19. package/src/extension/team-tool/status.ts +10 -0
  20. package/src/extension/team-tool.ts +393 -3
  21. package/src/extension/webhook-notify.ts +23 -1
  22. package/src/observability/exporters/otlp-exporter.ts +93 -11
  23. package/src/prompt/prompt-runtime.ts +13 -4
  24. package/src/runtime/agent-control.ts +1 -15
  25. package/src/runtime/background-runner.ts +3 -0
  26. package/src/runtime/broker/crew-broker.ts +12 -103
  27. package/src/runtime/broker/protocol/request-parsers.ts +9 -1
  28. package/src/runtime/broker/protocol/wait-resolve.ts +168 -0
  29. package/src/runtime/child-pi/child-pi-spawn.ts +5 -4
  30. package/src/runtime/dispatch-batch.ts +3 -1
  31. package/src/runtime/goal-workflow/dynamic-workflow-context.ts +111 -12
  32. package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +19 -0
  33. package/src/runtime/live-session/live-agent-manager.ts +35 -0
  34. package/src/runtime/live-session/live-session-runtime.ts +34 -5
  35. package/src/runtime/mcp-proxy.ts +110 -7
  36. package/src/runtime/model/pi-args.ts +14 -11
  37. package/src/runtime/model/provider-extensions.ts +67 -1
  38. package/src/runtime/recovery/crash-recovery.ts +6 -1
  39. package/src/runtime/scheduling/run-coalesced-task-group.ts +89 -6
  40. package/src/runtime/stale-reconciler.ts +55 -0
  41. package/src/runtime/task-output-context.ts +11 -1
  42. package/src/runtime/task-runner/child-executor.ts +130 -4
  43. package/src/runtime/task-runner/output-splitter.ts +48 -5
  44. package/src/runtime/task-runner/post-execution.ts +2 -2
  45. package/src/runtime/task-runner/pre-execution.ts +37 -24
  46. package/src/runtime/task-runner/retrieval-orchestrator.ts +28 -2
  47. package/src/schema/team-tool-schema.ts +27 -1
  48. package/src/state/contracts.ts +16 -0
  49. package/src/state/crew-init.ts +114 -0
  50. package/src/state/stores/active-run-registry.ts +13 -6
  51. package/src/state/types.ts +0 -8
  52. package/src/ui/goal-flag.ts +44 -0
  53. package/src/ui/live-run-sidebar.ts +5 -1
  54. package/src/ui/pi-ui-compat.ts +20 -3
  55. package/src/ui/run-dashboard.ts +2 -1
  56. package/src/ui/widget/widget-model.ts +6 -2
  57. package/src/utils/ip-literal.ts +145 -0
  58. package/src/workflows/workflow-config.ts +6 -0
@@ -0,0 +1,145 @@
1
+ /**
2
+ * IPv6 literal parsing / normalization helpers (RFC 3986 §3.2.2 host ABNF +
3
+ * RFC 4291 address forms). Shared by the OTLP exporter and webhook notifier
4
+ * SSRF guards so both classify EVERY literal form (bracketed, compressed,
5
+ * expanded, embedded-IPv4, zone-id) through ONE parser.
6
+ *
7
+ * G6 fix rationale: each guard previously matched only a subset of textual
8
+ * forms, so the bracketed IPv4-compatible spelling `::a.b.c.d` (e.g.
9
+ * `[::127.0.0.1]` — kernel-routed loopback) bypassed both layers, and
10
+ * `new URL("http://::1:4318")` threw before any classification ran.
11
+ */
12
+
13
+ /** Strip RFC 3986 `[...]` brackets from a URL hostname. Idempotent. */
14
+ export function unbracketHost(host: string): string {
15
+ return host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
16
+ }
17
+
18
+ /** True for a dotted-quad IPv4 string with octets in 0..255 and no leading
19
+ * zeros ("01" is rejected — getaddrinfo reads it as octal, we must not). */
20
+ function ipv4Octets(address: string): number[] | undefined {
21
+ const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(address);
22
+ if (!match) return undefined;
23
+ const octets = [Number(match[1]), Number(match[2]), Number(match[3]), Number(match[4])];
24
+ if (octets.some((octet) => octet > 255)) return undefined;
25
+ if (match.slice(1).some((part) => part.length > 1 && part.startsWith("0"))) return undefined;
26
+ return octets;
27
+ }
28
+
29
+ /** IPv4 octets → two 16-bit groups (network order). */
30
+ function octetsToGroups(octets: number[]): number[] {
31
+ return [(octets[0] << 8) | octets[1], (octets[2] << 8) | octets[3]];
32
+ }
33
+
34
+ /**
35
+ * Parse an IPv6 literal (WITHOUT brackets — call `unbracketHost` first)
36
+ * into its 8 16-bit groups. Accepts `::`-compressed forms, fully expanded
37
+ * forms, and a trailing embedded IPv4 (`::ffff:1.2.3.4`). Zone-ids
38
+ * (`fe80::1%eth0`, RFC 6874) are stripped before parsing. Returns undefined
39
+ * for anything that is not a syntactically valid IPv6 literal (hostnames,
40
+ * plain IPv4, junk) — callers treat "unparseable" as not-an-IPv6-literal.
41
+ */
42
+ export function parseIpv6Literal(host: string): number[] | undefined {
43
+ let literal = host.toLowerCase();
44
+ const zone = literal.indexOf("%");
45
+ if (zone !== -1) literal = literal.slice(0, zone);
46
+ if (!literal.includes(":")) return undefined;
47
+ const halves = literal.split("::");
48
+ if (halves.length > 2) return undefined;
49
+ const parseGroups = (part: string): number[] | undefined => {
50
+ if (part === "") return [];
51
+ const segments = part.split(":");
52
+ const groups: number[] = [];
53
+ for (let index = 0; index < segments.length; index++) {
54
+ const segment = segments[index];
55
+ if (segment.includes(".")) {
56
+ // Embedded IPv4 is only valid as the LAST segment.
57
+ if (index !== segments.length - 1) return undefined;
58
+ const octets = ipv4Octets(segment);
59
+ if (!octets) return undefined;
60
+ groups.push(...octetsToGroups(octets));
61
+ } else {
62
+ if (!/^[0-9a-f]{1,4}$/.test(segment)) return undefined;
63
+ groups.push(Number.parseInt(segment, 16));
64
+ }
65
+ }
66
+ return groups;
67
+ };
68
+ if (halves.length === 2) {
69
+ // An embedded IPv4 is only valid as the LAST 32 bits of the address —
70
+ // reject it in the pre-`::` half (e.g. `1.2.3.4::`).
71
+ if (halves[0].includes(".")) return undefined;
72
+ const head = parseGroups(halves[0]);
73
+ const tail = parseGroups(halves[1]);
74
+ if (!head || !tail) return undefined;
75
+ // `::` must expand to at least one zero group.
76
+ if (head.length + tail.length > 7) return undefined;
77
+ const zeros = Array.from({ length: 8 - head.length - tail.length }, () => 0);
78
+ return [...head, ...zeros, ...tail];
79
+ }
80
+ const groups = parseGroups(literal);
81
+ if (groups?.length !== 8) return undefined;
82
+ return groups;
83
+ }
84
+
85
+ /**
86
+ * Extract the IPv4 address embedded in an IPv4-mapped
87
+ * (`::ffff:a.b.c.d` — groups[5] == 0xffff) or IPv4-compatible
88
+ * (`::a.b.c.d` — groups[0..5] all zero) literal, as dotted quad. Both
89
+ * spellings are kernel-routed to the embedded IPv4 destination, so SSRF
90
+ * guards must apply their IPv4 predicate to the extracted quad.
91
+ */
92
+ export function embeddedIpv4FromGroups(groups: number[]): string | undefined {
93
+ if (groups.length !== 8) return undefined;
94
+ const quad = `${groups[6] >> 8}.${groups[6] & 0xff}.${groups[7] >> 8}.${groups[7] & 0xff}`;
95
+ // IPv4-mapped ::ffff:a.b.c.d — groups[0..4] zero, groups[5] == 0xffff.
96
+ if (groups[5] === 0xffff && groups.slice(0, 5).every((group) => group === 0)) return quad;
97
+ // IPv4-compatible ::a.b.c.d — groups[0..5] zero.
98
+ if (groups.slice(0, 6).every((group) => group === 0)) return quad;
99
+ return undefined;
100
+ }
101
+
102
+ /**
103
+ * Bracket a bare IPv6 literal host (`::1` → `[::1]`) per RFC 3986 §3.2.2 —
104
+ * `new URL` throws on unbracketed IPv6 hosts. Already-bracketed hosts and
105
+ * non-IPv6 hosts (fewer than 2 colons — a URL host with one colon is
106
+ * `host:port`) are returned unchanged.
107
+ */
108
+ export function bracketIpv6Host(host: string): string {
109
+ if (host.startsWith("[")) return host;
110
+ if ((host.match(/:/g) ?? []).length < 2) return host;
111
+ return `[${host}]`;
112
+ }
113
+
114
+ /**
115
+ * Normalize an http(s) URL whose authority carries a BARE IPv6 literal
116
+ * (`http://::1:4318` — unparseable by `new URL`) into bracketed form
117
+ * (`http://[::1]:4318`). The authority is isolated lexically (no URL
118
+ * parsing). Everything else — userinfo URLs, already-bracketed hosts,
119
+ * normal hostnames — is returned unchanged.
120
+ */
121
+ export function bracketIpv6InUrl(url: string): string {
122
+ const schemeMatch = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.exec(url);
123
+ if (!schemeMatch) return url;
124
+ const scheme = schemeMatch[0];
125
+ const rest = url.slice(scheme.length);
126
+ const authorityEnd = /[/?#]/.exec(rest);
127
+ const authority = authorityEnd ? rest.slice(0, authorityEnd.index) : rest;
128
+ const tail = authorityEnd ? rest.slice(authorityEnd.index) : "";
129
+ if (authority.includes("@") || authority.startsWith("[")) return url;
130
+ // Split a trailing `:port` only when what remains is still a plausible
131
+ // bare IPv6 literal (>= 2 colons): `::1:4318` → host `::1` + port
132
+ // `4318`, while `::1` stays whole (stripping `:1` would leave `:`).
133
+ let host = authority;
134
+ let port = "";
135
+ const portMatch = /:(\d{1,5})$/.exec(authority);
136
+ if (portMatch) {
137
+ const candidate = authority.slice(0, portMatch.index);
138
+ if ((candidate.match(/:/g) ?? []).length >= 2) {
139
+ host = candidate;
140
+ port = portMatch[0];
141
+ }
142
+ }
143
+ if ((host.match(/:/g) ?? []).length < 2) return url;
144
+ return `${scheme}[${host}]${port}${tail}`;
145
+ }
@@ -59,6 +59,12 @@ export interface WorkflowConfig {
59
59
  /** For runtime:"dynamic" — per-workflow token budget. When set, ctx.agent() auto-rejects with
60
60
  * ok:false once exhausted. Accumulated from each agent run's reported usage. */
61
61
  maxTokenBudget?: number;
62
+ /** SDD-3 W-C G13: for runtime:"dynamic" — cap on ctx.agent() invocations per
63
+ * run (new spawns AND cached replays). Unset → DEFAULT_MAX_AGENT_CALLS
64
+ * (dynamic-workflow-context.ts); a dynamic run is always bounded even
65
+ * without a token budget. Tripping the cap terminates the run with a
66
+ * structured DwfAgentCallCapError (dwf.failed), not the blind script timeout. */
67
+ maxAgentCalls?: number;
62
68
  /** Explicit topology classification from frontmatter `topology:` field.
63
69
  * When set, overrides the auto-classified topology in analyzeWorkflowTopology().
64
70
  * Used by preflight-validator to enforce "don't use pi-crew for sequential chains".