@parall/agent-core 1.34.0 → 1.36.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.
@@ -2,130 +2,176 @@ export const PARALL_WIKI_SKILL = `# Parall Wiki
2
2
 
3
3
  Manage organization wikis via the Parall CLI. Auth is pre-configured.
4
4
 
5
- ## Browsing (no local state needed)
5
+ ## Mental model
6
+
7
+ Wiki editing works on a **local workspace**: \`parall wiki sync\` downloads the
8
+ wiki into a directory on disk, you edit those files with your normal file
9
+ tools, then \`parall wiki changeset create\` uploads the result as a proposal.
10
+ There is no git in the workspace — your edits are detected by diffing against
11
+ the synced baseline.
12
+
13
+ Key facts the commands won't tell you:
14
+
15
+ - **Workspace location is fixed.** Sync output and \`parall wiki status\` print
16
+ the absolute workspace path (\`synced → /path/to/<slug>\` / \`Mount: ...\`).
17
+ Always address wiki files by that absolute path — your shell cwd is usually
18
+ NOT inside the workspace.
19
+ - **Text and binary are two paths.** The workspace + changeset flow is for
20
+ text (markdown, code, config). Binary assets (images, PDFs, archives) are
21
+ diff-less — they don't go in the workspace; use \`parall wiki file\` (see
22
+ **Binary files** below). Dropping a binary into the workspace just gets it
23
+ rejected on propose.
24
+ - **\`cat\`, \`search\`, \`query\`, \`outline\`, and \`section\` read your local
25
+ workspace copy when it exists** — including your own unproposed edits. Add
26
+ \`--remote\` to \`cat\` to read the server version instead.
27
+ - **\`search\`/\`query\` are keyword (lexical) matching**, not semantic search.
28
+ \`query\` additionally ranks whole documents — better for multi-word
29
+ questions; \`search\` for a single identifier.
30
+ - The \`<wiki>\` argument is the slug or ID from \`parall wiki list\`; with a
31
+ single wiki in the org it can be omitted.
32
+
33
+ ## Core workflow
6
34
 
7
35
  \`\`\`bash
8
- parall wiki list # List all wikis
9
- parall wiki tree <slug> # List files and directories
10
- parall wiki tree <slug> --path docs/ # List a subdirectory
36
+ parall wiki sync # 1. get/update files (prints workspace path)
37
+ # 2. read + edit files under the workspace path with standard file tools
38
+ parall wiki diff <wiki> # 3. review exactly what you'll propose
39
+ parall wiki changeset create <wiki> --title "..." # 4. submit
11
40
  \`\`\`
12
41
 
13
- ## Editing
42
+ Always sync before starting and always check \`diff\` before proposing — the
43
+ changeset uploads the full content of every changed file.
14
44
 
15
- Wiki editing works on a **local workspace** — a directory on disk where wiki
16
- files are synced. You sync from the server, edit files locally, then propose
17
- a changeset.
45
+ Unprotected paths auto-merge immediately (\`auto_merged: true\`); protected
46
+ paths stay open for human review. Follow the returned \`next_action\` either way.
18
47
 
19
- ### Step 1: Sync
48
+ ## Stale base (server moved since your sync)
20
49
 
21
- \`\`\`bash
22
- parall wiki sync <slug>
23
- \`\`\`
24
-
25
- This downloads wiki files to a local directory. The output includes the
26
- **absolute mount path** for each wiki (e.g. \`synced → /path/to/workspace/kb\`).
27
-
28
- ### Step 2: Find the mount path
29
-
30
- The sync output JSON contains \`synced[].path\` — the absolute path where files
31
- live. You can also check it anytime with:
50
+ If files changed on the server after your last sync, \`changeset create\` is
51
+ rejected both by a CLI precheck and by the server (409 \`STALE_BASE\`) — so
52
+ you can't silently overwrite someone's concurrent edit. Recovery:
32
53
 
33
54
  \`\`\`bash
34
- parall wiki status <slug>
55
+ parall wiki sync # pull latest; your local edits are preserved
56
+ # if a file conflicts, resolve it (see next section)
57
+ parall wiki changeset create <wiki> --title "..."
35
58
  \`\`\`
36
59
 
37
- The output includes \`Mount: /absolute/path/to/<slug>\`.
38
-
39
- Use this path with \`read\`, \`write\`, and \`edit\` tools. For example, if the mount
40
- is \`/data/.openclaw/workspace/kb\`, then \`README.md\` is at
41
- \`/data/.openclaw/workspace/kb/README.md\`.
60
+ ## Sync conflicts
42
61
 
43
- ### Step 3: Edit files
62
+ \`sync\` three-way merges. When both you and the server changed the same file,
63
+ your file is left intact and the upstream copy lands under
64
+ \`<workspace>/.parall-wiki/conflicts/\`:
44
65
 
45
- Use standard file tools (\`read\`, \`write\`, \`edit\`) on files under the mount path.
66
+ | Marker | Meaning |
67
+ |--------|---------|
68
+ | \`conflicts/<path>.remote\` | Server has different content for \`<path>\` |
69
+ | \`conflicts/<path>.remote-deleted\` | Server deleted \`<path>\`; you still have edits |
46
70
 
47
- ### Step 4: Review changes
71
+ All paths below are relative to the workspace root. Pick one:
48
72
 
49
73
  \`\`\`bash
50
- parall wiki diff <slug> # Shows unified diff of all local changes
51
- parall wiki status <slug> # Shows which files changed with +/- line counts
74
+ # Accept upstream (drop your edit):
75
+ cp <workspace>/.parall-wiki/conflicts/<path>.remote <workspace>/<path>
76
+
77
+ # Keep yours / hand-merge: edit <workspace>/<path> to final content, then
78
+ parall wiki changeset create <wiki> --title "Reconcile <path>"
79
+
80
+ # Accept server delete (.remote-deleted only):
81
+ rm <workspace>/<path>
52
82
  \`\`\`
53
83
 
54
- Always review before proposing.
84
+ Then re-run \`parall wiki sync\` and delete the used marker file. Conflicts
85
+ exit 0 (they need your decision); \`failed[]\` entries (download error,
86
+ shape-conflict) exit 1 and retry on the next sync.
55
87
 
56
- ### Step 5: Propose changeset
88
+ ## Changesets
57
89
 
58
90
  \`\`\`bash
59
- parall wiki changeset create <slug> --title "Description of changes"
91
+ parall wiki changeset list <wiki>
92
+ parall wiki changeset show <changesetId> <wiki> # status + feedback
93
+ parall wiki changeset diff <changesetId> <wiki>
94
+ parall wiki changeset create <wiki> --update <id> # re-propose after rejection (title inherited)
60
95
  \`\`\`
61
96
 
62
- This uploads your local changes. Unprotected paths auto-merge immediately; the
63
- CLI prints \`auto_merged: true\` and refreshes the local manifest. Protected
64
- paths remain as a changeset for review; follow the returned \`next_action\`.
97
+ Rejected: read the feedback (\`show\` / \`status\`), fix the files, re-propose
98
+ with \`--update <id>\`. Conflict status: \`sync\`, resolve, then \`--update <id>\`.
65
99
 
66
- ## Changeset Management
100
+ Re-propose REPLACES the changeset's previous contents with your current
101
+ workspace diff — to withdraw a file from the proposal, revert it locally
102
+ (restore the synced content) and re-propose; it drops out of the changeset.
67
103
 
68
- \`\`\`bash
69
- parall wiki changeset list <slug> # List all changesets
70
- parall wiki changeset show <changesetId> <slug> # Show detail + feedback
71
- parall wiki changeset diff <changesetId> <slug> # Show changeset diff
72
- \`\`\`
104
+ ## Binary files
73
105
 
74
- If a changeset is rejected, fix the files locally and re-propose:
106
+ Images, PDFs, archives anything that can't be diffed bypass the workspace
107
+ and changeset-text flow entirely. They never belong in the synced workspace
108
+ (propose rejects them); use \`parall wiki file\` instead. \`cat\` is text-only —
109
+ to read a binary's real bytes use \`file get\` (a plain \`sync\` only leaves a
110
+ few-line Git-LFS pointer on disk, since the runtime has no git-lfs).
75
111
 
76
112
  \`\`\`bash
77
- parall wiki changeset create <slug> --update <changesetId>
78
- \`\`\`
113
+ # Maintainer: direct-commit a binary to the default branch (no review)
114
+ parall wiki file upload ./diagram.png docs/assets/diagram.png
79
115
 
80
- The title is inherited from the original changeset no need to repeat it.
116
+ # Read a binary's real bytes (LFS pointers resolved server-side) to a file.
117
+ # Always use --output for binaries — without it the bytes stream to stdout and
118
+ # would flood your context.
119
+ parall wiki file get docs/assets/diagram.png --output ./diagram.png
120
+ parall wiki file get docs/assets/diagram.png --ref <commit-or-branch> --output ./diagram.png # a specific revision
81
121
 
82
- ## Handling sync conflicts
122
+ # Remove a binary from the default branch (git history still has it)
123
+ parall wiki file delete docs/assets/diagram.png
124
+ \`\`\`
83
125
 
84
- \`wiki sync\` runs a three-way merge. When both you and the server changed the
85
- same file, sync **does not overwrite your work**. It leaves your file intact
86
- and drops the upstream version under \`.parall-wiki/conflicts/\`:
126
+ \`upload\` needs **maintain**; it routes by size automatically (≤1 MiB inline,
127
+ larger LFS). A text file sent to \`upload\` is rejected that's the changeset
128
+ flow's job.
87
129
 
88
- | Marker | Meaning |
89
- |--------|---------|
90
- | \`.parall-wiki/conflicts/<path>.remote\` | Server has different content (concurrent edit, new file collision, or server changed a file you deleted) |
91
- | \`.parall-wiki/conflicts/<path>.remote-deleted\` | Server deleted the file; you still have edits |
130
+ ### Reader: propose markdown that embeds an image
92
131
 
93
- stderr prints one line per conflict. Recovery:
132
+ Without maintain you can still propose a doc with images — upload the binary
133
+ into your **changeset's** branch (read + author), not the default branch:
94
134
 
95
- **Accept upstream** (drop your edit):
96
135
  \`\`\`bash
97
- cp .parall-wiki/conflicts/<path>.remote <path>
98
136
  parall wiki sync
137
+ # edit a .md in the workspace to add ![alt](assets/foo.png)
138
+ parall wiki changeset create <wiki> --title "Add foo diagram" # creates the changeset (note its id)
139
+ parall wiki file upload ./foo.png assets/foo.png <wiki> --changeset <changesetId>
140
+ # leave it for a maintainer to merge — both the markdown and the image squash in together
99
141
  \`\`\`
100
142
 
101
- **Keep yours** (or hand-merge, then propose):
102
- \`\`\`bash
103
- # edit <path> to final content
104
- parall wiki diff <slug>
105
- parall wiki changeset create <slug> --title "Reconcile <path>"
106
- parall wiki sync # fast-forwards after server merges
107
- \`\`\`
143
+ Do the markdown \`changeset create\` first so the changeset exists, then attach
144
+ the image to it. Don't re-propose (\`--update\`) after attaching a binary —
145
+ re-propose replays only the text workspace and the server rejects dropping the
146
+ attached binary (422 \`REPLACE_HAS_BINARY\`).
147
+
148
+ ## Discovery & history
108
149
 
109
- **Accept server delete** (\`.remote-deleted\` only):
110
150
  \`\`\`bash
111
- rm <path>
112
- parall wiki sync
151
+ parall wiki query "how is auth configured" <wiki> # multi-word lookup (query FIRST, wiki second)
152
+ parall wiki search "JWT" <wiki> # single keyword (query FIRST, wiki second)
153
+ parall wiki outline <wiki> --path docs/ # heading structure
154
+ parall wiki cat docs/auth.md <wiki> # print a file (--remote for server version)
155
+ parall wiki tree <wiki> # list files
156
+ parall wiki log <wiki> # recent operations
157
+ parall wiki log <wiki> docs/auth.md # per-file commit history
113
158
  \`\`\`
114
159
 
115
- Sync failures (\`download\`, \`shape-conflict\`) set exit code 1 and retry on
116
- next sync. Conflicts exit 0 — they need your decision, not a retry.
117
-
118
- ## Discarding local changes
160
+ ## Permissions
119
161
 
120
- \`\`\`bash
121
- parall wiki reset <slug> # Restore all files to last synced state
122
- \`\`\`
162
+ Reads and writes are ACL-checked server-side per path. \`parall wiki access
163
+ <path>\` shows your level for a path. On a \`PERMISSION_DENIED\`, errors include
164
+ a \`Request approval:\` hint — use \`parall wiki request-access <path> --reason
165
+ "..."\` to file an approval card for a maintainer, then re-sync after approval.
123
166
 
124
- ## History
167
+ ## Recovery
125
168
 
126
169
  \`\`\`bash
127
- parall wiki log <slug> # Recent wiki operations
170
+ parall wiki reset <wiki> # discard ALL local edits, restore last-synced state
171
+ parall wiki status <wiki> # local changes + your changesets, anytime
128
172
  \`\`\`
129
173
 
130
- CLI success output is JSON; errors may add a plain-text \`Request approval:\` line on a \`PERMISSION_DENIED\` — read both. Run \`parall wiki --help\` for full options.
174
+ CLI success output is JSON on stdout (human summary on stderr); errors state
175
+ the reason and the next step — follow them. \`parall wiki --help\` for the
176
+ full command list.
131
177
  `;
