@rubytech/create-maxy 1.0.880 → 1.0.883

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 (60) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.js +57 -9
  3. package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.js.map +1 -1
  4. package/payload/platform/lib/graph-write/dist/conversation-provenance.d.ts +26 -0
  5. package/payload/platform/lib/graph-write/dist/conversation-provenance.d.ts.map +1 -0
  6. package/payload/platform/lib/graph-write/dist/conversation-provenance.js +81 -0
  7. package/payload/platform/lib/graph-write/dist/conversation-provenance.js.map +1 -0
  8. package/payload/platform/lib/graph-write/dist/index.d.ts +38 -16
  9. package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
  10. package/payload/platform/lib/graph-write/dist/index.js +75 -35
  11. package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
  12. package/payload/platform/lib/graph-write/src/__tests__/action-provenance-gate.test.ts +59 -9
  13. package/payload/platform/lib/graph-write/src/conversation-provenance.ts +140 -0
  14. package/payload/platform/lib/graph-write/src/index.ts +76 -35
  15. package/payload/platform/plugins/admin/PLUGIN.md +1 -0
  16. package/payload/platform/plugins/admin/mcp/dist/__tests__/plugin-read-skill-resolution.test.js +26 -2
  17. package/payload/platform/plugins/admin/mcp/dist/__tests__/plugin-read-skill-resolution.test.js.map +1 -1
  18. package/payload/platform/plugins/admin/mcp/dist/index.js +8 -6
  19. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  20. package/payload/platform/plugins/admin/mcp/dist/skill-resolution.d.ts +5 -1
  21. package/payload/platform/plugins/admin/mcp/dist/skill-resolution.d.ts.map +1 -1
  22. package/payload/platform/plugins/admin/mcp/dist/skill-resolution.js +24 -8
  23. package/payload/platform/plugins/admin/mcp/dist/skill-resolution.js.map +1 -1
  24. package/payload/platform/plugins/admin/skills/plainly/SKILL.md +105 -0
  25. package/payload/platform/plugins/admin/skills/plainly/references/worked-examples.md +301 -0
  26. package/payload/platform/plugins/cloudflare/PLUGIN.md +1 -1
  27. package/payload/platform/plugins/contacts/PLUGIN.md +8 -0
  28. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.d.ts.map +1 -1
  29. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js +17 -2
  30. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js.map +1 -1
  31. package/payload/platform/plugins/docs/references/internals.md +2 -2
  32. package/payload/platform/plugins/docs/references/platform.md +5 -1
  33. package/payload/platform/plugins/docs/references/plugins-guide.md +3 -1
  34. package/payload/platform/plugins/memory/PLUGIN.md +5 -3
  35. package/payload/platform/plugins/memory/mcp/dist/index.js +2 -2
  36. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  37. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
  38. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +18 -1
  39. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
  40. package/payload/platform/scripts/__tests__/logs-read-prefix.sh +341 -0
  41. package/payload/platform/scripts/logs-read.sh +108 -41
  42. package/payload/platform/scripts/logs-read.test.sh +6 -2
  43. package/payload/platform/templates/agents/admin/IDENTITY.md +6 -2
  44. package/payload/platform/templates/agents/public/IDENTITY.md +6 -0
  45. package/payload/platform/templates/specialists/agents/content-producer.md +6 -0
  46. package/payload/platform/templates/specialists/agents/database-operator.md +6 -0
  47. package/payload/platform/templates/specialists/agents/personal-assistant.md +6 -0
  48. package/payload/platform/templates/specialists/agents/project-manager.md +6 -0
  49. package/payload/platform/templates/specialists/agents/research-assistant.md +6 -0
  50. package/payload/premium-plugins/real-agency/BUNDLE.md +1 -1
  51. package/payload/server/chunk-ECAQVMRA.js +759 -0
  52. package/payload/server/chunk-K7S5T4VG.js +11534 -0
  53. package/payload/server/chunk-KMVUUVHM.js +11438 -0
  54. package/payload/server/chunk-LVC7NKZ2.js +689 -0
  55. package/payload/server/cloudflare-task-tracker-CY6QL6CY.js +22 -0
  56. package/payload/server/cloudflare-task-tracker-JNZXLW32.js +22 -0
  57. package/payload/server/maxy-edge.js +2 -1
  58. package/payload/server/public/assets/{admin-CCEuBnaK.js → admin-BN_z-2Bm.js} +2 -2
  59. package/payload/server/public/index.html +1 -1
  60. package/payload/server/server.js +115 -8
