@rubytech/create-maxy-code 0.1.300 → 0.1.301

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 (92) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/PLUGIN.md +10 -10
  3. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +84 -84
  4. package/payload/platform/plugins/admin/skills/public-agent-manager/SKILL.md +4 -4
  5. package/payload/platform/plugins/aeo/PLUGIN.md +1 -1
  6. package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +4 -4
  7. package/payload/platform/plugins/cloudflare/references/manual-setup.md +14 -8
  8. package/payload/platform/plugins/cloudflare/skills/cloudflare/SKILL.md +3 -0
  9. package/payload/platform/plugins/contacts/PLUGIN.md +1 -1
  10. package/payload/platform/plugins/docs/references/admin-identity-gate.md +6 -6
  11. package/payload/platform/plugins/docs/references/admin-session.md +15 -15
  12. package/payload/platform/plugins/docs/references/admin-ui.md +18 -18
  13. package/payload/platform/plugins/docs/references/aeo.md +1 -1
  14. package/payload/platform/plugins/docs/references/deployment.md +3 -3
  15. package/payload/platform/plugins/docs/references/internals.md +28 -28
  16. package/payload/platform/plugins/docs/references/platform.md +21 -21
  17. package/payload/platform/plugins/docs/references/plugins-guide.md +3 -3
  18. package/payload/platform/plugins/docs/references/troubleshooting.md +6 -6
  19. package/payload/platform/plugins/docs/references/visitor-graph.md +3 -3
  20. package/payload/platform/plugins/email/PLUGIN.md +2 -2
  21. package/payload/platform/plugins/email/references/email-reference.md +2 -2
  22. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md +2 -2
  23. package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
  24. package/payload/platform/plugins/memory/PLUGIN.md +9 -9
  25. package/payload/platform/plugins/memory/references/graph-primitives.md +2 -2
  26. package/payload/platform/plugins/memory/references/schema-base.md +14 -14
  27. package/payload/platform/plugins/memory/references/transcript-formats/circleback.md +1 -1
  28. package/payload/platform/plugins/memory/references/transcript-formats/otter.md +1 -1
  29. package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +6 -6
  30. package/payload/platform/plugins/memory/skills/conversation-archive-enrich/SKILL.md +1 -1
  31. package/payload/platform/plugins/memory/skills/conversation-archive-mcp/SKILL.md +8 -8
  32. package/payload/platform/plugins/memory/skills/document-ingest/SKILL.md +5 -5
  33. package/payload/platform/plugins/obsidian-import/skills/obsidian-import/references/daily-notes.md +1 -1
  34. package/payload/platform/plugins/prompt-optimiser/skills/prompt-optimiser/SKILL.md +1 -1
  35. package/payload/platform/plugins/slides/PROVENANCE.md +1 -1
  36. package/payload/platform/plugins/substack-import/skills/substack-import/SKILL.md +1 -1
  37. package/payload/platform/plugins/telegram/PLUGIN.md +1 -1
  38. package/payload/platform/plugins/url-get/PLUGIN.md +2 -2
  39. package/payload/platform/plugins/whatsapp/.claude-plugin/plugin.json +1 -1
  40. package/payload/platform/plugins/whatsapp/PLUGIN.md +5 -5
  41. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +5 -5
  42. package/payload/platform/plugins/whatsapp/skills/manage-whatsapp-config/SKILL.md +1 -1
  43. package/payload/platform/plugins/workflows/PLUGIN.md +1 -1
  44. package/payload/platform/plugins/x-import/PLUGIN.md +2 -2
  45. package/payload/platform/plugins/x-import/skills/x-import/SKILL.md +1 -1
  46. package/payload/platform/scripts/__tests__/check-no-task-id-leaks.test.sh +125 -0
  47. package/payload/platform/scripts/__tests__/resume-tunnel.test.sh +253 -0
  48. package/payload/platform/scripts/check-no-task-id-leaks.mjs +54 -6
  49. package/payload/platform/scripts/resume-tunnel.sh +89 -5
  50. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +14 -0
  51. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
  52. package/payload/platform/services/whatsapp-channel/dist/notification.js +11 -0
  53. package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
  54. package/payload/platform/services/whatsapp-channel/dist/server.d.ts +4 -0
  55. package/payload/platform/services/whatsapp-channel/dist/server.d.ts.map +1 -1
  56. package/payload/platform/services/whatsapp-channel/dist/server.js +54 -3
  57. package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
  58. package/payload/platform/services/whatsapp-channel/dist/turn-follow.d.ts +18 -0
  59. package/payload/platform/services/whatsapp-channel/dist/turn-follow.d.ts.map +1 -0
  60. package/payload/platform/services/whatsapp-channel/dist/turn-follow.js +137 -0
  61. package/payload/platform/services/whatsapp-channel/dist/turn-follow.js.map +1 -0
  62. package/payload/platform/templates/specialists/agents/database-operator.md +1 -1
  63. package/payload/platform/templates/specialists/agents/librarian.md +2 -2
  64. package/payload/server/public/assets/{AdminShell-BFdBxg4P.js → AdminShell-BJrxifYa.js} +1 -1
  65. package/payload/server/public/assets/{Checkbox-BZ1XbA1t.js → Checkbox-C7bBRzLP.js} +1 -1
  66. package/payload/server/public/assets/{Transcript-CaVix6W0.js → Transcript-bGy_Gx99.js} +1 -1
  67. package/payload/server/public/assets/admin-jYKA7jR5.js +1 -0
  68. package/payload/server/public/assets/{audio-attachment-mime-CFyc3gpD.js → audio-attachment-mime-DL5R1M2A.js} +1 -1
  69. package/payload/server/public/assets/{browser-ClnGUKp-.js → browser-7fyPCFWB.js} +1 -1
  70. package/payload/server/public/assets/{chat-ZTAhSoJV.js → chat-UF9ATDwe.js} +1 -1
  71. package/payload/server/public/assets/{data-DDjeQQ7e.js → data-CIJnkfgX.js} +1 -1
  72. package/payload/server/public/assets/{file-download-DBMW0BNz.js → file-download-gth6Gpmn.js} +1 -1
  73. package/payload/server/public/assets/{graph-CR74qec3.js → graph-DHnB3BHK.js} +1 -1
  74. package/payload/server/public/assets/{graph-labels-1Uz0ZBRd.js → graph-labels-huk-fKcY.js} +1 -1
  75. package/payload/server/public/assets/jsx-runtime-DZddXDvJ.css +1 -0
  76. package/payload/server/public/assets/operator-D3QAzlPs.js +1 -0
  77. package/payload/server/public/assets/page-86edaOS1.js +1 -0
  78. package/payload/server/public/assets/{public-W-HholkU.js → public-B4LqL6lA.js} +1 -1
  79. package/payload/server/public/assets/{useSelectionMode-geYq13zs.js → useSelectionMode-lrH0qwAR.js} +1 -1
  80. package/payload/server/public/browser.html +6 -6
  81. package/payload/server/public/chat.html +8 -8
  82. package/payload/server/public/data.html +5 -5
  83. package/payload/server/public/graph.html +8 -8
  84. package/payload/server/public/index.html +9 -9
  85. package/payload/server/public/operator.html +9 -9
  86. package/payload/server/public/public.html +6 -6
  87. package/payload/server/server.js +51 -6
  88. package/payload/server/public/assets/admin-BK7xIr3o.js +0 -1
  89. package/payload/server/public/assets/jsx-runtime-BqGVUT1h.css +0 -1
  90. package/payload/server/public/assets/operator-QP_z97s8.js +0 -1
  91. package/payload/server/public/assets/page-CLijftr1.js +0 -1
  92. /package/payload/server/public/assets/{jsx-runtime-D3JkFIbi.js → jsx-runtime-CMu8uN0-.js} +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whatsapp",
