@rubytech/create-maxy 1.0.799 → 1.0.801

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 (23) hide show
  1. package/dist/index.js +7 -1
  2. package/package.json +1 -1
  3. package/payload/platform/plugins/admin/hooks/__tests__/archive-ingest-surface-gate.test.sh +191 -0
  4. package/payload/platform/plugins/admin/hooks/archive-ingest-surface-gate.sh +207 -0
  5. package/payload/platform/plugins/cloudflare/references/manual-setup.md +12 -0
  6. package/payload/platform/plugins/cloudflare/scripts/_cdp-authorize-matcher.mjs +74 -0
  7. package/payload/platform/plugins/cloudflare/scripts/_cdp-authorize.mjs +60 -50
  8. package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +118 -22
  9. package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +4 -0
  10. package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
  11. package/payload/platform/plugins/whatsapp-import/PLUGIN.md +2 -2
  12. package/payload/platform/plugins/whatsapp-import/bin/ingest.mjs +732 -0
  13. package/payload/platform/plugins/whatsapp-import/bin/whatsapp-ingest.sh +102 -0
  14. package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import/SKILL.md +49 -97
  15. package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import/references/export-parse.md +1 -1
  16. package/payload/platform/scripts/seed-neo4j.sh +24 -15
  17. package/payload/platform/templates/specialists/agents/database-operator.md +13 -3
  18. package/payload/server/public/assets/{admin-C0lKk6WM.js → admin-Sa301b8q.js} +6 -6
  19. package/payload/server/public/index.html +1 -1
  20. package/payload/platform/plugins/admin/hooks/__tests__/archive-ingest-gate.test.sh +0 -166
  21. package/payload/platform/plugins/admin/hooks/archive-ingest-gate.sh +0 -147
  22. package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import/references/conversation-and-messages.md +0 -99
  23. package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import/references/insight-extraction.md +0 -121
@@ -1,33 +1,51 @@
1
1
  #!/usr/bin/env node
2
- // Drive the Cloudflare argotunnel Authorize click via CDP WebSocket (Task 588).
2
+ // Drive the Cloudflare argotunnel consent page to a deterministic terminal
3
+ // state via CDP WebSocket. Originally Task 588 (Authorize click from code,
4
+ // collapsing a 180 s human-latency window). Task 855 widens the contract to
5
+ // three states because the dashboard does not always render a button — once
6
+ // the bound account has authorized this device's zones, navigating the same
7
+ // (or a fresh) consent URL renders a Success modal verbatim, and the
8
+ // pre-855 helper conflated "no button visible yet" with "no button will
9
+ // ever appear" and exited error.
3
10
  //
4
- // Why this exists: setup-tunnel.sh navigates the VNC browser to the argotunnel
5
- // consent URL via `PUT /json/new?<url>`. That opens the page but does not
6
- // press the Authorize button. Pre-Task 588 the script then polled for
7
- // cert.pem for up to 180 seconds, waiting for a human click in VNC. This
8
- // helper does the click from code, collapsing the wait to ~1-3 seconds.
9
- //
10
- // Protocol: Chrome DevTools Protocol over WebSocket (https://chromedevtools.github.io/devtools-protocol/).
11
+ // Protocol: Chrome DevTools Protocol over WebSocket
12
+ // (https://chromedevtools.github.io/devtools-protocol/):
11
13
  // 1. GET http://127.0.0.1:9222/json/list → find the target with matching id;
12
14
  // extract webSocketDebuggerUrl.
13
15
  // 2. Open WS, enable Page + Runtime domains.
14
16
  // 3. Wait for Page.loadEventFired (or observe document.readyState==='complete').
15
- // 4. Poll Runtime.evaluate every 200 ms for up to ~3 s, looking for a
16
- // button or input[type=submit] whose trimmed text/value matches
17
- // /^(authorize|connect)$/i. When found, click via JS (click() on the
18
- // matched node). Exit 0 on success, 1 with structured stderr otherwise.
17
+ // 4. Poll Runtime.evaluate every 200 ms for up to ~3 s, evaluating the
18
+ // tri-state matcher from _cdp-authorize-matcher.mjs. The matcher
19
+ // returns one of:
20
+ // - { kind:'button', descriptor } Authorize/Connect button found,
21
+ // click() fired in-page; emit
22
+ // reason=clicked, exit 0.
23
+ // - { kind:'success' } — Success modal text detected
24
+ // ("Cloudflared has installed a
25
+ // certificate"); cert is bound to
26
+ // the account. Emit reason=
27
+ // cert-already-installed, exit 0.
28
+ // Caller's existing cert-poll picks
29
+ // up the cert (callback is
30
+ // idempotent on this navigation).
31
+ // - null — Neither anchor matched yet; loop.
32
+ // After BUTTON_POLL_TIMEOUT_MS with no terminal match, emit
33
+ // reason=authorize-button-not-found, exit 1.
19
34
  //