package/dist/types.d.ts CHANGED
@@ -26,7 +26,7 @@ export type DispatchState = {
26
26
  };
27
27
  /** Normalized inbound event from Parall. */
28
28
  export type ParallEvent = {
29
- type: 'message' | 'task' | 'task_comment' | 'wiki_comment' | 'schedule' | 'approval';
29
+ type: 'message' | 'task' | 'task_comment' | 'wiki_comment' | 'schedule' | 'external_trigger' | 'approval';
30
30
  targetId: string;
31
31
  targetName?: string;
32
32
  targetType?: string;
@@ -54,10 +54,16 @@ export type ParallEvent = {
54
54
  scheduledFireAt?: string;
55
55
  /** Fire-time attached_to_uri snapshot, used for schedule events. */
56
56
  attachedUri?: string;
57
+ /** External trigger metadata, used for external_trigger events. */
58
+ externalConnectionId?: string;
59
+ externalConnectionSourceType?: string;
60
+ externalConnectionDisplayName?: string;
61
+ externalIngressEventId?: string;
62
+ externalIngressEventType?: string;
57
63
  /** Original event timestamp (e.g., message.created_at). When present,
58
64
  * input steps use this instead of server insertion time for ordering. */
59
65
  sentAt?: string;
60
- ackSourceType?: 'message' | 'task_activity' | 'comment' | 'schedule_run';
66
+ ackSourceType?: 'message' | 'task_activity' | 'comment' | 'schedule_run' | 'external_trigger_run';
61
67
  ackSourceId?: string;
62
68
  /** Unread message count in the target chat since agent's last interaction. */
63
69
  unreadCount?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,MAAM,MAAM,UAAU,GAAG;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACjE,mFAAmF;IACnF,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,sFAAsF;IACtF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qFAAqF;IACrF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,2DAA2D;AAC3D,MAAM,MAAM,aAAa,GAAG;IAC1B,eAAe,EAAE,OAAO,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,kBAAkB,EAAE,UAAU,EAAE,CAAC;IACjC,UAAU,EAAE,WAAW,EAAE,CAAC;IAC1B,oFAAoF;IACpF,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC,CAAC;AAEF,4CAA4C;AAC5C,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,cAAc,GAAG,cAAc,GAAG,UAAU,GAAG,UAAU,CAAC;IACrF,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,KAAK,CAAC;QAClB,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC,CAAC;IACH,8DAA8D;IAC9D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;8EAC0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,SAAS,GAAG,eAAe,GAAG,SAAS,GAAG,cAAc,CAAC;IACzE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,MAAM,MAAM,UAAU,GAAG;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACjE,mFAAmF;IACnF,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,sFAAsF;IACtF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qFAAqF;IACrF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,2DAA2D;AAC3D,MAAM,MAAM,aAAa,GAAG;IAC1B,eAAe,EAAE,OAAO,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,kBAAkB,EAAE,UAAU,EAAE,CAAC;IACjC,UAAU,EAAE,WAAW,EAAE,CAAC;IAC1B,oFAAoF;IACpF,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC,CAAC;AAEF,4CAA4C;AAC5C,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EACA,SAAS,GACT,MAAM,GACN,cAAc,GACd,cAAc,GACd,UAAU,GACV,kBAAkB,GAClB,UAAU,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,KAAK,CAAC;QAClB,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC,CAAC;IACH,8DAA8D;IAC9D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC,6BAA6B,CAAC,EAAE,MAAM,CAAC;IACvC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC;8EAC0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,SAAS,GAAG,eAAe,GAAG,SAAS,GAAG,cAAc,GAAG,sBAAsB,CAAC;IAClG,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/agent-core",
3
- "version": "1.34.0",
3
+ "version": "1.36.0",
4
4
  "description": "Shared agent runtime orchestration helpers for Parall",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,7 +35,7 @@
35
35
  "@opentelemetry/sdk-logs": "^0.57.0",
36
36
  "@opentelemetry/sdk-metrics": "^1.30.0",
37
37
  "@opentelemetry/sdk-trace-node": "^1.30.0",
38
- "@parall/sdk": "1.34.0"
38
+ "@parall/sdk": "1.36.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "^22.0.0",
@@ -89,6 +89,26 @@ export function buildEventBody(event: ParallEvent): string {
89
89
  if (event.scheduledFireAt) lines.push(`[Scheduled at: ${sanitizeMeta(event.scheduledFireAt)}]`);
90
90
  if (event.attachedUri) lines.push(`[Attached: ${sanitizeMeta(event.attachedUri)}]`);
91
91
  lines.push('', event.body);
92
+ } else if (event.type === 'external_trigger') {
93
+ lines.push(`[Event: external.trigger]`);
94
+ lines.push(`[Trigger: prll://${event.targetId}]`);
95
+ lines.push(`[Run: prll://${event.messageId}]`);
96
+ if (event.externalConnectionId) {
97
+ const label = event.externalConnectionDisplayName
98
+ ? `${sanitizeMeta(event.externalConnectionDisplayName)} (prll://${event.externalConnectionId})`
99
+ : `prll://${event.externalConnectionId}`;
100
+ lines.push(`[Connection: ${label}]`);
101
+ }
102
+ if (event.externalIngressEventId)
103
+ lines.push(`[Ingress: prll://${event.externalIngressEventId}]`);
104
+ if (event.attachedUri) lines.push(`[Attached: ${sanitizeMeta(event.attachedUri)}]`);
105
+ if (event.externalConnectionSourceType) {
106
+ lines.push(`[Source: ${sanitizeMeta(event.externalConnectionSourceType)}]`);
107
+ }
108
+ if (event.externalIngressEventType) {
109
+ lines.push(`[External event: ${sanitizeMeta(event.externalIngressEventType)}]`);
110
+ }
111
+ lines.push('', event.body);
92
112
  } else {
93
113
  lines.push(`[Event: task.assigned]`);
94
114
  const taskLabel = event.targetName
@@ -125,6 +145,10 @@ function buildSendMessageHint(event: ParallEvent): string {
125
145
  return `\n<system-reminder>To communicate, use the CLI: \`parall messages send\` / \`parall dm\`. Your plain text output is not delivered.</system-reminder>`;
126
146
  }
127
147
 
148
+ if (event.type === 'external_trigger' || event.targetId.startsWith('xtr_')) {
149
+ return `\n<system-reminder>This external trigger is incoming-only. Your plain text output is not sent back to the external provider. To communicate in Parall, use \`parall messages send\` / \`parall dm\`; provider-specific outbound actions require a separate capability.</system-reminder>`;
150
+ }
151
+
128
152
  return '';
129
153
  }
130
154
 
@@ -12,6 +12,7 @@ import type {
12
12
  Comment,
13
13
  DispatchDeliveryReason,
14
14
  DispatchNewData,
15
+ ExternalTriggerRun,
15
16
  HelloData,
16
17
  MessageNewData,
17
18
  ScheduleRun,
@@ -194,6 +195,9 @@ function resolveStepTarget(event: ParallEvent): { target_type: string; target_id
194
195
  if (event.type === 'schedule' || event.targetId.startsWith('sch_')) {
195
196
  return { target_type: 'schedule', target_id: event.targetId };
196
197
  }
198
+ if (event.type === 'external_trigger' || event.targetId.startsWith('xtr_')) {
199
+ return { target_type: 'external_trigger', target_id: event.targetId };
200
+ }
197
201
  if (event.type === 'wiki_comment') {
198
202
  // target_id is the full wiki target_uri (scheme-stripped routing key). The
199
203
  // server stores target_type freely and only publishes step WS events /
@@ -457,6 +461,18 @@ export class ParallAgentGateway {
457
461
  `schedule fire dispatch failed for ${data.source_id}: ${String(err)}`,
458
462
  );
459
463
  }
464
+ } else if (data.event_type === 'external_trigger') {
465
+ if (!data.source_id) return;
466
+ try {
467
+ const dispatched = await this.fetchAndHandleExternalTriggerRun(data.source_id);
468
+ if (dispatched) {
469
+ this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {});
470
+ }
471
+ } catch (err) {
472
+ this.opts.log?.error(
473
+ `external trigger dispatch failed for ${data.source_id}: ${String(err)}`,
474
+ );
475
+ }
460
476
  } else if (data.event_type === 'approval_decided') {
461
477
  if (!data.source_id) return;
462
478
  try {
@@ -570,9 +586,11 @@ export class ParallAgentGateway {
570
586
  ? 'wiki_comment'
571
587
  : event.type === 'schedule'
572
588
  ? 'schedule_fire'
573
- : event.type === 'approval'
574
- ? 'approval_decided'
575
- : 'mention',
589
+ : event.type === 'external_trigger'
590
+ ? 'external_trigger'
591
+ : event.type === 'approval'
592
+ ? 'approval_decided'
593
+ : 'mention',
576
594
  trigger_ref:
577
595
  event.type === 'task'
578
596
  ? { task_id: event.targetId }
@@ -582,9 +600,16 @@ export class ParallAgentGateway {
582
600
  ? { comment_id: event.messageId, target_uri: event.replyTargetUri }
583
601
  : event.type === 'schedule'
584
602
  ? { schedule_id: event.targetId, run_id: event.messageId }
585
- : event.type === 'approval'
586
- ? { approval_id: event.messageId }
587
- : { message_id: event.messageId },
603
+ : event.type === 'external_trigger'
604
+ ? {
605
+ trigger_id: event.targetId,
606
+ run_id: event.messageId,
607
+ connection_id: event.externalConnectionId,
608
+ ingress_event_id: event.externalIngressEventId,
609
+ }
610
+ : event.type === 'approval'
611
+ ? { approval_id: event.messageId }
612
+ : { message_id: event.messageId },
588
613
  sender_id: event.senderId,
589
614
  sender_name: event.senderName,
590
615
  summary: event.body.substring(0, 200),
@@ -1342,6 +1367,13 @@ export class ParallAgentGateway {
1342
1367
  this.dispatchState.mainCurrentTargetId = undefined;
1343
1368
  this.dispatchState.mainPreDispatchBranchPoint = undefined;
1344
1369
  if (!this.shuttingDown && this.dispatchState.mainBuffer.length > 0) {
1370
+ // Opportunistic re-drain — best-effort, not a recovery deadline, so it
1371
+ // must not be the sole handle keeping the event loop alive. In a live
1372
+ // bridge the WS connection holds the loop open and this still fires;
1373
+ // when nothing else is pending there is no runtime to drain to anyway,
1374
+ // and an un-ref'd timer lets the process (or a test run) exit instead
1375
+ // of idling out the 5s. The abort deadlines deliberately stay ref'd —
1376
+ // firing as the sole handle IS their recovery job.
1345
1377
  setTimeout(() => {
1346
1378
  if (
1347
1379
  !this.draining &&
@@ -1351,7 +1383,7 @@ export class ParallAgentGateway {
1351
1383
  this.dispatchState.mainDispatching = true;
1352
1384
  void this.drainMainBuffer();
1353
1385
  }
1354
- }, 5000);
1386
+ }, 5000).unref();
1355
1387
  }
1356
1388
  }
1357
1389
  }
@@ -1923,6 +1955,70 @@ export class ParallAgentGateway {
1923
1955
  return dispatched;
1924
1956
  }
1925
1957
 
1958
+ private async fetchAndHandleExternalTriggerRun(runId: string): Promise<boolean> {
1959
+ let run: ExternalTriggerRun | null = null;
1960
+ try {
1961
+ run = await this.opts.client.getExternalTriggerRun(this.opts.config.org_id, runId);
1962
+ } catch (err: unknown) {
1963
+ const status = (err as { status?: number })?.status;
1964
+ if (status === 404) {
1965
+ this.opts.log?.warn(
1966
+ `external trigger run ${runId} not accessible (404), acking stale dispatch`,
1967
+ );
1968
+ return true;
1969
+ }
1970
+ this.opts.log?.warn(
1971
+ `external trigger run fetch failed for ${runId}, leaving pending: ${String(err)}`,
1972
+ );
1973
+ return false;
1974
+ }
1975
+ if (!run) return true;
1976
+ return this.handleExternalTriggerRun(run);
1977
+ }
1978
+
1979
+ private async handleExternalTriggerRun(run: ExternalTriggerRun): Promise<boolean> {
1980
+ if (this.shuttingDown) return false;
1981
+ const dedupeKey = `external_trigger_run:${run.id}`;
1982
+ if (this.dispatchedTasks.has(dedupeKey)) return false;
1983
+ this.dispatchedTasks.add(dedupeKey);
1984
+ this.opts.log?.info(`external trigger fired: ${run.id} (trigger ${run.trigger_id})`);
1985
+ const attachedUri =
1986
+ typeof run.trigger_snapshot?.attached_to_uri === 'string'
1987
+ ? run.trigger_snapshot.attached_to_uri
1988
+ : undefined;
1989
+
1990
+ const event: ParallEvent = {
1991
+ type: 'external_trigger',
1992
+ targetId: run.trigger_id,
1993
+ targetName: run.trigger_name || undefined,
1994
+ targetType: 'external_trigger',
1995
+ senderId: 'system',
1996
+ senderName: 'external',
1997
+ messageId: run.id,
1998
+ body: run.agent_input_body ?? '',
1999
+ externalConnectionId: run.connection_id,
2000
+ externalConnectionSourceType: run.connection_source_type || undefined,
2001
+ externalConnectionDisplayName: run.connection_display_name || undefined,
2002
+ externalIngressEventId: run.ingress_event_id,
2003
+ externalIngressEventType: run.ingress_event_type || undefined,
2004
+ attachedUri,
2005
+ ackSourceType: 'external_trigger_run',
2006
+ ackSourceId: run.id,
2007
+ };
2008
+
2009
+ let dispatched: boolean;
2010
+ try {
2011
+ dispatched = await this.handleInboundEvent(event);
2012
+ } catch (err) {
2013
+ this.dispatchedTasks.delete(dedupeKey);
2014
+ throw err;
2015
+ }
2016
+ if (!dispatched) {
2017
+ this.dispatchedTasks.delete(dedupeKey);
2018
+ }
2019
+ return dispatched;
2020
+ }
2021
+
1926
2022
  private async fetchAndHandleApprovalDecided(
1927
2023
  approvalId: string,
1928
2024
  actorId: string | null,
@@ -2074,6 +2170,8 @@ export class ParallAgentGateway {
2074
2170
  );
2075
2171
  } else if (item.event_type === 'schedule.fire' && item.source_id) {
2076
2172
  dispatched = await this.fetchAndHandleScheduleFire(item.source_id, item.actor_id);
2173
+ } else if (item.event_type === 'external_trigger' && item.source_id) {
2174
+ dispatched = await this.fetchAndHandleExternalTriggerRun(item.source_id);
2077
2175
  } else if (item.event_type === 'approval_decided' && item.source_id) {
2078
2176
  dispatched = await this.fetchAndHandleApprovalDecided(
2079
2177
  item.source_id,
@@ -5,12 +5,14 @@ export { PARALL_PLATFORM_SKILL } from './parall-platform.js';
5
5
  export { PARALL_TASKS_SKILL } from './parall-tasks.js';
6
6
  export { PARALL_WIKI_SKILL } from './parall-wiki.js';
7
7
  export { PARALL_SCHEDULES_SKILL } from './parall-schedules.js';
8
+ export { PARALL_EXTERNAL_TRIGGERS_SKILL } from './parall-external-triggers.js';
8
9
  export { PARALL_CLIPS_SKILL } from './parall-clips.js';
9
10
 
10
11
  import { PARALL_PLATFORM_SKILL } from './parall-platform.js';
11
12
  import { PARALL_TASKS_SKILL } from './parall-tasks.js';
12
13
  import { PARALL_WIKI_SKILL } from './parall-wiki.js';
13
14
  import { PARALL_SCHEDULES_SKILL } from './parall-schedules.js';
15
+ import { PARALL_EXTERNAL_TRIGGERS_SKILL } from './parall-external-triggers.js';
14
16
  import { PARALL_CLIPS_SKILL } from './parall-clips.js';
15
17
 
16
18
  export type SkillMeta = { name: string; description: string; content: string };
@@ -40,6 +42,12 @@ export const SKILLS: SkillMeta[] = [
40
42
  'Parall schedule operations: create / pause / resume / cancel recurring or one-shot time triggers; respond to schedule fire events. Use when: user asks to set up a recurring reminder, schedule a delayed prompt, run cron-like work, or when the agent receives an `[Event: schedule.fired]` dispatch.',
41
43
  content: PARALL_SCHEDULES_SKILL,
42
44
  },
45
+ {
46
+ name: 'parall-external-triggers',
47
+ description:
48
+ 'Parall External Trigger operations: create incoming connections, configure CEL/Liquid triggers, inspect incoming events and runs, and respond to `[Event: external.trigger]` dispatches. Use when: user asks to connect an external system to an agent, set up incoming callbacks/notifications, or when the agent receives an external trigger event.',
49
+ content: PARALL_EXTERNAL_TRIGGERS_SKILL,
50
+ },
43
51
  {
44
52
  name: 'parall-clips',
45
53
  description:
@@ -0,0 +1,88 @@
1
+ export const PARALL_EXTERNAL_TRIGGERS_SKILL = `# Parall External Triggers
2
+
3
+ An **External Trigger** is an incoming platform trigger. External systems send events to an External Trigger Connection, Parall matches active triggers with CEL, renders a Liquid template into an agent input body, and dispatches that input to the configured target agents.
4
+
5
+ Use External Triggers for incoming events such as GitHub callbacks, Slack/Feishu notifications, generic webhooks, or emails once the platform has a connection for them. The runtime behavior is incoming-only: receiving a trigger does not imply that you can call the external system back unless another explicit Parall capability is available.
6
+
7
+ ## Prerequisite
8
+
9
+ External Trigger CLI commands are gated by the org-level \`external-triggers\` feature flag. If a command reports that the feature is unavailable, ask a human org admin to enable the flag before trying again. Public ingress delivery may still be live even when the management CLI is hidden behind the rollout flag.
10
+
11
+ ## Creating a trigger
12
+
13
+ \`\`\`bash
14
+ # 1. Create a connection. The ingress token is shown once; prefer writing it
15
+ # to a local file so it does not land in shell history or logs.
16
+ parall external-triggers create-connection --name "GitHub CI" --token-file ./github-ci-webhook-token.txt
17
+
18
+ # 2. Create a trigger that targets one or more agents.
19
+ parall external-triggers create \\
20
+ --connection prll://xcn_xxx \\
21
+ --name "Failed checks" \\
22
+ --target-ids prll://usr_agent_xxx \\
23
+ --filter "body.json.check_run.conclusion == 'failure'" \\
24
+ --template-file ./github-check-failed.md \\
25
+ --attached-to-uri prll://tsk_xxx
26
+ \`\`\`
27
+
28
+ \`--filter\` is CEL. Omit it to match all incoming events (\`true\`). Keep filters small and deterministic; do not model provider-specific branching in agent code when the CEL expression can select the relevant events first.
29
+
30
+ \`--template\` / \`--template-file\` is Liquid with the safe Parall profile. It can read event data, body data, safe request metadata, trigger fields, run fields, and connection fields. It cannot call HTTP, read databases, evaluate arbitrary code, access platform secrets, or read arbitrary request headers/query parameters.
31
+
32
+ Example template:
33
+
34
+ \`\`\`liquid
35
+ GitHub check failed.
36
+
37
+ Event: {{ request.headers.x_github_event | default: "unknown" }}
38
+ Repo: {{ body.json.repository.full_name }}
39
+ PR: {{ body.json.pull_request.number }} {{ body.json.pull_request.title }}
40
+ Check: {{ body.json.check_run.name }}
41
+ Conclusion: {{ body.json.check_run.conclusion }}
42
+
43
+ Run: {{ body.json.check_run.html_url }}
44
+ \`\`\`
45
+
46
+ Do not render access tokens, signing secrets, cookies, or private credentials into agent input. Request snapshots and bodies may contain third-party data; treat them as user-provided input.
47
+
48
+ ## Inspecting and lifecycle
49
+
50
+ \`\`\`bash
51
+ parall external-triggers connections
52
+ parall external-triggers connection prll://xcn_xxx
53
+ parall external-triggers schema prll://xcn_xxx
54
+
55
+ parall external-triggers list
56
+ parall external-triggers list --connection prll://xcn_xxx
57
+ parall external-triggers get prll://xtr_xxx
58
+ parall external-triggers update prll://xtr_xxx --filter "event.type == 'check_run'"
59
+ parall external-triggers pause prll://xtr_xxx
60
+ parall external-triggers resume prll://xtr_xxx
61
+ parall external-triggers delete prll://xtr_xxx
62
+
63
+ parall external-triggers runs prll://xtr_xxx
64
+ parall external-triggers run prll://xrn_xxx
65
+ parall external-triggers events --connection prll://xcn_xxx
66
+ parall external-triggers event prll://xin_xxx
67
+ \`\`\`
68
+
69
+ ## Responding to external trigger dispatches
70
+
71
+ When you receive \`[Event: external.trigger]\`, Parall has already matched a trigger and rendered its template. The prompt includes headers such as:
72
+
73
+ - \`[Trigger: prll://xtr_xxx]\`
74
+ - \`[Run: prll://xrn_xxx]\`
75
+ - \`[Connection: ... (prll://xcn_xxx)]\`
76
+ - \`[Ingress: prll://xin_xxx]\`
77
+ - Optional \`[Attached: prll://...]\`
78
+
79
+ The rendered agent input body follows those headers. You usually do not need to fetch the run before acting. Fetch the run only for audit/debugging:
80
+
81
+ \`\`\`bash
82
+ parall external-triggers run prll://xrn_xxx
83
+ \`\`\`
84
+
85
+ Act on the rendered input the same way you would act on a user message: send a message, create or update tasks, edit wiki pages, or use available clips. If no visible response is needed, use \`parall no-reply --reason "handled external trigger"\` before sending any message.
86
+
87
+ CLI command results are JSON on stdout; mutation commands may emit auxiliary hints on stderr, for example \`Created: prll://xtr_xxx\`.
88
+ `;