3
- "description": "WhatsApp messaging channel — Baileys QR pairing, DM policy enforcement, inbound routing, business hours gating, conversation browsing. Reply-only in DMs (no agent-initiated send, Task 723); observation-only in groups (the agent never responds in a group, Task 726).",
3
+ "description": "WhatsApp messaging channel — Baileys QR pairing, DM policy enforcement, inbound routing, business hours gating, conversation browsing. Reply-only in DMs (no agent-initiated send); observation-only in groups (the agent never responds in a group).",
4
4
  "version": "0.1.0",
5
5
  "author": {
6
6
  "name": "Rubytech LLC"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: whatsapp
3
- description: WhatsApp messaging channel — Baileys QR pairing, DM policy enforcement, inbound routing, business hours gating, conversation browsing. Reply-only in DMs (no agent-initiated send, Task 723); observation-only in groups (the agent never responds in a group, Task 726).
3
+ description: WhatsApp messaging channel — Baileys QR pairing, DM policy enforcement, inbound routing, business hours gating, conversation browsing. Reply-only in DMs (no agent-initiated send); observation-only in groups (the agent never responds in a group).
4
4
  tools:
5
5
  - name: whatsapp-login-start
6
6
  publicAllowlist: false
@@ -57,7 +57,7 @@ Activate when the user asks about WhatsApp — connecting, linking, checking sta
57
57
  - QR-code-based WhatsApp pairing via Baileys (linked device) — `whatsapp-login-start` returns the QR as a scannable Unicode block printed on the PTY surface (the only operator surface); the operator scans it directly from the terminal output
58
58
  - Multi-account support (optional, multiple linked numbers)
59
59
  - Inbound message routing — sender-keyed `claude` PTY sessions via the loopback claude-session-manager (admin phone → admin agent, public → resolved public agent); a "typing…" presence is sent on receipt as an ack while the agent turn runs
60
- - Group observation — group messages are stored, persisted, and browsable, but the agent never responds in a group (observation-only, Task 726)
60
+ - Group observation — group messages are stored, persisted, and browsable, but the agent never responds in a group (observation-only)
61
61
  - DM policy enforcement (open, allowlist, disabled)
62
62
  - Conversation browsing — list active conversations, read message history, inspect group metadata
63
63
  - Inbound voice note transcription (OGG Opus → text via local whisper.cpp)
@@ -76,17 +76,17 @@ Four tools for reading WhatsApp conversation context. The first two read from th
76
76
  | `whatsapp-conversation-graph-state` | Persisted `:Message:WhatsAppMessage` rows attached to the `:Conversation` (sessionKey-keyed) plus the resolved `sessionId`, the cypher string, and a divergence header reporting graph-row count vs in-memory count. Use when the question is "what got persisted" — never compose the cypher manually |
77
77
  | `whatsapp-group-info` | Group metadata from WhatsApp servers — subject, description, participants with admin flags, creation date. Requires active connection |
78
78
 
79
- ## Group access — observation-only (Task 726)
79
+ ## Group access — observation-only
80
80
 
81
81
  The agent never responds in a group. `checkGroupAccess` ([`inbound/access-control.ts`](../../ui/app/lib/whatsapp/inbound/access-control.ts)) is a structural observe-only deny — it always returns `allowed:false reason:group-observe-only`, so every group inbound ends at the manager's `!accessResult.allowed` return before dispatch. There is no group policy, allowlist, per-group activation, or @mention setting; setting any of those keys is a `.strict()` schema rejection, not a silently-dead key. A belt invariant in the manager's `reply` closure refuses any `@g.us` recipient and logs `op=group-reply-blocked` — that line is the leak signature if a dispatch path ever leaks.
82
82
 
83
83
  Observation is fully preserved: group messages are stored in the ring buffer, recorded as activity, and persisted to the graph at the access-independent capture site, and remain visible via `whatsapp-conversations`, `whatsapp-group-info`, `list-groups`, and the `/whatsapp` reader. This is a Meta-policy constraint (automated messaging from an unofficial linked device is penalised, the same class as the reply-only DM posture); for agent-driven group messaging, Telegram is the recommended channel.
84
84
 
85
- ## Native channel reply surface (Task 751)
85
+ ## Native channel reply surface
86
86
 
87
87
  A native admin channel session runs a per-sender `whatsapp-channel` MCP server (`platform/services/whatsapp-channel/`) exposing two reply-only tools: `reply` (text) and `reply-document` (one or more files delivered as WhatsApp documents). `reply-document` POSTs to the gateway's `POST /wa-channel/reply-document`, which funnels through `sendWhatsAppDocument` — the single send core that owns account-dir path validation, the 100 MB ceiling, the canonical `[whatsapp:outbound] sent document …` log, and per-file reconciliation (`op=reply-document-reconciled` on delivery, `file-delivery-unreconciled` on a miss — no silent failure). A bare `SendUserFile` is also caught and delivered through the same core by a read-only JSONL follower on the session, so an attachment reaches the sender even when the agent skips the explicit tool. Both surfaces are reply-only: a file reply to a sender with no live conversation is refused. See [channels-whatsapp.md](references/channels-whatsapp.md) and `.docs/whatsapp-inbound-lifeline.md`.
88
88
 
89
- ## Self-chat command lane (Task 774)
89
+ ## Self-chat command lane
90
90
 
91
91
  The agent links as a companion device on the operator's own WhatsApp account, so the operator cannot DM the agent from the paired handset — those messages arrive as `fromMe=true` / upsert type `append`. The operator's **self-chat** (message-yourself thread) is the command lane from that handset: the `append` branch in `manager.ts` evaluates a six-clause discriminator (`inbound/self-chat.ts`) — fromMe, self-surface jid (selfJid/selfLid/`selfPhone@s.whatsapp.net`, device-suffix normalised), not an agent-sent echo, timestamp strictly after `lastConnectedAt` (excludes reconnect history backfill; restart-safe backstop for the 2-min echo cache), not a duplicate, non-empty text — and dispatches a match to the **admin** agent with raw text; the reply lands back in the self-chat. Every self-chat `fromMe` message emits one `[whatsapp:self-chat] op=seen` line followed by `op=dispatch` or `op=skip reason=echo|backfill|duplicate|no-text`, so a dropped command is never silent. `fromMe` in any other thread is unchanged (`[Owner reply]` mirror). See [channels-whatsapp.md](references/channels-whatsapp.md) §Self-chat command lane.
92
92
 
@@ -6,13 +6,13 @@ WhatsApp connects via Baileys multi-account:
6
6
  |----------|----------|------------|
7
7
  | **baileys** | All WhatsApp accounts | WebSocket via QR code (linked device) |
8
8
 
9
- ## Reply-only posture (Task 723)
9
+ ## Reply-only posture
10
10
 
11
11
  The agent has **no tool to initiate a WhatsApp message to an arbitrary number.** The `whatsapp-send` and `whatsapp-send-document` MCP tools were removed after an agent-initiated cold first-contact send (to a number with no prior conversation) tripped WhatsApp's anti-automation guard and restricted the operator's account. Initiating is intentionally unavailable, not merely gated — a soft approval gate already existed and did not prevent the incident.
12
12
 
13
- What the agent **can** still do: reply within an existing chat. On a native admin channel session the whatsapp-channel server (`platform/services/whatsapp-channel/`) exposes two reply tools, both reply-only: `reply` (text) and `reply-document` (one or more files delivered as WhatsApp documents). `reply-document` POSTs to `POST /wa-channel/reply-document` → `sendWhatsAppDocument`, the one send core that owns path validation, the 100 MB ceiling, the canonical log, and per-file reconciliation (Task 751). A bare `SendUserFile` is also caught and delivered through the same core by a read-only JSONL follower on the session, so an attachment reaches the sender even if the agent skips the explicit tool. File delivery is refused for a sender with no live conversation — same reply-only posture as text. (The removed `whatsapp-send-document` route tool and the bridge path `SendUserFile` → `file-delivery-bridge.ts` → `POST /api/whatsapp/send-document` are the legacy webchat/email bridge surface, not the native WhatsApp channel.) Replying within your chats is what WhatsApp permits; starting new chats from a linked device is what it penalises.
13
+ What the agent **can** still do: reply within an existing chat. On a native admin channel session the whatsapp-channel server (`platform/services/whatsapp-channel/`) exposes two reply tools, both reply-only: `reply` (text) and `reply-document` (one or more files delivered as WhatsApp documents). `reply-document` POSTs to `POST /wa-channel/reply-document` → `sendWhatsAppDocument`, the one send core that owns path validation, the 100 MB ceiling, the canonical log, and per-file reconciliation. A bare `SendUserFile` is also caught and delivered through the same core by a read-only JSONL follower on the session, so an attachment reaches the sender even if the agent skips the explicit tool. File delivery is refused for a sender with no live conversation — same reply-only posture as text. (The removed `whatsapp-send-document` route tool and the bridge path `SendUserFile` → `file-delivery-bridge.ts` → `POST /api/whatsapp/send-document` are the legacy webchat/email bridge surface, not the native WhatsApp channel.) Replying within your chats is what WhatsApp permits; starting new chats from a linked device is what it penalises.
14
14
 
15
- ## Observation-only in groups (Task 726)
15
+ ## Observation-only in groups
16
16
 
17
17
  The agent **never responds in a group chat.** Every inbound group message is stored, recorded, and persisted to the graph (observation is fully preserved — `list-groups`, `whatsapp-group-info`, conversations, and the `/whatsapp` reader all still work), but it is never dispatched to a responding session and never produces an outbound message into the group. `checkGroupAccess` is a structural observe-only deny — there is no group policy, allowlist, per-group activation, or @mention setting to turn responses on. A belt invariant in the manager's `reply` closure refuses any `@g.us` recipient (`op=group-reply-blocked`).
18
18
 
@@ -118,7 +118,7 @@ Admin phones always bypass DM policy — the `isAdminPhone()` check runs before
118
118
 
119
119
  **Schema constraint:** `dmPolicy: "open"` requires `allowFrom` to include `"*"` (enforced by Zod validation).
120
120
 
121
- **Key file:** `access-control.ts` — `checkDmAccess()` gates DMs by the policy above; `checkGroupAccess()` is a structural observe-only deny (always `allowed:false reason:group-observe-only`, Task 726) — no group policy is consulted.
121
+ **Key file:** `access-control.ts` — `checkDmAccess()` gates DMs by the policy above; `checkGroupAccess()` is a structural observe-only deny (always `allowed:false reason:group-observe-only`) — no group policy is consulted.
122
122
 
123
123
  **Config path:** Per-account at `whatsapp.accounts.{accountId}.dmPolicy` in `account.json`. Falls back to `whatsapp.dmPolicy`. Changes are conversational — the admin agent writes to config via `whatsapp-config action: "update-config"`.
124
124
 
@@ -150,7 +150,7 @@ The dedupe cache (`agentSentMessages`) has a 2-minute TTL and 500-entry cap, whi
150
150
  | `src/web/inbound/dedupe.ts` | `trackAgentSentMessage` / `isAgentSentMessage` — echo suppression cache |
151
151
  | `src/infra/outbound/cross-agent-echo.ts` | Adapter: agent tool → `mirrorOwnerReplyToSession` |
152
152
 
153
- ## Self-chat command lane (Task 774)
153
+ ## Self-chat command lane
154
154
 
155
155
  The companion-device link means the operator's paired phone cannot DM the agent — a message typed from the paired number arrives as `fromMe=true` with upsert type `append`, which the manager's append branch historically dropped before dispatch. The operator's **self-chat** (the message-yourself thread, `remoteJid` == the account's own jid/lid) is the exception: the agent never writes there first, so it is an unambiguous admin command surface.