20
35
  // Exit codes:
21
- // 0 - clicked successfully
22
- // 1 - authorize-button-not-found (the happy-path loud failure)
36
+ // 0 - terminal success (reason=clicked OR reason=cert-already-installed)
37
+ // 1 - authorize-button-not-found (loud failure button AND modal absent)
23
38
  // 2 - cdp-ws-unreachable or node-websocket-unavailable
24
39
  // 3 - target-not-found (the target_id isn't in /json/list)
25
40
  // 4 - click-evaluate-threw (Runtime.evaluate returned wasThrown=true)
26
41
  // 5 - protocol-error (malformed CDP response)
27
42
  //
28
- // Each structured failure prints ONE line to stdout in phase_line-compatible
29
- // shape so setup-tunnel.sh's tee_subprocess picks it up into the stream log:
43
+ // Stdout contract (read by setup-tunnel.sh's awk-based reason parser):
30
44
  // cdp-authorize result=<ok|error> reason=<…> elapsed_ms=<…> [detail=<…>]
45
+ // Exactly ONE such line per invocation; the wrapper takes the LAST
46
+ // `result=ok` line via awk so future debug-line additions cannot mis-route.
47
+
48
+ import { MATCH_EXPR } from './_cdp-authorize-matcher.mjs';
31
49
 
32
50
  const CDP_HOST = '127.0.0.1';
33
51
  const CDP_PORT = 9222;
@@ -211,35 +229,16 @@ try {
211
229
  die(1, 'page-load-timeout', { detail: err.message, elapsed_ms: Date.now() - started });
212
230
  }
213
231
 