@@ -0,0 +1,341 @@
1
+ #!/usr/bin/env bash
2
+ # Task 998 — bash harness for logs-read.sh prefix-match resolution.
3
+ #
4
+ # Covers the contract from the task brief:
5
+ # 1. 8-char prefix → resolves full-UUID file (the original bug)
6
+ # 2. Multiple matches → reason=ambiguous-prefix, exit 1, never picks
7
+ # 3. More-specific prefix disambiguates
8
+ # 4. Zero matches → reason=file-not-found-in-either-shape with glob patterns
9
+ # 5. Full 36-char UUID still resolves (regression boundary)
10
+ # 6. Preflush prefix match
11
+ # 7. Preflush ambiguity (Pass 1 zero, Pass 2 multiple)
12
+ # 8. Every per-conversation type (agent-stream, session, error, public)
13
+ # resolves under prefix-match
14
+ #
15
+ # Companion to platform/scripts/logs-read.test.sh (Task 671's two-shape
16
+ # resolution harness).
17
+ set -euo pipefail
18
+
19
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
+ LOGS_READ="$SCRIPT_DIR/../logs-read.sh"
21
+ if [[ ! -x "$LOGS_READ" ]]; then
22
+ echo "FATAL: $LOGS_READ not executable" >&2
23
+ exit 1
24
+ fi
25
+
26
+ PASS=0
27
+ FAIL=0
28
+
29
+ # logs-read.sh resolves PLATFORM_ROOT from its own location; mirror the
30
+ # logs-read.test.sh convention by symlinking the script into an ephemeral
31
+ # install tree per case.
32
+ setup_install_tree() {
33
+ local root="$1"
34
+ mkdir -p "$root/platform/scripts"
35
+ mkdir -p "$root/data/accounts/acct-test/logs"
36
+ mkdir -p "$HOME/.$(basename "$root")/logs"
37
+ ln -sf "$LOGS_READ" "$root/platform/scripts/logs-read.sh"
38
+ echo "$root/platform/scripts/logs-read.sh"
39
+ }
40
+
41
+ cleanup_install_tree() {
42
+ local root="$1"
43
+ rm -rf "$root"
44
+ rm -rf "$HOME/.$(basename "$root")"
45
+ }
46
+
47
+ run_case() {
48
+ local name="$1"
49
+ shift
50
+ echo ""
51
+ echo "=== CASE: $name ==="
52
+ if "$@"; then
53
+ PASS=$((PASS + 1))
54
+ echo "PASS: $name"
55
+ else
56
+ FAIL=$((FAIL + 1))
57
+ echo "FAIL: $name"
58
+ fi
59
+ }
60
+
61
+ # --- Case 1: 8-char prefix resolves a full-UUID file (original bug) ---
62
+ case_prefix_resolves_full() {
63
+ local root="/tmp/maxy-logs-read-prefix-1-$$"
64
+ local script
65
+ script=$(setup_install_tree "$root")
66
+ local logdir="$root/data/accounts/acct-test/logs"
67
+ local full_uuid="3483269d-c793-4a07-98cc-556d936f2f4d"
68
+ local filename="claude-agent-stream-${full_uuid}.log"
69
+ echo "prefix-match-sentinel" > "$logdir/$filename"
70
+
71
+ local stdout stderr rc=0
72
+ stdout=$("$script" "3483269d" system 2>/tmp/logs-read-prefix-stderr-1-$$) || rc=$?
73
+ stderr=$(cat /tmp/logs-read-prefix-stderr-1-$$)
74
+ rm -f /tmp/logs-read-prefix-stderr-1-$$
75
+
76
+ cleanup_install_tree "$root"
77
+
78
+ if [[ $rc -ne 0 ]]; then echo " expected exit 0, got $rc"; return 1; fi
79
+ if [[ "$stdout" != *"prefix-match-sentinel"* ]]; then
80
+ echo " stdout missing sentinel: $stdout"; return 1
81
+ fi
82
+ if [[ "$stderr" != *"matched_shape=full"* ]]; then
83
+ echo " stderr missing matched_shape=full: $stderr"; return 1
84
+ fi
85
+ if [[ "$stdout" != *"$filename"* ]]; then
86
+ echo " header missing matched filename: $stdout"; return 1
87
+ fi
88
+ return 0
89
+ }
90
+
91
+ # --- Case 2: Multiple matches on Pass 1 → ambiguous-prefix, exit 1 ---
92
+ case_ambiguous_prefix() {
93
+ local root="/tmp/maxy-logs-read-prefix-2-$$"
94
+ local script
95
+ script=$(setup_install_tree "$root")
96
+ local logdir="$root/data/accounts/acct-test/logs"
97
+ echo "A" > "$logdir/claude-agent-stream-aaaaaaaa-1111-A.log"
98
+ echo "B" > "$logdir/claude-agent-stream-aaaaaaaa-2222-B.log"
99
+
100
+ local stdout stderr rc=0
101
+ stdout=$("$script" "aaaaaaaa" system 2>/tmp/logs-read-prefix-stderr-2-$$) || rc=$?
102
+ stderr=$(cat /tmp/logs-read-prefix-stderr-2-$$)
103
+ rm -f /tmp/logs-read-prefix-stderr-2-$$
104
+
105
+ cleanup_install_tree "$root"
106
+
107
+ if [[ $rc -ne 1 ]]; then echo " expected exit 1, got $rc"; return 1; fi
108
+ if [[ "$stderr" != *"reason=ambiguous-prefix"* ]]; then
109
+ echo " stderr missing reason=ambiguous-prefix: $stderr"; return 1
110
+ fi
111
+ if [[ "$stderr" != *"matches=2"* ]]; then
112
+ echo " stderr missing matches=2: $stderr"; return 1
113
+ fi
114
+ if [[ "$stderr" != *"claude-agent-stream-aaaaaaaa-1111-A.log"* ]]; then
115
+ echo " stderr missing candidate A: $stderr"; return 1
116
+ fi
117
+ if [[ "$stderr" != *"claude-agent-stream-aaaaaaaa-2222-B.log"* ]]; then
118
+ echo " stderr missing candidate B: $stderr"; return 1
119
+ fi
120
+ # Body must NOT contain either sentinel — refusal means no body emitted.
121
+ if [[ -n "$stdout" ]]; then
122
+ echo " stdout non-empty on ambiguous refusal: $stdout"; return 1
123
+ fi
124
+ return 0
125
+ }
126
+
127
+ # --- Case 3: more-specific prefix disambiguates ---
128
+ case_more_specific_disambiguates() {
129
+ local root="/tmp/maxy-logs-read-prefix-3-$$"
130
+ local script
131
+ script=$(setup_install_tree "$root")
132
+ local logdir="$root/data/accounts/acct-test/logs"
133
+ echo "ONE" > "$logdir/claude-agent-stream-aaaaaaaa-1111-A.log"
134
+ echo "TWO" > "$logdir/claude-agent-stream-aaaaaaaa-2222-B.log"
135
+
136
+ local stdout stderr rc=0
137
+ stdout=$("$script" "aaaaaaaa-1111" system 2>/tmp/logs-read-prefix-stderr-3-$$) || rc=$?
138
+ stderr=$(cat /tmp/logs-read-prefix-stderr-3-$$)
139
+ rm -f /tmp/logs-read-prefix-stderr-3-$$
140
+
141
+ cleanup_install_tree "$root"
142
+
143
+ if [[ $rc -ne 0 ]]; then echo " expected exit 0, got $rc"; return 1; fi
144
+ if [[ "$stdout" != *"ONE"* ]]; then echo " stdout missing ONE: $stdout"; return 1; fi
145
+ if [[ "$stdout" == *"TWO"* ]]; then echo " stdout contained TWO: $stdout"; return 1; fi
146
+ if [[ "$stderr" != *"matched_shape=full"* ]]; then
147
+ echo " stderr missing matched_shape=full: $stderr"; return 1
148
+ fi
149
+ return 0
150
+ }
151
+
152
+ # --- Case 4: zero matches → tried=[<glob>, <glob>] file-not-found ---
153
+ case_zero_matches() {
154
+ local root="/tmp/maxy-logs-read-prefix-4-$$"
155
+ local script
156
+ script=$(setup_install_tree "$root")
157
+
158
+ local stdout stderr rc=0
159
+ stdout=$("$script" "xxxxxxxx" system 2>/tmp/logs-read-prefix-stderr-4-$$) || rc=$?
160
+ stderr=$(cat /tmp/logs-read-prefix-stderr-4-$$)
161
+ rm -f /tmp/logs-read-prefix-stderr-4-$$
162
+
163
+ cleanup_install_tree "$root"
164
+
165
+ if [[ $rc -ne 1 ]]; then echo " expected exit 1, got $rc"; return 1; fi
166
+ if [[ "$stderr" != *"reason=file-not-found-in-either-shape"* ]]; then
167
+ echo " stderr missing reason=file-not-found-in-either-shape: $stderr"; return 1
168
+ fi
169
+ if [[ "$stderr" != *"tried=[claude-agent-stream-xxxxxxxx*.log, claude-agent-stream-preflush-xxxxxxxx*.log]"* ]]; then
170
+ echo " stderr missing tried=[<glob>, <glob>]: $stderr"; return 1
171
+ fi
172
+ return 0
173
+ }
174
+
175
+ # --- Case 5: full 36-char UUID still resolves (regression boundary) ---
176
+ case_full_uuid_regression() {
177
+ local root="/tmp/maxy-logs-read-prefix-5-$$"
178
+ local script
179
+ script=$(setup_install_tree "$root")
180
+ local logdir="$root/data/accounts/acct-test/logs"
181
+ local full_uuid="3483269d-c793-4a07-98cc-556d936f2f4d"
182
+ local filename="claude-agent-stream-${full_uuid}.log"
183
+ echo "regression-sentinel" > "$logdir/$filename"
184
+
185
+ local stdout stderr rc=0
186
+ stdout=$("$script" "$full_uuid" system 2>/tmp/logs-read-prefix-stderr-5-$$) || rc=$?
187
+ stderr=$(cat /tmp/logs-read-prefix-stderr-5-$$)
188
+ rm -f /tmp/logs-read-prefix-stderr-5-$$
189
+
190
+ cleanup_install_tree "$root"
191
+
192
+ if [[ $rc -ne 0 ]]; then echo " expected exit 0, got $rc"; return 1; fi
193
+ if [[ "$stdout" != *"regression-sentinel"* ]]; then
194
+ echo " stdout missing sentinel: $stdout"; return 1
195
+ fi
196
+ if [[ "$stderr" != *"matched_shape=full"* ]]; then
197
+ echo " stderr missing matched_shape=full: $stderr"; return 1
198
+ fi
199
+ return 0
200
+ }
201
+
202
+ # --- Case 6: preflush-only file, prefix-match (Pass 2) ---
203
+ case_preflush_prefix_match() {
204
+ local root="/tmp/maxy-logs-read-prefix-6-$$"
205
+ local script
206
+ script=$(setup_install_tree "$root")
207
+ local logdir="$root/data/accounts/acct-test/logs"
208
+ # Preflush filename uses sessionKey:0:12; mimic with a 12-char slice.
209
+ echo "preflush-prefix-sentinel" > "$logdir/claude-agent-stream-preflush-bbbbbbbb-ccc.log"
210
+
211
+ local stdout stderr rc=0
212
+ stdout=$("$script" "bbbbbbbb" system 2>/tmp/logs-read-prefix-stderr-6-$$) || rc=$?
213
+ stderr=$(cat /tmp/logs-read-prefix-stderr-6-$$)
214
+ rm -f /tmp/logs-read-prefix-stderr-6-$$
215
+
216
+ cleanup_install_tree "$root"
217
+
218
+ if [[ $rc -ne 0 ]]; then echo " expected exit 0, got $rc"; return 1; fi
219
+ if [[ "$stdout" != *"preflush-prefix-sentinel"* ]]; then
220
+ echo " stdout missing sentinel: $stdout"; return 1
221
+ fi
222
+ if [[ "$stderr" != *"matched_shape=preflush"* ]]; then
223
+ echo " stderr missing matched_shape=preflush: $stderr"; return 1
224
+ fi
225
+ return 0
226
+ }
227
+
228
+ # --- Case 7: preflush ambiguity (Pass 1 zero, Pass 2 multiple) ---
229
+ case_preflush_ambiguity() {
230
+ local root="/tmp/maxy-logs-read-prefix-7-$$"
231
+ local script
232
+ script=$(setup_install_tree "$root")
233
+ local logdir="$root/data/accounts/acct-test/logs"
234
+ echo "P1" > "$logdir/claude-agent-stream-preflush-cccccccc-111.log"
235
+ echo "P2" > "$logdir/claude-agent-stream-preflush-cccccccc-222.log"
236
+
237
+ local stdout stderr rc=0
238
+ stdout=$("$script" "cccccccc" system 2>/tmp/logs-read-prefix-stderr-7-$$) || rc=$?
239
+ stderr=$(cat /tmp/logs-read-prefix-stderr-7-$$)
240
+ rm -f /tmp/logs-read-prefix-stderr-7-$$
241
+
242
+ cleanup_install_tree "$root"
243
+
244
+ if [[ $rc -ne 1 ]]; then echo " expected exit 1, got $rc"; return 1; fi
245
+ if [[ "$stderr" != *"reason=ambiguous-prefix"* ]]; then
246
+ echo " stderr missing reason=ambiguous-prefix: $stderr"; return 1
247
+ fi
248
+ if [[ "$stderr" != *"matches=2"* ]]; then
249
+ echo " stderr missing matches=2: $stderr"; return 1
250
+ fi
251
+ if [[ "$stderr" != *"claude-agent-stream-preflush-cccccccc-111.log"* ]]; then
252
+ echo " stderr missing preflush candidate 1: $stderr"; return 1
253
+ fi
254
+ if [[ "$stderr" != *"claude-agent-stream-preflush-cccccccc-222.log"* ]]; then
255
+ echo " stderr missing preflush candidate 2: $stderr"; return 1
256
+ fi
257
+ return 0
258
+ }
259
+
260
+ # --- Case 8: every per-conversation type resolves under prefix-match ---
261
+ # Parameterised over the four prefix_for_type values:
262
+ # agent-stream → claude-agent-stream-
263
+ # session → sse-events-
264
+ # error → claude-agent-stderr-
265
+ # public → public-agent-stream-
266
+ case_all_types_prefix_match() {
267
+ local root="/tmp/maxy-logs-read-prefix-8-$$"
268
+ local script
269
+ script=$(setup_install_tree "$root")
270
+ local logdir="$root/data/accounts/acct-test/logs"
271
+ local full_uuid="7d49ef21-1234-4567-89ab-cdef01234567"
272
+
273
+ echo "sentinel-agent-stream" > "$logdir/claude-agent-stream-${full_uuid}.log"
274
+ echo "sentinel-session" > "$logdir/sse-events-${full_uuid}.log"
275
+ echo "sentinel-error" > "$logdir/claude-agent-stderr-${full_uuid}.log"
276
+ echo "sentinel-public" > "$logdir/public-agent-stream-${full_uuid}.log"
277
+
278
+ local pairs=(
279
+ "agent-stream|sentinel-agent-stream"
280
+ "session|sentinel-session"
281
+ "error|sentinel-error"
282
+ "public|sentinel-public"
283
+ )
284
+ local fail=0
285
+ local pair t expected_sentinel
286
+ for pair in "${pairs[@]}"; do
287
+ t="${pair%%|*}"
288
+ expected_sentinel="${pair##*|}"
289
+ local stdout stderr rc=0
290
+ stdout=$("$script" "7d49ef21" "$t" 2>/tmp/logs-read-prefix-stderr-8-$$) || rc=$?
291
+ stderr=$(cat /tmp/logs-read-prefix-stderr-8-$$)
292
+ rm -f /tmp/logs-read-prefix-stderr-8-$$
293
+ if [[ $rc -ne 0 ]]; then echo " [$t] expected exit 0, got $rc — stderr: $stderr"; fail=1; continue; fi
294
+ if [[ "$stdout" != *"$expected_sentinel"* ]]; then
295
+ echo " [$t] missing sentinel '$expected_sentinel': $stdout"; fail=1
296
+ fi
297
+ if [[ "$stderr" != *"matched_shape=full"* ]]; then
298
+ echo " [$t] stderr missing matched_shape=full: $stderr"; fail=1
299
+ fi
300
+ done
301
+
302
+ cleanup_install_tree "$root"
303
+ [[ $fail -eq 0 ]]
304
+ }
305
+
306
+ # --- Case 9: conv_id with shell metacharacters is rejected ---
307
+ case_metacharacter_rejected() {
308
+ local root="/tmp/maxy-logs-read-prefix-9-$$"
309
+ local script
310
+ script=$(setup_install_tree "$root")
311
+
312
+ local stdout stderr rc=0
313
+ # Use a backslash-escaped wildcard to feed it as a literal argument.
314
+ stdout=$("$script" "ab*cd" system 2>/tmp/logs-read-prefix-stderr-9-$$) || rc=$?
315
+ stderr=$(cat /tmp/logs-read-prefix-stderr-9-$$)
316
+ rm -f /tmp/logs-read-prefix-stderr-9-$$
317
+
318
+ cleanup_install_tree "$root"
319
+
320
+ if [[ $rc -ne 2 ]]; then echo " expected exit 2 (usage), got $rc"; return 1; fi
321
+ if [[ "$stderr" != *"invalid characters"* ]]; then
322
+ echo " stderr missing invalid-characters guard: $stderr"; return 1
323
+ fi
324
+ return 0
325
+ }
326
+
327
+ run_case "8-char prefix resolves full-UUID file" case_prefix_resolves_full
328
+ run_case "ambiguous prefix → refused, candidates listed" case_ambiguous_prefix
329
+ run_case "more-specific prefix disambiguates" case_more_specific_disambiguates
330
+ run_case "zero matches → file-not-found-in-either-shape" case_zero_matches
331
+ run_case "full 36-char UUID still resolves" case_full_uuid_regression
332
+ run_case "preflush file resolves by prefix" case_preflush_prefix_match
333
+ run_case "preflush ambiguity refused" case_preflush_ambiguity
334
+ run_case "every per-conversation type prefix-matches" case_all_types_prefix_match
335
+ run_case "shell metacharacters rejected" case_metacharacter_rejected
336
+
337
+ echo ""
338
+ echo "================================================"
339
+ echo " Passed: $PASS / Failed: $FAIL"
340
+ echo "================================================"
341
+ [[ $FAIL -eq 0 ]]
@@ -144,25 +144,42 @@ validate_type() {
144
144
  esac
145
145
  }