156
156
 
@@ -70,7 +70,7 @@ The form's `submitMessage` must map the field values back to `update-config` fie
70
70
 
71
71
  The agent is **observation-only in groups**: it sees and records every group message but never replies in a group. There is no per-group setting to turn responses on — the restriction is structural, not a toggle. `whatsapp-config action: list-groups` still lists the groups the connected phone is in, for reference.
72
72
 
73
- This is a **Meta-policy constraint, not a Maxy limitation.** WhatsApp penalises automated and business-style messaging sent from an unofficial linked device. Replying in groups and sending cold first-contact messages are both anti-automation triggers — they restricted a live operator account, which is why the agent is observe-only in groups and reply-only in DMs (Task 723).
73
+ This is a **Meta-policy constraint, not a Maxy limitation.** WhatsApp penalises automated and business-style messaging sent from an unofficial linked device. Replying in groups and sending cold first-contact messages are both anti-automation triggers — they restricted a live operator account, which is why the agent is observe-only in groups and reply-only in DMs.
74
74
 
75
75
  If the user wants an agent that actively messages in groups or initiates conversations, recommend **Telegram** instead. Telegram has no equivalent linked-device automation penalty, so the agent can send and respond freely there. Point them to the Telegram setup path at `platform/plugins/telegram/references/setup-guide.md`.
76
76
 