214
- // Poll for the Authorize button. The expression finds the first
215
- // button/input[type=submit] whose trimmed textContent/value matches
216
- // /^(authorize|connect)$/i, clicks it, and returns a descriptor. Returns
217
- // null when no match yet.
218
- const CLICK_EXPR = `
219
- (() => {
220
- const candidates = Array.from(document.querySelectorAll('button, input[type="submit"]'));
221
- const match = candidates.find((el) => {
222
- const text = (el.textContent ?? el.value ?? '').trim();
223
- return /^(authorize|connect)$/i.test(text) && !el.disabled;
224
- });
225
- if (!match) return null;
226
- const descriptor = {
227
- tag: match.tagName.toLowerCase(),
228
- text: (match.textContent ?? match.value ?? '').trim().slice(0, 40),
229
- disabled: Boolean(match.disabled),
230
- };
231
- match.click();
232
- return descriptor;
233
- })()
234
- `;
235
-
236
- const clickDeadline = Date.now() + BUTTON_POLL_TIMEOUT_MS;
237
- let clicked = null;
238
- while (Date.now() < clickDeadline) {
232
+ // Poll the consent page until one of the three terminal states resolves.
233
+ // MATCH_EXPR is built from _cdp-authorize-matcher.mjs's findMatch so the
234
+ // JSDOM unit tests and the live CDP path execute literally identical logic.
235
+ const matchDeadline = Date.now() + BUTTON_POLL_TIMEOUT_MS;
236
+ let matched = null;
237
+ while (Date.now() < matchDeadline) {
239
238
  let r;
240
239
  try {
241
240
  r = await send('Runtime.evaluate', {
242
- expression: CLICK_EXPR,
241
+ expression: MATCH_EXPR,
243
242
  returnByValue: true,
244
243
  awaitPromise: false,
245
244
  });
@@ -253,22 +252,33 @@ while (Date.now() < clickDeadline) {
253
252
  });
254
253
  }
255
254
  const val = r?.result?.value;
256
- if (val && typeof val === 'object') {
257
- clicked = val;
255
+ if (val && typeof val === 'object' && (val.kind === 'button' || val.kind === 'success')) {
256
+ matched = val;
258
257
  break;
259
258
  }
260
259
  await new Promise((res) => setTimeout(res, BUTTON_POLL_INTERVAL_MS));
261
260
  }
262
261
 
263
- if (!clicked) {
262
+ if (!matched) {
264
263
  die(1, 'authorize-button-not-found', { elapsed_ms: Date.now() - started });
265
264
  }
266
265
 
267
- emit('ok', 'clicked', {
268
- tag: clicked.tag,
269
- text: clicked.text,
270
- elapsed_ms: Date.now() - started,
271
- });
266
+ if (matched.kind === 'button') {
267
+ const d = matched.descriptor ?? {};
268
+ emit('ok', 'clicked', {
269
+ tag: d.tag,
270
+ text: d.text,
271
+ elapsed_ms: Date.now() - started,
272
+ });
273
+ } else {
274
+ // matched.kind === 'success' — Success modal text is on the page; the
275
+ // bound account already has a cert. Caller drops into the cert-poll loop;
276
+ // the in-flight cloudflared subprocess receives the idempotent callback
277
+ // and writes ~/.cloudflared/cert.pem.
278
+ emit('ok', 'cert-already-installed', {
279
+ elapsed_ms: Date.now() - started,
280
+ });
281
+ }
272
282
 
273
283
  try { ws.close(); } catch { /* best-effort */ }
274
284
  process.exit(0);
@@ -60,22 +60,74 @@ CFG_DIR="${HOME}/.${BRAND}/cloudflared"
60
60
  mkdir -p "${CFG_DIR}"
61
61
 
62
62
  # --------------------------------------------------------------------------
63
- # Step 1: OAuth login (only if cert.pem missing). Corresponds to runbook Step 1.
63
+ # Step 1: OAuth login. Corresponds to runbook Step 1.
64
64
  #
65
- # Control flow, rewritten per Task 556:
65
+ # Step 1 is a state machine over three observable variables — brand-scoped
66
+ # cert path, default cert path, and the helper's outcome (Task 855):
67
+ #
68
+ # ${CFG_DIR}/cert.pem present → Step 1 already done; skip.
69
+ # ${CFG_DIR}/cert.pem missing AND
70
+ # ~/.cloudflared/cert.pem present → pre-flight: a prior run
71
+ # reached the OAuth callback
72
+ # but failed before the mv;
73
+ # promote and skip the
74
+ # helper. Idempotent.
75
+ # both missing → spawn cloudflared, drive
76
+ # the consent page via CDP,
77
+ # poll for cert.pem, mv.
78
+ #
79
+ # Control flow when both certs are missing (Task 556 / 588 / 855):
66
80
  # 1. CDP precheck on 127.0.0.1:9222 — loud failure if Chromium isn't up.
67
81
  # 2. Spawn cloudflared with stdout+stderr teed line-by-line to
68
82
  # $STREAM_LOG_PATH with prefix [script:setup-tunnel:cloudflared]
69
83
  # (Task 605's chat-surface namespace — see _stream-log.sh header).
70
84
  # 3. Extract the authorize URL with a tolerant regex as it streams.
71
85
  # 4. Drive the VNC Chromium to that URL via CDP `PUT /json/new?<url>`.
72
- # 5. Wait for ~/.cloudflared/cert.pem to land with bounded timeout.
73
- # 6. Move cert.pem into the brand-scoped path.
86
+ # 5. Run _cdp-authorize.mjs tri-state matcher returns one of:
87
+ # reason=clicked → button matched + clicked
88
+ # reason=cert-already-installed → Success modal detected
89
+ # reason=authorize-button-not-found → loud failure, exit 1
90
+ # (a)+(b) drop into the cert-pem poll; (c) bubbles up.
91
+ # 6. Wait for ~/.cloudflared/cert.pem to land with bounded timeout.
92
+ # 7. Move cert.pem into the brand-scoped path.
74
93
  #
75
- # Every branch exits 1 loudly naming the failure — no silent retries,
94
+ # Every failure branch exits 1 loudly naming the cause — no silent retries,
76
95
  # no xdg-open race, no cloudflared-internal browser-spawn path.
77
96
  # --------------------------------------------------------------------------
78
97
 
98
+ # Pre-flight cert-promotion (Task 855). When ${CFG_DIR}/cert.pem is missing
99
+ # but the OAuth-default ~/.cloudflared/cert.pem is present, a prior run
100
+ # reached the cloudflared callback but did not survive to the brand-scoped
101
+ # mv at the end of Step 1. The cert is bound to the account already; the
102
+ # only remediation is the move. Doing it before re-spawning cloudflared is
103
+ # what stops Step 1 from looping forever on a dashboard that has moved past
104
+ # a button-bearing state.
105
+ #
106
+ # The mv exit code is checked: if it fails (EACCES, ENOSPC, weird FS state),
107
+ # loud-fail with reason=cert-promote-failed instead of pretending Step 1
108
+ # succeeded — the next steps would die opaquely on the missing brand cert.
109
+ # stderr is captured into the phase_line so the operator sees the actual
110
+ # failure cause (mv exit-code 1 alone cannot disambiguate EACCES from ENOSPC).
111
+ if [ ! -f "${CFG_DIR}/cert.pem" ] && [ -f "${HOME}/.cloudflared/cert.pem" ]; then
112
+ MV_ERR="$(mktemp -t maxy-cert-promote-err.XXXXXX)"
113
+ if mv "${HOME}/.cloudflared/cert.pem" "${CFG_DIR}/cert.pem" 2>"${MV_ERR}"; then
114
+ rm -f "${MV_ERR}"
115
+ phase_line setup-tunnel step=oauth-login result=ok \
116
+ reason=cert-promoted-from-default-path waited="0s"
117
+ else
118
+ MV_RC=$?
119
+ MV_STDERR="$(tr '\n' ' ' < "${MV_ERR}" 2>/dev/null | head -c 200 || echo unavailable)"
120
+ rm -f "${MV_ERR}"
121
+ phase_line setup-tunnel step=oauth-login result=error \
122
+ reason=cert-promote-failed mv_rc="${MV_RC}" stderr="${MV_STDERR}" \
123
+ from="${HOME}/.cloudflared/cert.pem" to="${CFG_DIR}/cert.pem"
124
+ echo "ERROR: failed to promote ~/.cloudflared/cert.pem to ${CFG_DIR}/cert.pem (mv exit=${MV_RC})." >&2
125
+ echo " mv stderr: ${MV_STDERR}" >&2
126
+ echo " Step 1 cannot proceed without the cert in the brand-scoped path." >&2
127
+ exit 1
128
+ fi
129
+ fi
130
+
79
131
  if [ ! -f "${CFG_DIR}/cert.pem" ]; then
80
132
  phase_line setup-tunnel step=oauth-login cert_path="${CFG_DIR}/cert.pem" display="${DISPLAY:-:99}"
81
133
 
@@ -215,22 +267,61 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
215
267
  echo "ERROR: _cdp-authorize.mjs helper missing or not executable at ${AUTH_HELPER}" >&2
216
268
  exit 1
217
269
  fi
218
- # tee_subprocess pipes the helper's cdp-authorize result= line into the
219
- # stream log under the setup-tunnel:cdp-click tag so the chat UI renders
220
- # the click outcome in real time. Exit code drives control flow.
221
- if tee_subprocess setup-tunnel:cdp-click -- \
222
- node "${AUTH_HELPER}" "${CDP_TARGET_ID}"; then
223
- phase_line setup-tunnel step=oauth-login result=authorize-clicked \
224
- target_id="${CDP_TARGET_ID}"
270
+ # tee_subprocess_capture pipes the helper's `cdp-authorize result=…` line
271
+ # into the stream log under the setup-tunnel:cdp-click tag (live-tailable)
272
+ # AND through this shell's stdout so the wrapper can capture it for the
273
+ # tri-state reason discriminator (Task 855). The helper writes exactly
274
+ # one such line per invocation and exits; the shape is its contract:
275
+ # `cdp-authorize result=<ok|error> reason=<token> [k=v …]`.
276
+ # The wrapper's awk takes the LAST `cdp-authorize result=ok ` line so that
277
+ # any future emit() additions cannot mis-route the discriminator — the
278
+ # success line is always the last one before exit.
279
+ HELPER_OUT="$(mktemp -t maxy-cdp-authorize-out.XXXXXX)"
280
+ if tee_subprocess_capture setup-tunnel:cdp-click -- \
281
+ node "${AUTH_HELPER}" "${CDP_TARGET_ID}" \
282
+ > "${HELPER_OUT}"; then
283
+ HELPER_REASON="$(awk '/^cdp-authorize result=ok / {m=$0} END {if (m) {match(m, /reason=[a-z0-9-]+/); print substr(m, RSTART+7, RLENGTH-7)}}' "${HELPER_OUT}")"
284
+ rm -f "${HELPER_OUT}"
285
+ case "${HELPER_REASON}" in
286
+ clicked)
287
+ phase_line setup-tunnel step=browser-drive result=ok \
288
+ reason=clicked target_id="${CDP_TARGET_ID}"
289
+ ;;
290
+ cert-already-installed)
291
+ # The dashboard rendered the Success modal — the bound account
292
+ # already has a cert. The OAuth callback URL was exercised by this
293
+ # navigation, so the in-flight cloudflared subprocess receives the
294
+ # idempotent callback and writes ~/.cloudflared/cert.pem; the poll
295
+ # below picks it up.
296
+ phase_line setup-tunnel step=browser-drive result=ok \
297
+ reason=cert-already-installed target_id="${CDP_TARGET_ID}"
298
+ ;;
299
+ *)
300
+ # Helper exited 0 but emitted a reason this wrapper does not
301
+ # recognise — protocol drift between helper and wrapper. Loud-fail
302
+ # rather than silently fall through to the cert-poll, which would
303
+ # mask the cause if the cert never lands.
304
+ kill "${CF_PIPELINE_PID}" 2>/dev/null || true
305
+ phase_line setup-tunnel step=browser-drive result=error \
306
+ reason=helper-unknown-success-reason raw="${HELPER_REASON:-empty}"
307
+ echo "ERROR: CDP helper exited 0 with unrecognised reason: ${HELPER_REASON:-empty}" >&2
308
+ echo " Expected reason=clicked or reason=cert-already-installed." >&2
309
+ echo " Helper / wrapper protocol drift — investigate stream log." >&2
310
+ exit 1
311
+ ;;
312
+ esac
225
313
  else