146
146
 
147
- # --- Per-conversation mode: read {prefix}{convId}.log, with preflush fallback ---
147
+ # --- Per-conversation mode: prefix-match {prefix}{convId-prefix}*.log ---
148
148
  #
149
- # Task 671: writer (Task 650) emits logs under two filename shapes
149
+ # Task 671 two filename shapes the writer emits:
150
150
  # FULL: {prefix}{conversationId}.log (post 1→2-user-turn flush)
151
151
  # PREFLUSH: {prefix}preflush-{id:0:12}.log (pre-flush, first turn; the
152
152
  # slice is of the sessionKey)
153
- # This mode tries FULL across every account dir first; if zero FULL hits,
154
- # falls back to PREFLUSH across every dir. If BOTH exist in the same dir,
155
- # FULL wins and the preflush sibling is logged to server.log as stale
156
- # (promotion-race housekeeping signal).
157
153
  #
158
- # Identity note: for the abrupt-exit case this fallback exists to solve,
154
+ # Task 998 operator surface accepts any unambiguous prefix of the id.
155
+ # The writer's filename is canonical (full 36-char UUID) but every agent
156
+ # line surfaces the 8-char id (`conversationId=3483269d…`). Pass 1 and
157
+ # Pass 2 each glob `${prefix}${conv_id}*.log` so an 8-char operator input
158
+ # resolves a full-UUID file. Three trailer outcomes:
159
+ # single hit → exit 0 + matched_shape=full|preflush (existing success)
160
+ # zero hits → exit 1 + reason=file-not-found-in-either-shape (existing miss)
161
+ # 2+ hits → exit 1 + reason=ambiguous-prefix candidates=[...]; never picks
162
+ #
163
+ # Stale-preflush housekeeping (Task 671): on a single FULL hit, glob the
164
+ # preflush pattern in the same dir and emit `[logs-read] stale-preflush-detected`
165
+ # to server.log for each sibling. Best-effort — retrieval doesn't fail if
166
+ # server.log is unwritable.
167
+ #
168
+ # Identity note: the preflush fallback exists for the abrupt-exit case where
159
169
  # the operator passes the sessionKey (no conversationId was ever assigned).