@@ -124,7 +124,7 @@ Each step declares an `onFailure` policy:
124
124
  | `skipped` | Step failed but `onFailure: "skip"` kept execution going |
125
125
  | `failed` | Step failed and `onFailure: "abort"` stopped execution |
126
126
 
127
- (Historic `degraded` step results from before task 127 remain in the graph; the runner no longer produces them.)
127
+ (Historic `degraded` step results from an earlier runner version remain in the graph; the runner no longer produces them.)
128
128
 
129
129
  ## Execution History
130
130
 
@@ -21,7 +21,7 @@ The admin agent delegates to `specialists:database-operator` when the operator d
21
21
 
22
22
  ## What this plugin is not
23
23
 
24
- - **Not a new graph writer.** Every write routes through the existing `memory-ingest` surface — the document path keyed on `attachmentId` (tweet stream) writes `:KnowledgeDocument`, or the conversation-archive path keyed on `conversationIdentity` (DMs) writes `:ConversationArchive` (Task 397). Both attach `:Section` children. No bespoke `:Post`, `:DirectMessage`, `:DMThread`, `:Attachment`, or `:Thread` label is introduced; per-thread KD granularity is explicitly rejected.
24
+ - **Not a new graph writer.** Every write routes through the existing `memory-ingest` surface — the document path keyed on `attachmentId` (tweet stream) writes `:KnowledgeDocument`, or the conversation-archive path keyed on `conversationIdentity` (DMs) writes `:ConversationArchive`. Both attach `:Section` children. No bespoke `:Post`, `:DirectMessage`, `:DMThread`, `:Attachment`, or `:Thread` label is introduced; per-thread KD granularity is explicitly rejected.
25
25
  - **Not a live X API client.** Archive-only — the X API is rate-limited and paid.
26
26
  - **Not a retweet importer.** Retweets-without-comment are not persisted reliably in X archives; the skill drops them.
27
27
 
@@ -29,5 +29,5 @@ The admin agent delegates to `specialists:database-operator` when the operator d
29
29
 
30
30
  - **memory** — the underlying graph-write surface used by the skill (`mcp__plugin_memory_memory__memory-write` etc.). The skill passes `source='x'` / `source='x-dm'` and `createdByAgent='x-import'` for provenance on every write.
31
31
  - **document-ingest** (memory skill) — owns the tweet-stream KnowledgeDocument writes. `:Section` classification is its responsibility, not this plugin's.
32
- - **conversation-archive** (memory skill) — owns the DM transcript writes (`:ConversationArchive` keyed on `conversationIdentity` — Task 397). The `x-dm` enum entry on `CONVERSATION_SOURCES` and the sibling normaliser in `platform/plugins/memory/mcp/src/lib/conversation-normalisers/x-dm.ts` are the only new code outside this plugin.
32
+ - **conversation-archive** (memory skill) — owns the DM transcript writes (`:ConversationArchive` keyed on `conversationIdentity`). The `x-dm` enum entry on `CONVERSATION_SOURCES` and the sibling normaliser in `platform/plugins/memory/mcp/src/lib/conversation-normalisers/x-dm.ts` are the only new code outside this plugin.
33
33
  - **database-operator specialist** — owns execution. See [platform/templates/specialists/agents/database-operator.md](../../templates/specialists/agents/database-operator.md) external-archive ingestion clause.
@@ -118,6 +118,6 @@ Every `.js` shim read by this skill is gated through the wrapper regex documente
118
118
 
119
119
  - **Per-thread KDs.** One archive = one tweet-stream KD. Thread boundaries are section boundaries the classifier owns, not graph objects.
120
120
  - **Bespoke labels.** No `:Post`, `:Tweet`, `:DirectMessage`, `:DMThread`, `:Attachment`. Everything maps to the existing `:KnowledgeDocument` + `:ConversationArchive` + `:ImageObject` shapes.
121
- - **Tweet-level typed edges.** `:REPLIES_TO` / `:QUOTES` as graph edges off individual tweets are rejected; document-level `REPLIED_TO` / `QUOTED` from the KD to each `:Person` covers the same traversals at the right granularity. The Task 305 typed-edge pass enumerates the new KD and writes any further `COMMITTED_TO` / `MENTIONS` / `REFERENCES` edges from in-body references.
121
+ - **Tweet-level typed edges.** `:REPLIES_TO` / `:QUOTES` as graph edges off individual tweets are rejected; document-level `REPLIED_TO` / `QUOTED` from the KD to each `:Person` covers the same traversals at the right granularity. The typed-edge pass enumerates the new KD and writes any further `COMMITTED_TO` / `MENTIONS` / `REFERENCES` edges from in-body references.
122
122
  - **Retweets-without-comment.** Not persisted reliably in X archives; dropped at parse.