226
314
  CLICK_RC=$?
315
+ rm -f "${HELPER_OUT}"
227
316
  kill "${CF_PIPELINE_PID}" 2>/dev/null || true
228
- # Helper exit codes: 1=authorize-button-not-found (loud, expected failure
229
- # mode when the VNC browser isn't signed into Cloudflare), 2=cdp-ws-unreachable,
230
- # 3=target-not-found, 4=click-evaluate-threw, 5=protocol-error. All are
231
- # final no retry, no silent fallback. Map the exit code 1:1 to a reason
232
- # string so the stream-log `reason=<…>` is greppable by exact cause; a
233
- # single catch-all reason would defeat the observability contract.
317
+ # Helper exit codes (mapped 1:1 to reason= so stream-log grep is precise;
318
+ # a catch-all reason would defeat the observability contract):
319
+ # 1 = authorize-button-not-found (loud neither button nor success
320
+ # modal matched before BUTTON_POLL_TIMEOUT_MS; the form surfaces
321
+ # a verbatim quote of references/dashboard-guide.md "Authorise a
322
+ # new tunnel" so the operator can recover without SSH).
323
+ # 2 = cdp-ws-unreachable, 3 = target-not-found, 4 = click-evaluate-threw,
324
+ # 5 = protocol-error. All are final — no retry, no silent fallback.
234
325
  case "${CLICK_RC}" in