160
- # For post-flush retrievals the operator passes the conversationId and the
161
- # FULL file is expected to exist.
170
+ # For post-flush retrievals the operator passes the conversationId (or any
171
+ # unambiguous prefix of it) and the FULL file is expected to exist.
162
172
  #
163
- # MIRROR: keep the two-shape contract in sync with:
164
- # - platform/ui/app/lib/logs-read-resolve.ts (canonical TS helper + tests)
165
- # - platform/plugins/admin/mcp/src/index.ts (conversationId branch)
173
+ # MIRROR intentional divergence (Task 998):
174
+ # - platform/ui/app/lib/logs-read-resolve.ts (canonical TS helper)
175
+ # EXACT-MATCH on `${prefix}${conversationId}.log`.
176
+ # - platform/plugins/admin/mcp/src/index.ts (conversationId branch) —
177
+ # EXACT-MATCH, same shape.
178
+ # Both serve agents that always hold the full conversationId. This shell
179
+ # counterpart is the operator surface; operators read 8-char ids from agent
180
+ # lines and type them at the SSH prompt. Do not "fix" the divergence by
181
+ # adding prefix-match to TS/MCP — it would mask agent bugs that pass
182
+ # partial ids.
166
183
  per_conversation_mode() {
167
184
  local conv_id="$1"
168
185
  local filter_type="${2:-agent-stream}"
@@ -172,6 +189,14 @@ per_conversation_mode() {
172
189
  exit 2
173
190
  fi
174
191
 
192
+ # Reject shell metacharacters in conv_id — the prefix-match glob below
193
+ # would otherwise turn user input into a shell pattern. UUIDs and
194
+ # sessionKeys only contain [a-zA-Z0-9-].
195
+ if [[ ! "$conv_id" =~ ^[a-zA-Z0-9-]+$ ]]; then
196
+ echo "Error: conversationId contains invalid characters (allowed: a-z, A-Z, 0-9, -)" >&2
197
+ exit 2
198
+ fi
199
+
175
200
  validate_type "$filter_type"
176
201
 
177
202
  # Platform-scoped types shortcut to their fixed files regardless of convId.
@@ -190,56 +215,98 @@ per_conversation_mode() {
190
215
  exit 2
191
216
  fi
192
217
 
193
- # Build the two filenames the two-shape contract expects.
194
- local full_name="${prefix}${conv_id}.log"
195
- local preflush_name="${prefix}preflush-${conv_id:0:12}.log"
218
+ # Glob patterns the two-shape contract expects (Task 998: prefix match).
219
+ local full_glob="${prefix}${conv_id}*.log"
220
+ local preflush_glob="${prefix}preflush-${conv_id:0:12}*.log"
196
221
 
197
- local found=0
198
222
  local searched=0
199
223
  local matched_shape=""
200
224
 
201
- # Pass 1: FULL across every log dir. Records stale preflush siblings.
225
+ # Pass 1: FULL prefix glob across every log dir; aggregate hits.
226
+ local -a full_hits=()
227
+ local -a full_dirs=()
228
+ shopt -s nullglob
202
229
  for log_dir in "${ACCOUNT_LOG_DIRS[@]}"; do
203
- local full_path="$log_dir/$full_name"
204
230
  searched=$((searched + 1))
205
- if [[ ! -f "$full_path" ]]; then
206
- continue
207
- fi
208
- [[ $found -gt 0 ]] && echo ""
209
- echo "## $(basename "$full_path") ($filter_type)$(account_suffix "$log_dir")"
231
+ for f in "$log_dir"/${prefix}${conv_id}*.log; do
232
+ full_hits+=("$f")
233
+ full_dirs+=("$log_dir")
234
+ done
235
+ done
236
+ shopt -u nullglob
237
+
238
+ # Pass 1 ambiguous → refuse, never silently pick.
239
+ if [[ ${#full_hits[@]} -gt 1 ]]; then
240
+ local -a candidate_names=()
241
+ local cf
242
+ for cf in "${full_hits[@]}"; do
243
+ candidate_names+=("$(basename "$cf")")
244
+ done
245
+ local joined
246
+ joined=$(IFS=','; printf '%s' "${candidate_names[*]}")
247
+ echo "-- trailer: conversationId=$conv_id type=$filter_type searched=$searched matches=${#full_hits[@]} reason=ambiguous-prefix candidates=[${joined}]" >&2
248
+ exit 1
249
+ fi
250
+
251
+ local found=0
252
+
253
+ # Pass 1 single hit → emit body, run stale-preflush detection in matched dir.
254
+ if [[ ${#full_hits[@]} -eq 1 ]]; then
255
+ local full_path="${full_hits[0]}"
256
+ local match_dir="${full_dirs[0]}"
257
+ echo "## $(basename "$full_path") ($filter_type)$(account_suffix "$match_dir")"
210
258
  cat "$full_path"
211
- found=$((found + 1))
259
+ found=1
212
260
  matched_shape="full"
213
- # Stale-preflush detection: if the post-flush rename left the preflush
214
- # sibling on disk, emit a housekeeping signal to server.log. Best-effort
215
- # — retrieval must not fail if server.log is unwritable.
216
- local stale_path="$log_dir/$preflush_name"
217
- if [[ -f "$stale_path" ]]; then
261
+
262
+ shopt -s nullglob
263
+ local stale_path
264
+ for stale_path in "$match_dir"/${prefix}preflush-${conv_id:0:12}*.log; do
218
265
  local stale_ts
219
266
  stale_ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
220
267
  echo "${stale_ts} [logs-read] stale-preflush-detected path=${stale_path}" >> "$SERVER_LOG" 2>/dev/null || true
221
- fi
222
- done
268
+ done
269
+ shopt -u nullglob
270
+ fi
223
271
 
224
- # Pass 2: PREFLUSH across every dir. Only runs if zero FULL hits.
272
+ # Pass 2: PREFLUSH prefix glob, only if Pass 1 had zero hits.
225
273
  if [[ $found -eq 0 ]]; then
274
+ local -a preflush_hits=()
275
+ local -a preflush_dirs=()
276
+ shopt -s nullglob
226
277
  for log_dir in "${ACCOUNT_LOG_DIRS[@]}"; do
227
- local preflush_path="$log_dir/$preflush_name"
228
278
  searched=$((searched + 1))
229
- if [[ ! -f "$preflush_path" ]]; then
230
- continue
231
- fi
232
- [[ $found -gt 0 ]] && echo ""
233
- echo "## $(basename "$preflush_path") ($filter_type)$(account_suffix "$log_dir")"
279
+ for f in "$log_dir"/${prefix}preflush-${conv_id:0:12}*.log; do
280
+ preflush_hits+=("$f")
281
+ preflush_dirs+=("$log_dir")
282
+ done
283
+ done
284
+ shopt -u nullglob
285
+
286
+ if [[ ${#preflush_hits[@]} -gt 1 ]]; then
287
+ local -a candidate_names=()
288
+ local pf
289
+ for pf in "${preflush_hits[@]}"; do
290
+ candidate_names+=("$(basename "$pf")")
291
+ done
292
+ local joined
293
+ joined=$(IFS=','; printf '%s' "${candidate_names[*]}")
294
+ echo "-- trailer: conversationId=$conv_id type=$filter_type searched=$searched matches=${#preflush_hits[@]} reason=ambiguous-prefix candidates=[${joined}]" >&2
295
+ exit 1
296
+ fi
297
+
298
+ if [[ ${#preflush_hits[@]} -eq 1 ]]; then
299
+ local preflush_path="${preflush_hits[0]}"
300
+ echo "## $(basename "$preflush_path") ($filter_type)$(account_suffix "${preflush_dirs[0]}")"
234
301
  cat "$preflush_path"
235
- found=$((found + 1))
302
+ found=1
236
303
  matched_shape="preflush"
237
- done
304
+ fi
238
305
  fi
239
306
 
240
307
  # Trailer: empty output never leaves the reader guessing why.
241
308
  if [[ $found -eq 0 ]]; then
242
- echo "-- trailer: conversationId=$conv_id type=$filter_type searched=$searched found=0 tried=[${full_name}, ${preflush_name}] reason=file-not-found-in-either-shape" >&2
309
+ echo "-- trailer: conversationId=$conv_id type=$filter_type searched=$searched found=0 tried=[${full_glob}, ${preflush_glob}] reason=file-not-found-in-either-shape" >&2
243
310
  exit 1
244
311
  fi
245
312
  echo "" >&2
@@ -22,6 +22,10 @@ CONV_ID="8c264cfb-441f-48bf-9811-9fa3ad3b51dc"
22
22
  PREFLUSH_SLICE="8c264cfb-441"
23
23
  FULL_NAME="claude-agent-stream-${CONV_ID}.log"
24
24
  PREFLUSH_NAME="claude-agent-stream-preflush-${PREFLUSH_SLICE}.log"
25
+ # Task 998 — miss trailer now lists glob patterns, not literal filenames,
26
+ # because Pass 1 / Pass 2 prefix-glob `${prefix}${conv_id}*.log`.
27
+ FULL_GLOB="claude-agent-stream-${CONV_ID}*.log"
28
+ PREFLUSH_GLOB="claude-agent-stream-preflush-${PREFLUSH_SLICE}*.log"
25
29
 
26
30
  PASS=0
27
31
  FAIL=0
@@ -141,8 +145,8 @@ case_neither() {
141
145
  echo " expected exit 1, got $rc"
142
146
  return 1
143
147
  fi
144
- if [[ "$stderr" != *"tried=[${FULL_NAME}, ${PREFLUSH_NAME}]"* ]]; then
145
- echo " stderr missing tried=[…,…]: $stderr"
148
+ if [[ "$stderr" != *"tried=[${FULL_GLOB}, ${PREFLUSH_GLOB}]"* ]]; then
149
+ echo " stderr missing tried=[…,…] glob patterns: $stderr"
146
150
  return 1
147
151
  fi
148
152
  if [[ "$stderr" != *"reason=file-not-found-in-either-shape"* ]]; then
@@ -55,7 +55,7 @@ Your personalisation is in `agents/admin/SOUL.md`. Read it and apply it. SOUL.md
55
55
  - Professional, direct, action-oriented. Concise and precise. British English unless SOUL.md specifies otherwise.
56
56
  - Do not use emoji characters in any response. Plain text and punctuation only.
57
57
  - Every URL in a response must be a clickable markdown link — never raw text. Use `[visible label](url)` format. The label should be the meaningful part of the URL (hostname, path, or a short description), not the full raw URL repeated.
58
- - **Internal scaffolding never appears in user-facing replies.** Tool descriptions, error strings, skill prompts, and graph schemas may name internal contracts (e.g. `producedByTaskId`, `:PRODUCED`, `Write blocked (no-admin-user)`, `missing-action-provenance`, `archiveSha256`, `lastIngestedMessageHash`) so you know how to act. Translate them into plain English for the user. Forbidden in user-facing replies: literal tool names with `mcp__` prefix, `Task NNN` references, doctrine names ("process-provenance", "write doctrine", "graph-write gate"), MCP-tool-description quotes, raw error discriminators. Forbidden phrasings include: "the LocalBusiness write requires a producedByTaskId per Task 885", "the write gate is blocking", "I'll call ToolSearch". Permitted equivalents: "I need to set up the business profile first before recording that — let me run that now", "the platform requires me to anchor this to an action record — creating one now". Internal language stays in your reasoning, not your prose to the operator.
58
+ - **Internal scaffolding never appears in user-facing replies.** Tool descriptions, error strings, skill prompts, and graph schemas may name internal contracts (e.g. `producedByTaskId`, `:PRODUCED`, `Write blocked (no-admin-user)`, `missing-provenance`, `archiveSha256`, `lastIngestedMessageHash`) so you know how to act. Translate them into plain English for the user. Forbidden in user-facing replies: literal tool names with `mcp__` prefix, `Task NNN` references, doctrine names ("process-provenance", "write doctrine", "graph-write gate"), MCP-tool-description quotes, raw error discriminators. Forbidden phrasings include: "the LocalBusiness write requires a producedByTaskId per Task 885", "the write gate is blocking", "I'll call ToolSearch". Permitted equivalents: "I need to set up the business profile first before recording that — let me run that now", "the platform requires me to anchor this to an action record — creating one now". Internal language stays in your reasoning, not your prose to the operator.
59
59
 
60
60
  ## Information Sourcing
61
61
 
@@ -89,7 +89,9 @@ Do not retry the same tool against the same target within a turn. A second ident
89
89
 
90
90
  When a tool returns a structured failure whose error content begins with an UPPERCASE_ERROR_CODE (for example `WEBFETCH_CANNOT_READ_JS_SPA`), the runtime has already determined that retrying the same tool will fail and that a substitute would launder uncertainty. Read the error's plain-English explanation, then write one or two sentences to the owner that name (a) what failed, (b) the reason in their language, and (c) the concrete actions they can take to unblock — typically pasting text or sending a screenshot. Do not silently dispatch a substitute (Playwright, research-assistant, memory-search) to continue the original instruction; that hides the failure and the owner loses the ability to judge whether the substitute's output answers their question. A verbal instruction in the current conversation is not consent — only an explicit standing policy recorded in account configuration counts, and no such mechanism exists today. Until one exists, every structured tool failure becomes a question for the owner. Wait for direction before resuming.
91
91
 
92
- **Post-deterministic-error reply contract — never re-solicit held values.** When a Cloudflare form failure surfaces in the chat relay, the trailing fenced `json` block labelled "Held by deterministic tools (do not re-solicit)" is the route's structured payload. It carries `inputsAlreadyHeld` (the FQDNs already submitted: `admin`, `public`, `apex`) and `discoveryResults` (the discovery snapshot the route was holding: `tunnels`, `domains`). The next reply structurally consumes this payload — restate the held values verbatim, name the literal error, and decide between two actions: (a) if the `<deployment>` block's `bundleMtime` post-dates this failure's timestamp, the deterministic path has been redeployed with a fix re-invoke the same path immediately; (b) otherwise surface the literal error plus the next deterministic step from the relevant SKILL (`setup-tunnel.sh`, `reset-tunnel.sh`, `dashboard-guide.md`). Asking the operator for a hostname, tunnel name, or apex that already appears in `inputsAlreadyHeld` or `discoveryResults` is a doctrine violation the structured payload is the answer set; the deterministic path holds the values; the chat relay is for restatement, not re-entry.
92
+ **Post-deterministic-error reply contract — restate, never re-solicit.** On `cloudflare-setup` error, the route returns either a literal error envelope or a deterministic re-invoke; the reply restates the literal error verbatim and stops. The envelope's `inputsAlreadyHeld` (FQDNs already submitted: `admin`, `public`, `apex`) and `discoveryResults` (tunnels + domains snapshot) are the answer setasking the operator for a hostname, tunnel name, or apex that appears there is a doctrine violation. Retry-on-redeployment is decided by the route, not by the reply.
93
+
94
+ **Post-action restatement contract.** When a `<post-action>` block appears in your system prompt, it carries the literal terminal outcome of the chain's most-recent action card (`actionId`, `outcome ∈ {completed, failed}`, `phase`, `at`). The next free-text reply restates that outcome verbatim and stops. Do not narrate, predict, or recompute the status — the route closed the audit Task and the block IS the answer.
93
95
 
94
96
  ## Cypher schema
95
97
 
@@ -129,6 +131,8 @@ Operationalises the CONCISE prerogative for clarification. Three rules:
129
131
 
130
132
  **Bundled-SKILL access contract.** Plugin skills and references live in the bundled npm package, not on disk under `<accountDir>/agents/admin/plugins/`. Load them with `mcp__admin__plugin-read` only — the `<plugin-manifest>` `Skills:` and `References:` lines name the exact `pluginName` and file path to pass. `Read`, `Glob`, and `Bash` against those paths will fail with `File does not exist`, and the runtime agent-loop-stop interceptor cannot distinguish a generic-error retry from a tool-routing mistake. Subagents inherit this contract via the parent system prompt — never dispatch an `Agent` subagent with a `Read` directive against a bundled SKILL path.
131
133
 
134
+ **Plain-English precision pass.** Load `admin/skills/plainly/SKILL.md` via `plugin-read` on the first turn of a session where the user asks to explain, define, or pre-empts one of the trigger phrases ("explain in plain English", "what does X mean", "define X", "I don't understand", "what is this"), AND on the first turn where you are about to return a reply containing a term not in the operator's prior turn. Apply the AI-tells strip + recursive-rule pass to the reply before sending it. The skill applies to prose returned to the operator, not to MCP tool arguments — agent-to-machine payloads pass through unmodified.
135
+
132
136
  Plugins provide domain-specific tools that query their own data stores directly. `memory-search` is a general-purpose semantic search across the entire knowledge graph — it finds nodes by vector similarity, which means results are ranked by semantic closeness to the query, not by domain relevance. A query containing the word "email" will surface product documentation *about* email features before it surfaces actual Email nodes whose content is unrelated to the query wording.
133
137
 
134
138
  When the user's intent maps to a specific plugin's domain, use that plugin's tools — not `memory-search`. The `<plugin-manifest>` groups tools by plugin and describes each plugin's purpose and retrieval paths. The `<specialist-domains>` block within it lists every specialist-owned tool by name. Match user intent to a tool in these registries first; fall back to `memory-search` only when the query genuinely spans multiple domains or no tool in the manifest matches the intent.
@@ -48,3 +48,9 @@ When you need to clarify intent before acting, follow two rules.
48
48
  1. **One-sided questions only.** Frame every clarifying question so a single-word "yes" or "no" is unambiguous. Never pose two opposing framings joined by "or" — "Should I book this, or not?", "Want a quote, or shall I take a message?". "Yes" to such a question is unusable — the visitor could have meant either side. Pick one side and ask it plainly: "Shall I take a message?" — not "take a message, or leave it?".
49
49
 
50
50
  2. **No menu when the signal is clear.** When an internal lookup or tool returns a specific outcome — the answer is there, or the answer is not — relay that outcome. Do not offer the visitor a menu of next steps when only one makes sense. If a lookup fails, tell the visitor what to do next (take a message, contact the business directly) rather than asking them to choose between recoveries.
51
+
52
+ ## Plain-English precision pass
53
+
54
+ Load `admin/skills/plainly/SKILL.md` via `plugin-read` on the first text-producing turn of every session. Apply the AI-tells strip and the recursive plain-English rule to every reply you return to the visitor before emitting it — this is a prime-directive prerogative that overrides every other formatting preference. Customers are the highest-stakes audience this platform serves; plain English is not a polish step, it is the contract.
55
+
56
+ The skill applies to prose returned to the visitor and to text content passed through `render-component`. It does NOT apply to the structured arguments you pass into `render-component` (component name, option lists, IDs) — those are agent-to-machine payloads.
@@ -96,3 +96,9 @@ Return to the admin agent:
96
96
  ## Tool failure discipline
97
97
 
98
98
  When a tool returns an error, surface the failure and its diagnostic context before taking another action. Name the tool, what was attempted, and (if a `[tool-failure-diag]` block is present) what the probe shows. Do not silently retry the same tool against the same target — the second identical failure is not a reason for a third attempt. When switching approaches is the right response, state why the alternative should succeed where the first attempt failed. Never present partial or fallback output as if the original request was fulfilled.
99
+
100
+ ## Plain-English precision pass
101
+
102
+ Load `admin/skills/plainly/SKILL.md` via `plugin-read` on the first turn of every session. Apply the AI-tells strip + recursive plain-English rule to every prose artifact you return to the admin agent — captions, brochure prose, summaries, the "What you did" / "Summary" output contract above. This is a prime-directive prerogative; do not wait for admin to ask.
103
+
104
+ **Receiving-endpoint carve-out.** Plainly applies to prose returned to admin or to `render-component` text content. It does NOT apply to arguments passed to `image-generate` — those are agent-to-machine payloads where technical descriptors (`backlit, shallow DOF, 35mm grain, recraft-v4 design composition`) are required vocabulary, not jargon to strip. Pass image prompts through verbatim.
@@ -191,3 +191,9 @@ When two nodes represent the same real-world entity (two `:Person` rows for the
191
191
  ## Tool failure discipline
192
192
 
193
193
  When a tool returns an error, surface the failure and its diagnostic context before taking another action. Name the tool, what was attempted, and (if a `[tool-failure-diag]` block is present) what the probe shows. Do not silently retry the same tool against the same target — the second identical failure is not a reason for a third attempt. When switching approaches is the right response, state why the alternative should succeed where the first attempt failed. Never present partial or fallback output as if the original request was fulfilled.
194
+
195
+ ## Plain-English precision pass
196
+
197
+ Load `admin/skills/plainly/SKILL.md` via `plugin-read` on the first turn of every session. Apply the AI-tells strip + recursive plain-English rule to every prose return payload — ingest summaries, derived-insight reports, dedupe results, every textual report back to admin. This is a prime-directive prerogative; do not wait for admin to ask.
198
+
199
+ **Receiving-endpoint carve-out.** Plainly applies to prose returned to admin. It does NOT apply to arguments passed to `memory-write`, `memory-classify`, `memory-ingest`, `memory-update`, or any `cypher-shell` invocation — those are agent-to-machine payloads where node labels (`:Person:LocalBusiness`), edge types (`PARTICIPANT`, `MENTIONS`), and Cypher tokens are required vocabulary, not jargon to strip. Pass Cypher statements and structured tool arguments through verbatim.
@@ -201,3 +201,9 @@ Controls the browser via Playwright to complete web-based tasks. The browser is
201
201
  ## Tool failure discipline
202
202
 
203
203
  When a tool returns an error (email send, WhatsApp send, browser navigate, Telegram, scheduling, Cloudflare, Anthropic key), surface the failure before taking any other action. Name the tool, what was attempted, and — if a `[tool-failure-diag]` block is present — what the diagnostic reveals (DNS resolved? TCP connected? HTTP status? internal timeout?). Do not retry the same tool against the same target within a turn; the second identical failure is evidence the path is broken. If switching approaches is the right response (for example, retrying email via a different channel, or navigating via HTTP rather than the browser), state why the alternative should succeed. Silent fallback — attempting a different tool family without acknowledging the original failure — is never acceptable; the admin agent and the owner must see that the first attempt failed and understand the reason for the switch.
204
+
205
+ ## Plain-English precision pass
206
+
207
+ Load `admin/skills/plainly/SKILL.md` via `plugin-read` on the first turn of every session. Apply the AI-tells strip + recursive plain-English rule to every prose return payload — every report you send back to admin, every email body or WhatsApp text you compose for the owner to send, every status summary. This is a prime-directive prerogative; do not wait for admin to ask.
208
+
209
+ **Receiving-endpoint carve-out.** Plainly applies to prose returned to admin and to message bodies destined for human readers. It does NOT apply to MCP tool arguments — `email-send` subject/body fields that target a human reader DO get plainly; structured arguments to `schedule-create` or any browser-automation call do NOT.