123
123
  - **Live X API.** Archive-only.
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env bash
2
+ # Standing guard for the markdown task-id leak scan. If a fixture citation under
3
+ # a shipped plugin/template .md stops failing the gate, the markdown coverage
4
+ # regressed back to code-only and internal task numbers can ship to clients
5
+ # again. The gate reads its scan root from TASK_ID_LEAK_ROOT so this test points
6
+ # it at a mktemp fixture tree, never the real corpus.
7
+ set -u
8
+ GATE="$(cd "$(dirname "$0")/.." && pwd)/check-no-task-id-leaks.mjs"
9
+ [[ -f "$GATE" ]] || { echo "FAIL: $GATE missing" >&2; exit 1; }
10
+ PASS=0; FAIL=0
11
+
12
+ ROOT=$(mktemp -d)
13
+ trap 'rm -rf "$ROOT"' EXIT
14
+ mkdir -p "$ROOT/platform/plugins/demo/skills/demo" \
15
+ "$ROOT/platform/templates/specialists" \
16
+ "$ROOT/.docs"
17
+
18
+ clean_skill() { printf '# Demo skill\n\nDoes a thing.\n' > "$ROOT/platform/plugins/demo/skills/demo/SKILL.md"; }
19
+ clean_skill
20
+
21
+ # 1. Clean corpus -> gate passes.
22
+ out=$(TASK_ID_LEAK_ROOT="$ROOT" node "$GATE" 2>&1); rc=$?
23
+ if [[ "$rc" -eq 0 ]]; then
24
+ echo "PASS: clean markdown corpus -> exit 0"; PASS=$((PASS+1))
25
+ else
26
+ echo "FAIL: clean corpus tripped gate (rc=$rc): $out" >&2; FAIL=$((FAIL+1))
27
+ fi
28
+
29
+ # 2. `(Task 999)` parenthetical under a shipped skill -> gate fails + names it.
30
+ printf '# Demo skill\n\nDoes a thing (Task 999).\n' > "$ROOT/platform/plugins/demo/skills/demo/SKILL.md"
31
+ out=$(TASK_ID_LEAK_ROOT="$ROOT" node "$GATE" 2>&1); rc=$?
32
+ if [[ "$rc" -ne 0 ]] \
33
+ && printf '%s' "$out" | grep -q '\[task-id-leak\] file=platform/plugins/demo/skills/demo/SKILL.md'; then
34
+ echo "PASS: (Task 999) in shipped skill fails the gate"; PASS=$((PASS+1))
35
+ else
36
+ echo "FAIL: parenthetical citation not caught (rc=$rc): $out" >&2; FAIL=$((FAIL+1))
37
+ fi
38
+ clean_skill
39
+
40
+ # 3. Bare `Task 999` (prose form) under a shipped template -> gate fails + names it.
41
+ printf 'Task 999 changed the routing.\n' > "$ROOT/platform/templates/specialists/AGENT.md"
42
+ out=$(TASK_ID_LEAK_ROOT="$ROOT" node "$GATE" 2>&1); rc=$?
43
+ if [[ "$rc" -ne 0 ]] \
44
+ && printf '%s' "$out" | grep -q '\[task-id-leak\] file=platform/templates/specialists/AGENT.md'; then
45
+ echo "PASS: bare Task 999 in shipped template fails the gate"; PASS=$((PASS+1))
46
+ else
47
+ echo "FAIL: bare prose citation not caught (rc=$rc): $out" >&2; FAIL=$((FAIL+1))
48
+ fi
49
+ rm -f "$ROOT/platform/templates/specialists/AGENT.md"
50
+
51
+ # 4. `Tasks 12 and 34` plural form under a shipped skill -> gate fails.
52
+ printf '# Demo skill\n\nMerged behaviour (Tasks 12 and 34).\n' > "$ROOT/platform/plugins/demo/skills/demo/SKILL.md"
53
+ out=$(TASK_ID_LEAK_ROOT="$ROOT" node "$GATE" 2>&1); rc=$?
54
+ if [[ "$rc" -ne 0 ]] \
55
+ && printf '%s' "$out" | grep -q '\[task-id-leak\] .*match=Tasks 12'; then
56
+ echo "PASS: plural Tasks NNN form fails the gate"; PASS=$((PASS+1))
57
+ else
58
+ echo "FAIL: plural citation not caught (rc=$rc): $out" >&2; FAIL=$((FAIL+1))
59
+ fi
60
+ clean_skill
61
+
62
+ # 4b. Hyphenated `Pre-Task-748` form under a shipped skill -> gate fails.
63
+ printf '# Demo skill\n\nPre-Task-748 the index was small.\n' > "$ROOT/platform/plugins/demo/skills/demo/SKILL.md"
64
+ out=$(TASK_ID_LEAK_ROOT="$ROOT" node "$GATE" 2>&1); rc=$?
65
+ if [[ "$rc" -ne 0 ]] \
66
+ && printf '%s' "$out" | grep -q '\[task-id-leak\] .*match=Task-748'; then
67
+ echo "PASS: hyphenated Pre-Task-748 form fails the gate"; PASS=$((PASS+1))
68
+ else
69
+ echo "FAIL: hyphenated citation not caught (rc=$rc): $out" >&2; FAIL=$((FAIL+1))
70
+ fi
71
+ clean_skill
72
+
73
+ # 4c. A lowercase `task-836-foo` slug (branch/file name) -> gate does NOT trip.
74
+ printf '# Demo skill\n\nWork landed on branch task-836-foo.\n' > "$ROOT/platform/plugins/demo/skills/demo/SKILL.md"
75
+ out=$(TASK_ID_LEAK_ROOT="$ROOT" node "$GATE" 2>&1); rc=$?
76
+ if [[ "$rc" -eq 0 ]]; then
77
+ echo "PASS: lowercase task-836 slug is not flagged"; PASS=$((PASS+1))
78
+ else
79
+ echo "FAIL: lowercase slug wrongly tripped gate (rc=$rc): $out" >&2; FAIL=$((FAIL+1))
80
+ fi
81
+ clean_skill
82
+
83
+ # 4d. Lowercase prose `before task 127` under a shipped skill -> gate fails.
84
+ printf '# Demo skill\n\nResults from before task 127 remain in the graph.\n' > "$ROOT/platform/plugins/demo/skills/demo/SKILL.md"
85
+ out=$(TASK_ID_LEAK_ROOT="$ROOT" node "$GATE" 2>&1); rc=$?
86
+ if [[ "$rc" -ne 0 ]] \
87
+ && printf '%s' "$out" | grep -q '\[task-id-leak\] .*match=task 127'; then
88
+ echo "PASS: lowercase prose 'task 127' fails the gate"; PASS=$((PASS+1))
89
+ else
90
+ echo "FAIL: lowercase prose citation not caught (rc=$rc): $out" >&2; FAIL=$((FAIL+1))
91
+ fi
92
+ clean_skill
93
+
94
+ # 4e. A `.tasks/<n>-…-task-NNN.md` file-path reference -> gate does NOT trip
95
+ # (file paths are out of scope; the hyphenated-lowercase shape is exempt).
96
+ printf '# Demo skill\n\nTracked in `.tasks/339-installer-smoke-task-297.md` for now.\n' > "$ROOT/platform/plugins/demo/skills/demo/SKILL.md"
97
+ out=$(TASK_ID_LEAK_ROOT="$ROOT" node "$GATE" 2>&1); rc=$?
98
+ if [[ "$rc" -eq 0 ]]; then
99
+ echo "PASS: .tasks/ file-path reference is not flagged"; PASS=$((PASS+1))
100
+ else
101
+ echo "FAIL: .tasks/ path wrongly tripped gate (rc=$rc): $out" >&2; FAIL=$((FAIL+1))
102
+ fi
103
+ clean_skill
104
+
105
+ # 5. A hex colour `#999` in a shipped .md -> gate does NOT trip (not a citation).
106
+ printf '# Demo skill\n\nUse colour `#999` for labels.\n' > "$ROOT/platform/plugins/demo/skills/demo/SKILL.md"
107
+ out=$(TASK_ID_LEAK_ROOT="$ROOT" node "$GATE" 2>&1); rc=$?
108
+ if [[ "$rc" -eq 0 ]]; then
109
+ echo "PASS: hex colour #999 is not flagged as a citation"; PASS=$((PASS+1))
110
+ else
111
+ echo "FAIL: hex colour wrongly tripped gate (rc=$rc): $out" >&2; FAIL=$((FAIL+1))
112
+ fi
113
+ clean_skill
114
+
115
+ # 6. Citation in a dev-only .docs doc -> gate does NOT trip (not a shipped root).
116
+ printf 'Internal note about Task 999.\n' > "$ROOT/.docs/note.md"
117
+ out=$(TASK_ID_LEAK_ROOT="$ROOT" node "$GATE" 2>&1); rc=$?
118
+ if [[ "$rc" -eq 0 ]]; then
119
+ echo "PASS: .docs citation is exempt (dev-only, unscanned)"; PASS=$((PASS+1))
120
+ else
121
+ echo "FAIL: .docs citation wrongly tripped gate (rc=$rc): $out" >&2; FAIL=$((FAIL+1))
122
+ fi
123
+
124
+ echo "----"; echo "PASS=$PASS FAIL=$FAIL"
125
+ [[ "$FAIL" -eq 0 ]]
@@ -276,12 +276,265 @@ case_supervision_contract() {
276
276
  return 0
277
277
  }