235
326
  1) CLICK_REASON=authorize-button-not-found ;;
236
327
  2) CLICK_REASON=cdp-ws-unreachable ;;
@@ -242,8 +333,13 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
242
333
  phase_line setup-tunnel step=browser-drive result=error \
243
334
  reason="${CLICK_REASON}" click_rc="${CLICK_RC}"
244
335
  echo "ERROR: CDP-driven Authorize click failed (helper exit=${CLICK_RC}, reason=${CLICK_REASON})." >&2
245
- echo " If the VNC browser is not signed into Cloudflare, sign in manually," >&2
246
- echo " close the sign-in tab, and re-run setup or run ~/reset-tunnel.sh first." >&2
336
+ if [ "${CLICK_REASON}" = "authorize-button-not-found" ]; then
337
+ echo " The dashboard did not present an Authorize button or a Success modal." >&2
338
+ echo " The form will surface the dashboard click-path for manual recovery." >&2
339
+ else
340
+ echo " The CDP helper failed before reaching a terminal page state." >&2
341
+ echo " Re-run setup — or run ~/reset-tunnel.sh first if state is corrupt." >&2
342
+ fi
247
343
  exit 1
248
344
  fi
249
345
  fi # end CDP-available branch (Task 664)
@@ -281,7 +377,7 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
281
377
  echo "ERROR: Timed out after ${LOGIN_WAIT}s waiting for cert.pem to land." >&2
282
378
  exit 1
283
379
  fi
284
- # Heartbeat every 2 s after the click. t=0 is the authorize-clicked
380
+ # Heartbeat every 2 s after the click. t=0 is the browser-drive
285
381
  # phase line above; the first heartbeat fires at t=2. Without this line
286
382
  # the tailer sees silence for the full 1-20 s round-trip — the exact
287
383
  # state Task 588 forbids.
@@ -294,7 +390,7 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
294
390
  done
295
391
 
296
392
  mv "${HOME}/.cloudflared/cert.pem" "${CFG_DIR}/cert.pem"
297
- phase_line setup-tunnel step=oauth-login result=cert-received \
393
+ phase_line setup-tunnel step=oauth-login result=ok \
298
394
  path="${CFG_DIR}/cert.pem" waited="${LOGIN_WAIT}s"
299
395
  fi
300
396
 
@@ -22,6 +22,8 @@ Any Cloudflare action outside these four surfaces is a discipline violation —
22
22
 
23
23
  Use this when the operator wants Cloudflare set up (or re-set up) end-to-end on the device. The script handles OAuth login, tunnel creation, DNS routing for each subdomain, config.yml + tunnel.state, and dispatches the `${BRAND}.service` restart to a transient `systemd-run` unit (Task 558) — all in one invocation. The restart fires a few seconds after the script exits so the script does not kill its own cgroup when invoked via the Bash tool; the chat UI receives a `server_shutdown` SSE frame and reconnects automatically. Post-restart hostname verification is out of scope for the script (connector is not up when the script exits) — verify via the next admin turn or manually with `curl -I https://<hostname>`. Apex hostnames cannot be routed by the CLI; when one is passed, the script prints an `ACTION REQUIRED` block naming the exact dashboard record to edit.
24
24
 
25
+ Step 1's OAuth flow is a state machine over three observable variables: the brand-scoped cert path (`${CFG_DIR}/cert.pem`), the OAuth-default cert path (`~/.cloudflared/cert.pem`), and the consent-page DOM via the CDP helper `_cdp-authorize.mjs` (Task 855). When the brand-scoped cert is missing but the default-path cert is present from any prior partial run, the wrapper promotes it (`mv`) and emits `step=oauth-login result=ok reason=cert-promoted-from-default-path` without re-spawning cloudflared. When both are missing, the helper polls `dash.cloudflare.com/argotunnel` and resolves to one of three terminal outcomes — `reason=clicked` (button matched and clicked), `reason=cert-already-installed` (Success modal text detected — the bound account already has a cert, callback is idempotent so the in-flight cloudflared subprocess writes the cert anyway), or `reason=authorize-button-not-found` (neither anchor matched; loud failure, exit 1). The first two paths drop into the existing brand-scoped cert poll; the third surfaces a remediation card in the form so the operator can complete the consent click in their own browser without SSH or `~/reset-tunnel.sh`.
26
+
25
27
  ### How inputs reach the script
26
28
 
27
29
  Inputs arrive through the `cloudflare-setup-form` component, not agent Q&A. The onboarding skill renders the form, the user submits admin/public/apex labels and the admin password in one action, and the `/api/admin/cloudflare/setup` endpoint runs (in this order):
@@ -50,6 +52,8 @@ The agent does not invoke the script directly during onboarding — the endpoint
50
52
 
51
53
  The endpoint returns `{ ok: false, field: "script", message, output }` and the form surfaces the error inline. Relay the output to the user, name the exit code, and cite `references/reset-guide.md` for the next action. Offer to re-render the form after any manual steps the script's error output named. Do not attempt a second invocation outside the form, a Playwright-driven dashboard inspection, or an alternative `cloudflared` command sequence. The discipline rule below applies.
52
54
 