278
278
 
279
+ # ---------------------------------------------------------------------------
280
+ # Task 833 — stray-connector reap.
281
+ #
282
+ # install_reap_stubs adds a `ps` stub and a `kill` recorder on top of setup().
283
+ # The ps stub reads a table file ($root/ps_table) of "<pid> <argv...>" lines and
284
+ # answers both query shapes the script uses: `-eo pid=,args=` (full table) and
285
+ # `-p <pid> -o args=` (single argv). The recorder ($root/bin/killrec, wired via
286
+ # RESUME_TUNNEL_KILL_CMD) appends -TERM/-KILL calls to $root/reap_log; on a `-0`
287
+ # liveness query it reports "gone" (exit 1) unless $root/reap_alive exists.
288
+ install_reap_stubs() {
289
+ local root="$1"
290
+ local bin_dir="$root/bin"
291
+
292
+ cat > "$bin_dir/ps" <<SH
293
+ #!/bin/bash
294
+ table="$root/ps_table"
295
+ if [[ "\$*" == *"-p"* ]]; then
296
+ pid=""; prev=""
297
+ for a in "\$@"; do
298
+ [[ "\$prev" == "-p" ]] && pid="\$a"
299
+ prev="\$a"
300
+ done
301
+ awk -v want="\$pid" '{ if (\$1==want){ \$1=""; sub(/^ /,""); print } }' "\$table" 2>/dev/null
302
+ exit 0
303
+ fi
304
+ cat "\$table" 2>/dev/null
305
+ exit 0
306
+ SH
307
+ chmod +x "$bin_dir/ps"
308
+
309
+ cat > "$bin_dir/killrec" <<SH
310
+ #!/bin/bash
311
+ if [[ "\$1" == "-0" ]]; then
312
+ [[ -f "$root/reap_alive" ]] && exit 0 || exit 1
313
+ fi
314
+ echo "\$*" >> "$root/reap_log"
315
+ exit 0
316
+ SH
317
+ chmod +x "$bin_dir/killrec"
318
+ }
319
+
320
+ # Write a config.yml carrying the brand tunnel id so the audit line can resolve it.
321
+ write_branded_config() {
322
+ local cfg="$1"
323
+ cat > "$cfg" <<'YML'
324
+ tunnel: test-tunnel-id
325
+ credentials-file: /dev/null
326
+ ingress:
327
+ - service: http_status:404
328
+ YML
329
+ }
330
+
331
+ # Run resume-tunnel with the reap stubs + recorder active.
332
+ # Args: root config_dir [extra env assignments...]
333
+ run_with_reap() {
334
+ local root="$1" config_dir="$2"; shift 2
335
+ # Use env so any extra "NAME=VALUE" args (e.g. RESUME_TUNNEL_REAP_GRACE=0)
336
+ # are applied as assignments — a variable-expanded NAME=VALUE word is NOT
337
+ # treated as an assignment by bash's own simple-command parsing.
338
+ env \
339
+ HOME="$root/home" \
340
+ PATH="$root/bin:$PATH" \
341
+ MAXY_PLATFORM_ROOT="$root/platform_root" \
342
+ RESUME_TUNNEL_KILL_CMD="$root/bin/killrec" \
343
+ RESUME_TUNNEL_VERIFY_DEADLINE=2 \
344
+ "$@" \
345
+ bash "$RESUME"
346
+ }
347
+
348
+ # CASE A: stray present, service inactive → reap kills stray, leaves none, then spawns.
349
+ case_reap_stray_then_spawn() {
350
+ local root; root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
351
+ setup "$root" ".maxy-code" "inactive" "yes"
352
+ install_reap_stubs "$root"
353
+ local home="$root/home"
354
+ local cert="$home/.maxy-code/cloudflared/cert.pem"
355
+ local cfg="$home/.maxy-code/cloudflared/config.yml"
356
+ write_branded_config "$cfg"
357
+ cat > "$root/ps_table" <<TBL
358
+ 210086 cloudflared --origincert $cert --config $cfg tunnel run
359
+ 777 cloudflared --origincert /home/other/.realagent/cloudflared/cert.pem --config /home/other/.realagent/cloudflared/config.yml tunnel run
360
+ TBL
361
+
362
+ run_with_reap "$root" ".maxy-code"
363
+ local log; log=$(cat "$home/.maxy-code/logs/cloudflared.log" 2>/dev/null || echo "")
364
+ local rec; rec=$(cat "$root/reap_log" 2>/dev/null || echo "")
365
+ cleanup "$root"
366
+
367
+ echo "$log" | grep -q "op=reap brand=maxy-code found=1 killed=1 survivor=none" || { echo " census wrong"; echo "$log"; return 1; }
368
+ echo "$rec" | grep -q -- "-TERM 210086" || { echo " stray not SIGTERMed"; echo "$rec"; return 1; }
369
+ echo "$rec" | grep -q "777" && { echo " co-resident 777 was signalled"; echo "$rec"; return 1; }
370
+ echo "$log" | grep -q "spawned service=cloudflared-maxy-code.service" || { echo " did not spawn after reap"; echo "$log"; return 1; }
371
+ return 0
372
+ }
373
+
374
+ # CASE B: supervised survivor + stray, service active → reap kills only stray, no spawn.
375
+ case_reap_keeps_survivor() {
376
+ local root; root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
377
+ setup "$root" ".maxy-code" "active" "yes"
378
+ install_reap_stubs "$root"
379
+ local home="$root/home"
380
+ local cert="$home/.maxy-code/cloudflared/cert.pem"
381
+ local cfg="$home/.maxy-code/cloudflared/config.yml"
382
+ write_branded_config "$cfg"
383
+ # 12345 is the MainPID the systemctl stub reports.
384
+ cat > "$root/ps_table" <<TBL
385
+ 12345 cloudflared --no-autoupdate --origincert $cert --config $cfg tunnel run
386
+ 210086 cloudflared --origincert $cert --config $cfg tunnel run
387
+ TBL
388
+
389
+ run_with_reap "$root" ".maxy-code"
390
+ local log; log=$(cat "$home/.maxy-code/logs/cloudflared.log" 2>/dev/null || echo "")
391
+ local rec; rec=$(cat "$root/reap_log" 2>/dev/null || echo "")
392
+ cleanup "$root"
393
+
394
+ echo "$log" | grep -q "op=reap brand=maxy-code found=2 killed=1 survivor=12345" || { echo " census wrong"; echo "$log"; return 1; }
395
+ echo "$rec" | grep -q -- "-TERM 210086" || { echo " stray not SIGTERMed"; echo "$rec"; return 1; }
396
+ echo "$rec" | grep -q "12345" && { echo " survivor 12345 was signalled"; echo "$rec"; return 1; }
397
+ echo "$log" | grep -q "spawned service=" && { echo " spawned while already active"; echo "$log"; return 1; }
398
+ echo "$log" | grep -q "already running" || { echo " missing already-running log"; echo "$log"; return 1; }
399
+ return 0
400
+ }
401
+
402
+ # CASE C: no stray, service active → idempotent, no kill, no spawn.
403
+ case_reap_idempotent() {
404
+ local root; root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
405
+ setup "$root" ".maxy-code" "active" "yes"
406
+ install_reap_stubs "$root"
407
+ local home="$root/home"
408
+ local cert="$home/.maxy-code/cloudflared/cert.pem"
409
+ local cfg="$home/.maxy-code/cloudflared/config.yml"
410
+ write_branded_config "$cfg"
411
+ cat > "$root/ps_table" <<TBL
412
+ 12345 cloudflared --no-autoupdate --origincert $cert --config $cfg tunnel run
413
+ TBL
414
+
415
+ run_with_reap "$root" ".maxy-code"
416
+ local log; log=$(cat "$home/.maxy-code/logs/cloudflared.log" 2>/dev/null || echo "")
417
+ local rec; rec=$(cat "$root/reap_log" 2>/dev/null || echo "")
418
+ cleanup "$root"
419
+
420
+ echo "$log" | grep -q "op=reap brand=maxy-code found=1 killed=0 survivor=12345" || { echo " census wrong"; echo "$log"; return 1; }
421
+ [ -z "$rec" ] || { echo " recorder not empty"; echo "$rec"; return 1; }
422
+ echo "$log" | grep -q "spawned service=" && { echo " spawned while single+active"; echo "$log"; return 1; }
423
+ return 0
424
+ }
425
+
426
+ # CASE D: audit line resolves the brand tunnel id from the victim's --config.
427
+ case_reap_audit_tunnel_id() {
428
+ local root; root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
429
+ setup "$root" ".maxy-code" "inactive" "yes"
430
+ install_reap_stubs "$root"
431
+ local home="$root/home"
432
+ local cert="$home/.maxy-code/cloudflared/cert.pem"
433
+ local cfg="$home/.maxy-code/cloudflared/config.yml"
434
+ write_branded_config "$cfg"
435
+ cat > "$root/ps_table" <<TBL
436
+ 210086 cloudflared --origincert $cert --config $cfg tunnel run
437
+ TBL
438
+
439
+ run_with_reap "$root" ".maxy-code"
440
+ local log; log=$(cat "$home/.maxy-code/logs/cloudflared.log" 2>/dev/null || echo "")
441
+ cleanup "$root"
442
+
443
+ echo "$log" | grep -q "op=reap brand=maxy-code target=210086 tunnel=test-tunnel-id" || { echo " audit line missing brand tunnel id"; echo "$log"; return 1; }
444
+ return 0
445
+ }
446
+
447
+ # CASE E: only a foreign (co-resident) connector present → untouched, brand still spawns.
448
+ case_reap_foreign_untouched() {
449
+ local root; root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
450
+ setup "$root" ".maxy-code" "inactive" "yes"
451
+ install_reap_stubs "$root"
452
+ local home="$root/home"
453
+ write_branded_config "$home/.maxy-code/cloudflared/config.yml"
454
+ cat > "$root/ps_table" <<TBL
455
+ 777 cloudflared --origincert /home/other/.realagent/cloudflared/cert.pem --config /home/other/.realagent/cloudflared/config.yml tunnel run
456
+ TBL
457
+
458
+ run_with_reap "$root" ".maxy-code"
459
+ local log; log=$(cat "$home/.maxy-code/logs/cloudflared.log" 2>/dev/null || echo "")
460
+ local rec; rec=$(cat "$root/reap_log" 2>/dev/null || echo "")
461
+ cleanup "$root"
462
+
463
+ echo "$log" | grep -q "op=reap brand=maxy-code found=0 killed=0 survivor=none" || { echo " census wrong"; echo "$log"; return 1; }
464
+ [ -z "$rec" ] || { echo " foreign connector signalled"; echo "$rec"; return 1; }
465
+ echo "$log" | grep -q "spawned service=cloudflared-maxy-code.service" || { echo " did not spawn own connector"; echo "$log"; return 1; }
466
+ return 0
467
+ }
468
+
469
+ # CASE G: service active but MainPID unresolved → must NOT reap (can't tell the
470
+ # live supervised connector from a stray), must skip without spawning.
471
+ case_reap_skip_when_mainpid_unresolved() {
472
+ local root; root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
473
+ setup "$root" ".maxy-code" "active" "yes"
474
+ install_reap_stubs "$root"
475
+ local home="$root/home"
476
+ local cert="$home/.maxy-code/cloudflared/cert.pem"
477
+ local cfg="$home/.maxy-code/cloudflared/config.yml"
478
+ write_branded_config "$cfg"
479
+ # Override systemctl so MainPID resolves empty while is-active stays active
480
+ # (the RestartSec transition window).
481
+ cat > "$root/bin/systemctl" <<'SH'
482
+ #!/bin/bash
483
+ if [[ "$*" == *"is-active"* ]]; then echo "active"; exit 0; fi
484
+ if [[ "$*" == *"--property=MainPID"* ]]; then echo ""; fi
485
+ exit 0
486
+ SH
487
+ chmod +x "$root/bin/systemctl"
488
+ cat > "$root/ps_table" <<TBL
489
+ 210086 cloudflared --no-autoupdate --origincert $cert --config $cfg tunnel run
490
+ TBL
491
+
492
+ run_with_reap "$root" ".maxy-code"
493
+ local log; log=$(cat "$home/.maxy-code/logs/cloudflared.log" 2>/dev/null || echo "")
494
+ local rec; rec=$(cat "$root/reap_log" 2>/dev/null || echo "")
495
+ cleanup "$root"
496
+
497
+ [ -z "$rec" ] || { echo " reaped while survivor unknown — would kill the live connector"; echo "$rec"; return 1; }
498
+ echo "$log" | grep -q "skipped reason=mainpid-unresolved" || { echo " missing mainpid-unresolved skip log"; echo "$log"; return 1; }
499
+ echo "$log" | grep -q "spawned service=" && { echo " spawned while active"; echo "$log"; return 1; }
500
+ return 0
501
+ }
502
+
503
+ # CASE F: stray survives SIGTERM → escalates to SIGKILL.
504
+ case_reap_sigkill_escalation() {
505
+ local root; root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
506
+ setup "$root" ".maxy-code" "inactive" "yes"
507
+ install_reap_stubs "$root"
508
+ local home="$root/home"
509
+ local cert="$home/.maxy-code/cloudflared/cert.pem"
510
+ local cfg="$home/.maxy-code/cloudflared/config.yml"
511
+ write_branded_config "$cfg"
512
+ touch "$root/reap_alive" # recorder reports the proc still alive on -0
513
+ cat > "$root/ps_table" <<TBL
514
+ 210086 cloudflared --origincert $cert --config $cfg tunnel run
515
+ TBL
516
+
517
+ run_with_reap "$root" ".maxy-code" RESUME_TUNNEL_REAP_GRACE=0
518
+ local rec; rec=$(cat "$root/reap_log" 2>/dev/null || echo "")
519
+ cleanup "$root"
520
+
521
+ echo "$rec" | grep -q -- "-KILL 210086" || { echo " no SIGKILL escalation"; echo "$rec"; return 1; }
522
+ return 0
523
+ }
524
+
279
525
  run_case "skip when service active" case_skip_when_scope_active