55
+ When the failure reason is `authorize-button-not-found` (Task 855), the form renders a verbatim quote of `references/dashboard-guide.md` § "Authorise a new tunnel (pick the right zone)" alongside a "Try again" button — the operator completes the consent click in their own browser, then re-submits the same form. No agent action is required; relay the form's chat acknowledgement when it arrives. Do not suggest `~/reset-tunnel.sh` for this reason — the cert path is intact and a fresh consent click is the only remediation needed.
56
+
53
57
  ---
54
58
 
55
59
  ## 2. Manual fallback — `references/manual-setup.md`
@@ -40,7 +40,7 @@ These are enabled during onboarding and can be added or removed at any time. Som
40
40
  | `waitlist` | Waitlist lifecycle — extract sign-ups from conversations, review | — |
41
41
  | `replicate` | Image generation — three models for photorealistic, design, and fast draft images | Content producer, Research assistant |
42
42
  | `linkedin-import` | Import a LinkedIn Basic Data Export — Profile and Connections today, more CSVs as references land | Database operator |
43
- | `whatsapp-import` | Import a WhatsApp `_chat.txt` export as a Conversation + chronologically-chained Messages, then extract typed insights (mentions, preferences, commitments, observed relationships) — distinct from the live `whatsapp` plugin which is a Baileys QR-pairing channel | Database operator |
43
+ | `whatsapp-import` | Import a WhatsApp `_chat.txt` export as a Conversation + chronologically-chained Messages plus `:Observation` nodes for typed insights (mentions, tasks, preferences, observed relationships). Single Bash entry `bash platform/plugins/whatsapp-import/bin/whatsapp-ingest.sh <archive> --owner-element-id <id> --scope <admin\|public>` runs parse → archive-write → Haiku insight in one process. Distinct from the live `whatsapp` plugin which is a Baileys QR-pairing channel. | Database operator |
44
44
 
45
45
  ### Claude Official (marketplace)
46
46
 
@@ -14,7 +14,7 @@ Ingests a WhatsApp "Export Chat" archive (the `_chat.txt` file plus media attach
14
14
 
15
15
  ## When this applies
16
16
 
17
- The admin agent delegates to `database-operator` when the operator drops a `_chat.txt` (or its containing folder) into chat. The specialist runs the skill's archive-owner + participant confirmation flow before any line is written, then ingests the messages via `mcp__memory__memory-archive-write` with `archiveType='whatsapp-export'`.
17
+ The admin agent delegates to `database-operator` when the operator drops a `_chat.txt` (or its containing folder) into chat. The specialist runs the skill's archive-owner confirmation flow before any line is written, then invokes the deterministic Bash entry (`bin/whatsapp-ingest.sh`) once: parse, archive-write (via `memoryArchiveWrite` in-process), and Haiku insight all run in one Node process — no MCP envelope between steps (Task 855).
18
18
 
19
19
  ## Accepted export shapes
20
20
 
@@ -28,6 +28,6 @@ WhatsApp's "Export Chat" emits `[DD/MM/YYYY, HH:MM:SS]` prefixes by default in m
28
28
 
29
29
  ## Relationship to other plugins
30
30
 
31
- - **memory** — the underlying write surface used by the skill (`memory-archive-write` for bulk Conversation+Messages, `memory-write` / `memory-update` for the second-pass typed observations). The skill is parameterised so all writes carry `source='whatsapp'` + `createdByAgent='whatsapp-import'` for provenance.
31
+ - **memory** — the underlying write surface imported in-process by `bin/ingest.mjs` (`memoryArchiveWrite` for bulk Conversation+Messages; direct Cypher `:Observation` writes for the insight pass). All writes carry `source='whatsapp'` + `createdByAgent='whatsapp-import'` provenance. The legacy `mcp__memory__whatsapp-export-parse` / `whatsapp-export-insight-write` MCP tools and the direct `memory-archive-write` MCP path with `archiveType=whatsapp-export` are blocked at the harness — the Bash entry is the only supported invocation surface (Task 855).
32
32
  - **database-operator specialist** — owns execution. See [admin/IDENTITY.md](../../../platform/templates/agents/admin/IDENTITY.md) delegation clause and [database-operator.md](../../../platform/templates/specialists/agents/database-operator.md) per-source archive list.
33
33
  - **linkedin-import** — sister plugin under the same pattern (LinkedIn Basic Data Export). Reading [linkedin-import/PLUGIN.md](../linkedin-import/PLUGIN.md) is the fastest way to understand the shape this plugin follows.