280
526
  run_case "spawn succeeds and logs verified" case_spawn_succeeds
281
527
  run_case "spawn with no connection logs ERROR exits 0" case_spawn_no_connection
282
528
  run_case "service unit name derived from configDir" case_scope_unit_name_from_config_dir
283
529
  run_case "slice flag passed to systemd-run" case_slice_flag_present
284
530
  run_case "supervision contract: restart-always + no-autoupdate + .service" case_supervision_contract
531
+ run_case "reap: stray reaped then spawn (inactive)" case_reap_stray_then_spawn
532
+ run_case "reap: keeps supervised survivor, no spawn (active)" case_reap_keeps_survivor
533
+ run_case "reap: idempotent when single+active" case_reap_idempotent
534
+ run_case "reap: audit line names brand tunnel id" case_reap_audit_tunnel_id
535
+ run_case "reap: foreign co-resident connector untouched" case_reap_foreign_untouched
536
+ run_case "reap: SIGTERM survivor escalates to SIGKILL" case_reap_sigkill_escalation
537
+ run_case "reap: skip when active unit's MainPID is unresolved" case_reap_skip_when_mainpid_unresolved
285
538
 
286
539
  echo ""
287
540
  echo "Results: $PASS passed, $FAIL failed"
@@ -21,7 +21,11 @@ import { dirname, join, relative, sep, basename } from 'node:path'
21
21
  import { fileURLToPath } from 'node:url'
22
22
 
23
23
  const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
24
- const REPO_ROOT = join(SCRIPT_DIR, '..', '..')
24
+ // REPO_ROOT is overridable so the `.test.sh` self-test can point the gate at a
25
+ // fixture tree without dropping citation-bearing files into the real corpus.
26
+ const REPO_ROOT = process.env.TASK_ID_LEAK_ROOT
27
+ ? process.env.TASK_ID_LEAK_ROOT
28
+ : join(SCRIPT_DIR, '..', '..')
25
29
 
26
30
  const SCAN_ROOTS = [
27
31
  join(REPO_ROOT, 'platform', 'scripts'),
@@ -37,6 +41,26 @@ const SKIP_DIR_NAMES = new Set([
37
41
  ])
38
42
  const TASK_ID_RE = /\(Task \d+\)/
39
43
 
44
+ // Markdown mode. Unlike code files — where a citation is legal in a comment and
45
+ // only the parenthetical operator-visible form is rejected — a shipped `.md`
46
+ // loads into a client-facing agent's context wholesale, so ANY citation form
47
+ // leaks. Catches `Task NNN`, `(Task NNN)`, `Tasks NNN`, `since Task NNN`. It
48
+ // deliberately does NOT match bare `#NNN`: every `#NNN` in the corpus is a hex
49
+ // colour and there is no signal to separate `#732`-issue from `#732`-hex.
50
+ // Two alternatives:
51
+ // `\bTasks?[\s-]+\d{2,4}` — capital-T, any separator: `Task 732`, `(Task 732)`,
52
+ // `Tasks 12`, the hyphenated `Pre-Task-748` / `post-Task-904` forms.
53
+ // `\btasks?\s+\d{2,4}` — lowercase but SPACE-separated only: catches prose
54
+ // citations like "before task 127", while a lowercase HYPHENATED `task-537`
55
+ // stays exempt because that shape is a branch slug or a `.tasks/<n>-…-task-NNN.md`
56
+ // file-path reference (file paths are out of this gate's scope). The 2-digit
57
+ // floor keeps single-digit domain counts ("task 1 of 3") out.
58
+ const MD_TASK_ID_RE = /\bTasks?[\s-]+\d{2,4}|\btasks?\s+\d{2,4}/
59
+ const MD_SCAN_ROOTS = [
60
+ join(REPO_ROOT, 'platform', 'plugins'),
61
+ join(REPO_ROOT, 'platform', 'templates'),
62
+ ]
63
+
40
64
  function isCommentLine(line) {
41
65
  const trimmed = line.replace(/^\s+/, '')
42
66
  if (trimmed.startsWith('#')) return true
@@ -97,14 +121,38 @@ for (const root of SCAN_ROOTS) {
97
121
  }
98
122
  }
99
123
 
124
+ // Markdown scan — shipped operator-loadable docs. Reuses `walk` (which already
125
+ // skips node_modules/dist/build/.next/payload/__tests__) and matches every line
126
+ // against MD_TASK_ID_RE with no comment skip: the whole doc is agent content.
127
+ // .docs/ and .tasks/ are not under MD_SCAN_ROOTS, so they stay citation-rich.
128
+ for (const root of MD_SCAN_ROOTS) {
129
+ for (const file of walk(root)) {
130
+ if (!file.endsWith('.md')) continue
131
+ let content
132
+ try {
133
+ content = readFileSync(file, 'utf-8')
134
+ } catch {
135
+ continue
136
+ }
137
+ const lines = content.split('\n')
138
+ for (let i = 0; i < lines.length; i++) {
139
+ const m = lines[i].match(MD_TASK_ID_RE)
140
+ if (!m) continue
141
+ leaks.push({ file: relative(REPO_ROOT, file), line: i + 1, content: m[0] })
142
+ }
143
+ }
144
+ }
145
+
100
146
  if (leaks.length > 0) {
101
- console.error(`check-no-task-id-leaks: ${leaks.length} operator-visible (Task NNN) citation(s) found:`)
147
+ console.error(`check-no-task-id-leaks: ${leaks.length} task-id citation(s) found:`)
102
148
  for (const leak of leaks) {
103
- console.error(` ${leak.file}:${leak.line}: ${leak.content}`)
149
+ console.error(`[task-id-leak] file=${leak.file} line=${leak.line} match=${leak.content}`)
104
150
  }
105
151
  console.error('')
106
- console.error('Citations belong in inline comments only, never in operator-visible strings')
107
- console.error('(stderr/stdout, log files surfaced via logs-read.sh, systemd unit')
108
- console.error('--description=, error messages, console.log/warn/error, throw new Error).')
152
+ console.error('Code files: citations belong in inline comments only, never in operator-visible')
153
+ console.error('strings (stderr/stdout, logs, systemd --description=, error messages, console.*).')
154
+ console.error('Shipped markdown (platform/plugins, platform/templates): no task citation in any')
155
+ console.error('form — the whole doc loads into a client-facing agent. Dev docs (.docs/, .tasks/)')
156
+ console.error('are exempt and keep their citations.')
109
157
  process.exit(1)
110
158
  }