@rubytech/create-maxy-code 0.1.223 → 0.1.225

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 (41) hide show
  1. package/dist/__tests__/installer-settings-permissions.test.js +112 -0
  2. package/dist/index.js +13 -0
  3. package/dist/permissions-seed.js +76 -0
  4. package/package.json +1 -1
  5. package/payload/platform/plugins/admin/mcp/dist/index.js +1 -1
  6. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  7. package/payload/platform/plugins/admin/skills/file-presentation/SKILL.md +9 -7
  8. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +18 -4
  9. package/payload/platform/plugins/admin/skills/professional-document/SKILL.md +1 -1
  10. package/payload/platform/plugins/docs/references/admin-ui.md +11 -1
  11. package/payload/platform/plugins/docs/references/deployment.md +1 -1
  12. package/payload/platform/plugins/docs/references/platform.md +4 -0
  13. package/payload/platform/plugins/docs/references/troubleshooting.md +1 -1
  14. package/payload/platform/plugins/graph-viewer/mcp/dist/index.js +1 -1
  15. package/payload/platform/plugins/graph-viewer/mcp/dist/index.js.map +1 -1
  16. package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.d.ts.map +1 -1
  17. package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.js +7 -5
  18. package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.js.map +1 -1
  19. package/payload/platform/plugins/graph-viewer/skills/render-graph/SKILL.md +6 -6
  20. package/payload/platform/plugins/scheduling/PLUGIN.md +2 -2
  21. package/payload/platform/plugins/scheduling/mcp/dist/index.js +2 -2
  22. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
  23. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-export-ics.d.ts.map +1 -1
  24. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-export-ics.js +3 -2
  25. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-export-ics.js.map +1 -1
  26. package/payload/platform/scripts/check-plugin-references.mjs +256 -0
  27. package/payload/platform/scripts/logs-read-jsonl.test.sh +307 -0
  28. package/payload/platform/scripts/logs-read.sh +435 -32
  29. package/payload/platform/scripts/setup-account.sh +20 -8
  30. package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts +0 -1
  31. package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
  32. package/payload/platform/services/claude-session-manager/dist/system-prompt.js +11 -10
  33. package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
  34. package/payload/platform/templates/specialists/agents/content-producer.md +1 -1
  35. package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
  36. package/payload/premium-plugins/teaching/PLUGIN.md +7 -7
  37. package/payload/premium-plugins/venture-studio/skills/brand-pack/SKILL.md +1 -1
  38. package/payload/server/public/assets/data-BzRzOKWv.js +1 -0
  39. package/payload/server/public/data.html +1 -1
  40. package/payload/server/server.js +13 -6
  41. package/payload/server/public/assets/data-CHdaTjgn.js +0 -1
@@ -1,14 +1,22 @@
1
1
  #!/usr/bin/env bash
2
2
  # logs-read.sh — Shell counterpart to the logs-read MCP tool.
3
3
  #
4
- # stream logs are per-session, not per-day or per-conversation.
5
- # The primary mode reads a single {prefix}-{sessionKey}.log file — the
6
- # operator types a sessionKey prefix and the surface returns the bytes from
7
- # first [spawn] to final [process-exit] without filtering. sessionKey is
8
- # the single identifier on disk; the earlier full vs preflush split is gone.
4
+ # On rc-spawn / claude.ai-code installs the per-session evidence is the Claude
5
+ # Code JSONL transcript under the configDir, NOT a per-account stream log.
6
+ # So the bare <key> mode (Task 585) accepts a claude.ai `session_<id>` (or its
7
+ # bare suffix, or the intrinsic <uuid> / a uuid prefix), maps it to the local
8
+ # <uuid> via sessions/<pid>.json (or the <uuid>.meta.json bridgeIds, or a
9
+ # content scan), and prints one timestamp-ordered timeline merging the parent
10
+ # transcript with every subagents/agent-*.jsonl — flagging each subagent
11
+ # is_error tool_result inline (agentType + failing tool + error text). An
12
+ # explicit <key> <type> still reads the legacy per-account stream log.
9
13
  #
10
14
  # Modes:
11
- # logs-read.sh <sessionKey-or-prefix> [type] Read {prefix}-{sessionKey}.log
15
+ # logs-read.sh <sessionKey> Merged JSONL session+subagent timeline
16
+ # logs-read.sh <sessionKey> <type> Legacy per-account stream-log read
17
+ # logs-read.sh --scan-subagent-errors [N] List subagent transcripts with is_error
18
+ # results (silent-failure audit; N = most-
19
+ # recent N transcripts, default all)
12
20
  # logs-read.sh --tail [type] [lines] Tail most recent file of type
13
21
  # logs-read.sh --grep <sessionKey> [type] Legacy grep across log files
14
22
  #
@@ -50,32 +58,54 @@ CONFIG_DIR="$HOME/.$INSTALL_DIR_NAME"
50
58
  SERVER_LOG="$CONFIG_DIR/logs/server.log"
51
59
  VNC_LOG="$CONFIG_DIR/logs/vnc-boot.log"
52
60
 
61
+ # --- Resolve the Claude Code state tree (Task 585) ---
62
+ # rc-spawn per-session evidence is NOT under data/accounts/<id>/logs/ — it is
63
+ # the Claude Code JSONL transcript tree under the configDir. The session
64
+ # manager exports CLAUDE_CONFIG_DIR for the spawned `claude` processes; an
65
+ # operator shell usually has it unset, so derive the brand-isolated default
66
+ # ($CONFIG_DIR/.claude) and honour an explicit override (mirrors
67
+ # jsonl-path.ts's claudeStateRoot). Layout consumed by the <key> and scan
68
+ # modes:
69
+ # projects/<slug>/<uuid>.jsonl — parent transcript
70
+ # projects/<slug>/<uuid>/subagents/agent-<hex>.jsonl — child transcripts
71
+ # projects/<slug>/<uuid>/subagents/agent-<hex>.meta.json — {agentType,...}
72
+ # projects/<slug>/<uuid>.meta.json — sidecar (carries bridgeIds)
73
+ # sessions/<pid>.json — {sessionId, bridgeSessionId}
74
+ CLAUDE_TREE="${CLAUDE_CONFIG_DIR:-$CONFIG_DIR/.claude}"
75
+ PROJECTS_ROOT="$CLAUDE_TREE/projects"
76
+ SESSIONS_DIR="$CLAUDE_TREE/sessions"
77
+
78
+ # --- Resolve account log dirs (best-effort; required only by stream-log modes) ---
79
+ # On rc-spawn installs data/accounts/<id>/ exists but carries no logs/ subdir,
80
+ # so this array is legitimately empty. The per-session JSONL <key> mode and the
81
+ # --scan-subagent-errors mode do NOT touch it; the stream-log tail/grep/
82
+ # explicit-type modes call require_account_logs() before iterating it, which
83
+ # preserves their original exit-2 "missing directory" contract.
53
84
  ACCOUNTS_DIR="$INSTALL_DIR/data/accounts"
54
- if [[ ! -d "$ACCOUNTS_DIR" ]]; then
55
- echo "Error: accounts directory does not exist: $ACCOUNTS_DIR" >&2
56
- exit 2
57
- fi
58
-
59
- # Glob for account dirs — may be >1 on multi-account devices
60
- shopt -s nullglob
61
- ACCOUNT_DIRS=("$ACCOUNTS_DIR"/*/)
62
- shopt -u nullglob
63
- if [[ ${#ACCOUNT_DIRS[@]} -eq 0 ]]; then
64
- echo "Error: no account directories found in $ACCOUNTS_DIR" >&2
65
- exit 2
66
- fi
67
-
68
- # Build array of account log directories that actually exist.
69
85
  ACCOUNT_LOG_DIRS=()
70
- for _adir in "${ACCOUNT_DIRS[@]}"; do
71
- [[ -d "${_adir}logs" ]] && ACCOUNT_LOG_DIRS+=("${_adir}logs")
72
- done
73
- if [[ ${#ACCOUNT_LOG_DIRS[@]} -eq 0 ]]; then
74
- echo "Error: no account log directories found in $ACCOUNTS_DIR" >&2
75
- exit 2
86
+ if [[ -d "$ACCOUNTS_DIR" ]]; then
87
+ shopt -s nullglob
88
+ for _adir in "$ACCOUNTS_DIR"/*/; do
89
+ [[ -d "${_adir}logs" ]] && ACCOUNT_LOG_DIRS+=("${_adir}logs")
90
+ done
91
+ shopt -u nullglob
76
92
  fi
77
93
  MULTI_ACCOUNT=$(( ${#ACCOUNT_LOG_DIRS[@]} > 1 ? 1 : 0 ))
78
94
 
95
+ # Stream-log modes require at least one account log dir; the JSONL <key> mode
96
+ # and the scan mode do not. Called at the entry of the stream-log readers so
97
+ # the configDir-only modes never dead-end on a missing data/accounts tree.
98
+ require_account_logs() {
99
+ if [[ ! -d "$ACCOUNTS_DIR" ]]; then
100
+ echo "Error: accounts directory does not exist: $ACCOUNTS_DIR" >&2
101
+ exit 2
102
+ fi
103
+ if [[ ${#ACCOUNT_LOG_DIRS[@]} -eq 0 ]]; then
104
+ echo "Error: no account log directories found in $ACCOUNTS_DIR" >&2
105
+ exit 2
106
+ fi
107
+ }
108
+
79
109
  # Account ID suffix for headers — only shown when multiple accounts exist.
80
110
  account_suffix() {
81
111
  if [[ $MULTI_ACCOUNT -eq 1 ]]; then
@@ -139,7 +169,10 @@ SESSIONKEY_REGEX='^[a-zA-Z0-9_-]+$'
139
169
  usage() {
140
170
  cat >&2 <<EOF
141
171
  Usage:
142
- logs-read.sh <sessionKey-or-prefix> [type] Read {prefix}-{sessionKey}.log
172
+ logs-read.sh <sessionKey> Merged JSONL session+subagent timeline
173
+ logs-read.sh <sessionKey> <type> Legacy per-account stream-log read
174
+ logs-read.sh --scan-subagent-errors [N] List subagent transcripts carrying an
175
+ is_error result (silent-failure audit)
143
176
  logs-read.sh --tail [type] [lines] Tail most recent log of type
144
177
  logs-read.sh --tail chat-attempts [min] Cross-grep edge.log + server.log
145
178
  for the chat-ingress quartet
@@ -148,9 +181,14 @@ Usage:
148
181
  Types: $VALID_TYPES
149
182
  Default type: agent-stream | Default lines: 50 | Default chat-attempts minutes: 5
150
183
 
151
- Per-session types (agent-stream, system, session, error, public) require
152
- sessionKey in the first mode the log file is named {prefix}-{sessionKey}.log.
153
- Platform-scoped types (server, vnc, heartbeat) ignore the id.
184
+ A bare <sessionKey> reads the Claude Code JSONL transcript tree under the
185
+ configDir (CLAUDE_CONFIG_DIR, default \$HOME/.<brand>/.claude): it accepts a
186
+ claude.ai session_<id>, its bare suffix, or the intrinsic <uuid> (or a uuid
187
+ prefix), and prints the parent transcript merged with every subagent
188
+ transcript, flagging subagent is_error results inline. An explicit <type>
189
+ keeps the legacy per-account stream-log read.
190
+ Per-session stream-log types (agent-stream, system, session, error, public)
191
+ require sessionKey; platform-scoped types (server, vnc, heartbeat) ignore the id.
154
192
  chat-attempts is --tail-only — it aggregates the four ingress checkpoints
155
193
  ([edge-admin] inbound, [admin-auth] outcome, [chat-route] entered,
156
194
  [edge-admin] outcome) into one timeline.
@@ -305,6 +343,10 @@ per_conversation_mode() {
305
343
  ;;
306
344
  esac
307
345
 
346
+ # Explicit-type per-session reads come from the per-account stream logs;
347
+ # preserve the original exit-2 contract when that tree is absent.
348
+ require_account_logs
349
+
308
350
  local prefix
309
351
  prefix=$(prefix_for_type "$filter_type")
310
352
  if [[ -z "$prefix" ]]; then
@@ -369,6 +411,9 @@ grep_mode() {
369
411
  exit 2
370
412
  fi
371
413
 
414
+ # grep mode reads per-account stream logs; preserve its missing-dir contract.
415
+ require_account_logs
416
+
372
417
  if [[ -n "$filter_type" ]]; then
373
418
  validate_type "$filter_type"
374
419
  fi
@@ -479,6 +524,7 @@ tail_mode() {
479
524
  fi
480
525
 
481
526
  if [[ "$log_type" == "heartbeat" ]]; then
527
+ require_account_logs
482
528
  local hb_file=""
483
529
  for log_dir in "${ACCOUNT_LOG_DIRS[@]}"; do
484
530
  local candidate="${log_dir}/check-due-events.log"
@@ -520,6 +566,10 @@ tail_mode() {
520
566
  exit 0
521
567
  fi
522
568
 
569
+ # Reached only for per-session stream-log types (agent-stream/error/session/
570
+ # public/mcp); these live under the per-account log dirs.
571
+ require_account_logs
572
+
523
573
  local prefix
524
574
  prefix=$(prefix_for_type "$log_type")
525
575
 
@@ -550,6 +600,349 @@ tail_mode() {
550
600
  exit 0
551
601
  }
552
602
 
603
+ # === rc-spawn JSONL retrieval (Task 585) ===================================
604
+ # The bare <key> mode and --scan-subagent-errors read the Claude Code JSONL
605
+ # transcript tree under the configDir, not the per-account stream logs.
606
+ # python3 is the JSON dependency, deliberately not node: the systemd unit
607
+ # PATH includes /usr/bin but not nvm's node bin, and python3 is already
608
+ # required by setup-account.sh / seed-neo4j.sh, so this survives a minimal
609
+ # non-interactive SSH PATH. argv[1] selects the mode (timeline|scan); the
610
+ # program is fed via -c so logs-read.sh stays a single self-contained file.
611
+ JSONL_PY=$(cat <<'PY'
612
+ import sys, os, re, json, glob
613
+
614
+ MODE = sys.argv[1] if len(sys.argv) > 1 else ''
615
+
616
+ UUID_RE = re.compile(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', re.I)
617
+ HEXPREFIX_RE = re.compile(r'^[0-9a-f]{8,}$', re.I)
618
+
619
+ def clip(s, n):
620
+ if not isinstance(s, str):
621
+ s = '' if s is None else str(s)
622
+ s = s.replace('\n', ' ').strip()
623
+ return s if len(s) <= n else s[:n-1] + '…'
624
+
625
+ def load_jsonl(path):
626
+ rows = []
627
+ try:
628
+ with open(path, 'r', errors='replace') as f:
629
+ for line in f:
630
+ line = line.strip()
631
+ if not line:
632
+ continue
633
+ try:
634
+ rows.append(json.loads(line))
635
+ except Exception:
636
+ continue
637
+ except OSError:
638
+ pass
639
+ return rows
640
+
641
+ def load_json(path):
642
+ try:
643
+ with open(path, 'r', errors='replace') as f:
644
+ return json.load(f)
645
+ except Exception:
646
+ return None
647
+
648
+ def parent_jsonl_exact(projects_root, uuid):
649
+ for p in glob.glob(os.path.join(projects_root, '*', uuid + '.jsonl')):
650
+ return p
651
+ for p in glob.glob(os.path.join(projects_root, '*', 'archive', uuid + '.jsonl')):
652
+ return p
653
+ return None
654
+
655
+ def parent_jsonls_by_prefix(projects_root, prefix):
656
+ out = set()
657
+ for p in glob.glob(os.path.join(projects_root, '*', prefix + '*.jsonl')):
658
+ if UUID_RE.match(os.path.basename(p)[:-6]):
659
+ out.add(p)
660
+ return sorted(out)
661
+
662
+ def agent_type(af):
663
+ o = load_json(af[:-len('.jsonl')] + '.meta.json')
664
+ if isinstance(o, dict) and isinstance(o.get('agentType'), str) and o['agentType']:
665
+ return o['agentType']
666
+ return None
667
+
668
+ def fmt_input(d):
669
+ if isinstance(d, dict):
670
+ for k in ('command', 'file_path', 'pattern', 'path', 'description', 'prompt'):
671
+ v = d.get(k)
672
+ if isinstance(v, str) and v:
673
+ return '%s=%s' % (k, v)
674
+ try:
675
+ return json.dumps(d, ensure_ascii=False)
676
+ except Exception:
677
+ return str(d)
678
+ return '' if d is None else str(d)
679
+
680
+ def result_text(block, row):
681
+ c = block.get('content')
682
+ if isinstance(c, str) and c.strip():
683
+ return c
684
+ if isinstance(c, list):
685
+ parts = [x.get('text') if isinstance(x, dict) else x for x in c]
686
+ joined = ' '.join(p for p in parts if isinstance(p, str))
687
+ if joined.strip():
688
+ return joined
689
+ tur = row.get('toolUseResult')
690
+ return tur if isinstance(tur, str) else ''
691
+
692
+ # Map a claude.ai session key to the local intrinsic <uuid> + its parent JSONL.
693
+ # Order: (a) the key already IS a uuid (or a hex prefix of one) on disk;
694
+ # (b) a sessions/<pid>.json whose bridgeSessionId matches (live/recent);
695
+ # (c) a <uuid>.meta.json whose bridgeIds carries the suffix (persistent, so an
696
+ # ENDED session whose PID file was deleted still resolves); (d) content grep
697
+ # of top-level transcripts (last resort). Returns (uuid, parent_path, via) or
698
+ # (None, None, reason).
699
+ def resolve_uuid(projects_root, sessions_dir, key):
700
+ suffix = key[len('session_'):] if key.startswith('session_') else key
701
+ candidates = [key] if key == suffix else [key, suffix]
702
+ for c in candidates:
703
+ p = parent_jsonl_exact(projects_root, c)
704
+ if p:
705
+ return (os.path.basename(p)[:-6], p, 'uuid-exact')
706
+ if HEXPREFIX_RE.match(c):
707
+ hits = parent_jsonls_by_prefix(projects_root, c)
708
+ if len(hits) == 1:
709
+ return (os.path.basename(hits[0])[:-6], hits[0], 'uuid-prefix')
710
+ if len(hits) > 1:
711
+ return (None, None, 'ambiguous-prefix candidates=%d' % len(hits))
712
+ for pf in glob.glob(os.path.join(sessions_dir, '*.json')):
713
+ o = load_json(pf)
714
+ if not isinstance(o, dict):
715
+ continue
716
+ sid, bsid = o.get('sessionId'), o.get('bridgeSessionId')
717
+ if not (isinstance(sid, str) and sid):
718
+ continue
719
+ if isinstance(bsid, str) and bsid and (
720
+ bsid == key or bsid == 'session_' + suffix or bsid.replace('session_', '') == suffix
721
+ ):
722
+ return (sid, parent_jsonl_exact(projects_root, sid), 'pid-bridge')
723
+ for mf in glob.glob(os.path.join(projects_root, '*', '*.meta.json')):
724
+ o = load_json(mf)
725
+ if isinstance(o, dict) and isinstance(o.get('bridgeIds'), list) and suffix in o['bridgeIds']:
726
+ uuid = os.path.basename(mf)[:-len('.meta.json')]
727
+ if UUID_RE.match(uuid):
728
+ return (uuid, parent_jsonl_exact(projects_root, uuid), 'bridgeids')
729
+ # Match the verbatim key (the full session_<suffix> the operator passed, or
730
+ # the bare token) — not a loose bare-suffix substring, which could collide
731
+ # with unrelated body text. Refuse on multiple matches rather than silently
732
+ # pick the wrong session (faithfulness over a best-effort guess).
733
+ matches = []
734
+ for p in glob.glob(os.path.join(projects_root, '*', '*.jsonl')):
735
+ if not UUID_RE.match(os.path.basename(p)[:-6]):
736
+ continue
737
+ try:
738
+ with open(p, 'r', errors='replace') as f:
739
+ blob = f.read()
740
+ except OSError:
741
+ continue
742
+ if key in blob:
743
+ matches.append(p)
744
+ if len(matches) == 1:
745
+ return (os.path.basename(matches[0])[:-6], matches[0], 'content-grep')
746
+ if len(matches) > 1:
747
+ return (None, None, 'ambiguous-content-match candidates=%d' % len(matches))
748
+ return (None, None, 'not-found')
749
+
750
+ def subagent_dir(projects_root, uuid):
751
+ for d in glob.glob(os.path.join(projects_root, '*', uuid, 'subagents')):
752
+ return d
753
+ return None
754
+
755
+ # Append (ts, source, kind, text) tuples for one transcript. tool_use_id is
756
+ # matched back to the assistant tool_use that issued it, so an is_error
757
+ # tool_result names the failing tool. is_sub distinguishes a subagent's
758
+ # failure (the task's target, flagged 'SUBAGENT ERROR' and counted) from the
759
+ # parent session's own tool error (shown plainly, never counted as a subagent
760
+ # error). Returns the count of SUBAGENT errors found (0 for the parent).
761
+ def collect_events(path, source, events, is_sub):
762
+ rows = load_jsonl(path)
763
+ id2tool = {}
764
+ for r in rows:
765
+ if not isinstance(r, dict):
766
+ continue
767
+ msg = r.get('message') if isinstance(r.get('message'), dict) else {}
768
+ content = msg.get('content')
769
+ if isinstance(content, list):
770
+ for b in content:
771
+ if isinstance(b, dict) and b.get('type') == 'tool_use' and b.get('id'):
772
+ id2tool[b['id']] = b.get('name')
773
+ n_err = 0
774
+ for r in rows:
775
+ if not isinstance(r, dict):
776
+ continue
777
+ ts = r.get('timestamp') or ''
778
+ t = r.get('type')
779
+ msg = r.get('message') if isinstance(r.get('message'), dict) else {}
780
+ content = msg.get('content')
781
+ if t == 'assistant':
782
+ if isinstance(content, list):
783
+ for b in content:
784
+ if not isinstance(b, dict):
785
+ continue
786
+ if b.get('type') == 'text' and isinstance(b.get('text'), str) and b['text'].strip():
787
+ events.append((ts, source, 'text', 'assistant: ' + clip(b['text'], 160)))
788
+ elif b.get('type') == 'tool_use':
789
+ events.append((ts, source, 'tool', 'tool_use: %s(%s)' % (b.get('name'), clip(fmt_input(b.get('input')), 120))))
790
+ elif isinstance(content, str) and content.strip():
791
+ events.append((ts, source, 'text', 'assistant: ' + clip(content, 160)))
792
+ elif t == 'user':
793
+ if isinstance(content, list):
794
+ for b in content:
795
+ if isinstance(b, dict) and b.get('type') == 'tool_result' and b.get('is_error'):
796
+ tool = id2tool.get(b.get('tool_use_id'))
797
+ flag = '‼ SUBAGENT ERROR' if is_sub else '‼ tool error'
798
+ events.append((ts, source, 'error', '%s tool=%s error="%s"' % (flag, tool or '?', clip(result_text(b, r), 200))))
799
+ if is_sub:
800
+ n_err += 1
801
+ elif isinstance(content, str) and content.strip():
802
+ events.append((ts, source, 'text', 'user: ' + clip(content, 160)))
803
+ return n_err
804
+
805
+ def timeline(projects_root, sessions_dir, key):
806
+ uuid, parent_path, via = resolve_uuid(projects_root, sessions_dir, key)
807
+ if not uuid:
808
+ sys.stderr.write('-- trailer: key=%s resolved=no reason=%s\n' % (key, via))
809
+ return 1
810
+ events = []
811
+ if parent_path and os.path.exists(parent_path):
812
+ collect_events(parent_path, 'parent', events, False)
813
+ sdir = subagent_dir(projects_root, uuid)
814
+ sub_count = err_count = 0
815
+ if sdir:
816
+ for af in sorted(glob.glob(os.path.join(sdir, 'agent-*.jsonl'))):
817
+ sub_count += 1
818
+ err_count += collect_events(af, agent_type(af) or os.path.basename(af)[:-6], events, True)
819
+ events.sort(key=lambda e: e[0])
820
+ print('# session %s (resolved via %s)' % (uuid, via))
821
+ print('# parent: %s' % (parent_path or '(no parent transcript on disk)'))
822
+ print('# subagents: %d subagent-errors: %d' % (sub_count, err_count))
823
+ print('')
824
+ for ts, source, _kind, text in events:
825
+ print('%s [%s] %s' % (ts or ('-' * 24), source, text))
826
+ sys.stderr.write('-- trailer: key=%s resolved=%s uuid=%s subagents=%d errors=%d\n' % (key, via, uuid, sub_count, err_count))
827
+ return 0
828
+
829
+ def scan(projects_root, limit):
830
+ files = glob.glob(os.path.join(projects_root, '*', '*', 'subagents', 'agent-*.jsonl'))
831
+ if limit > 0:
832
+ files = sorted(files, key=os.path.getmtime, reverse=True)[:limit]
833
+ files = sorted(files)
834
+ findings = []
835
+ for af in files:
836
+ rows = load_jsonl(af)
837
+ id2tool = {}
838
+ for r in rows:
839
+ if not isinstance(r, dict):
840
+ continue
841
+ msg = r.get('message') if isinstance(r.get('message'), dict) else {}
842
+ content = msg.get('content')
843
+ if isinstance(content, list):
844
+ for b in content:
845
+ if isinstance(b, dict) and b.get('type') == 'tool_use' and b.get('id'):
846
+ id2tool[b['id']] = b.get('name')
847
+ errs = []
848
+ for r in rows:
849
+ if not isinstance(r, dict):
850
+ continue
851
+ msg = r.get('message') if isinstance(r.get('message'), dict) else {}
852
+ content = msg.get('content')
853
+ if isinstance(content, list):
854
+ for b in content:
855
+ if isinstance(b, dict) and b.get('type') == 'tool_result' and b.get('is_error'):
856
+ errs.append((id2tool.get(b.get('tool_use_id')), result_text(b, r)))
857
+ if errs:
858
+ parent_uuid = os.path.basename(os.path.dirname(os.path.dirname(af)))
859
+ findings.append((af, agent_type(af), parent_uuid, errs))
860
+ print('# subagent-error scan: %d transcript(s) scanned, %d with errors' % (len(files), len(findings)))
861
+ print('')
862
+ for af, atype, parent_uuid, errs in findings:
863
+ print('‼ %s agentType=%s parent=%s' % (os.path.basename(af), atype or '?', parent_uuid[:8]))
864
+ for tool, etext in errs:
865
+ low = (etext or '').lower()
866
+ extra = ' [exit-127 / command-not-found]' if ('command not found' in low or 'exit code 127' in low) else ''
867
+ print(' tool=%s error="%s"%s' % (tool or '?', clip(etext, 200), extra))
868
+ sys.stderr.write('-- trailer: scan scanned=%d with_errors=%d\n' % (len(files), len(findings)))
869
+ return 0
870
+
871
+ if MODE == 'timeline':
872
+ sys.exit(timeline(sys.argv[2], sys.argv[3], sys.argv[4]))
873
+ elif MODE == 'scan':
874
+ lim = 0
875
+ if len(sys.argv) > 3 and sys.argv[3]:
876
+ try:
877
+ lim = int(sys.argv[3])
878
+ except ValueError:
879
+ lim = 0
880
+ sys.exit(scan(sys.argv[2], lim))
881
+ else:
882
+ sys.stderr.write('jsonl helper: unknown mode %r\n' % MODE)
883
+ sys.exit(2)
884
+ PY
885
+ )
886
+
887
+ # Best-effort missing-on-resolve marker, mirroring per_conversation_mode so the
888
+ # writer-side existence contract still records a <key> miss on server.log.
889
+ emit_missing_on_resolve() {
890
+ local key="$1" reason="$2" ts
891
+ ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
892
+ echo "${ts} [log-tee] missing-on-resolve sessionKey=${key:0:8} surface=logs-read-sh reason=\"${reason}\"" >> "$SERVER_LOG" 2>/dev/null || true
893
+ }
894
+
895
+ # Bare <key> mode: resolve the claude.ai session key against the configDir
896
+ # Claude tree and print a merged parent+subagent timeline with every subagent
897
+ # is_error tool_result flagged inline.
898
+ jsonl_timeline_mode() {
899
+ local key="$1"
900
+ if [[ -z "$key" ]]; then
901
+ echo "Error: sessionKey cannot be empty" >&2
902
+ exit 2
903
+ fi
904
+ if [[ ! "$key" =~ $SESSIONKEY_REGEX ]]; then
905
+ echo "Error: sessionKey contains invalid characters (allowed: a-z, A-Z, 0-9, _, -)" >&2
906
+ exit 2
907
+ fi
908
+ if ! command -v python3 >/dev/null 2>&1; then
909
+ echo "Error: python3 is required to read the JSONL session timeline" >&2
910
+ exit 2
911
+ fi
912
+ if [[ ! -d "$PROJECTS_ROOT" ]]; then
913
+ echo "Error: Claude project tree not found at $PROJECTS_ROOT" >&2
914
+ echo " (set CLAUDE_CONFIG_DIR if this install uses a non-default config dir)" >&2
915
+ emit_missing_on_resolve "$key" "projects-root-missing"
916
+ exit 1
917
+ fi
918
+ local rc=0
919
+ python3 -c "$JSONL_PY" timeline "$PROJECTS_ROOT" "$SESSIONS_DIR" "$key" || rc=$?
920
+ [[ $rc -ne 0 ]] && emit_missing_on_resolve "$key" "jsonl-not-resolved"
921
+ exit $rc
922
+ }
923
+
924
+ # Standing scan: list every subagents/agent-*.jsonl carrying an is_error
925
+ # tool_result, so a silently-failed delivery (e.g. exit-127 / command-not-found
926
+ # on a nonexistent tool name) is discoverable without reproduction. Optional N
927
+ # limits the scan to the N most-recently-modified subagent transcripts.
928
+ scan_subagent_errors_mode() {
929
+ local limit="${1:-}"
930
+ if [[ -n "$limit" && ! "$limit" =~ ^[0-9]+$ ]]; then
931
+ echo "Error: scan limit must be a number, got '$limit'" >&2
932
+ exit 2
933
+ fi
934
+ if ! command -v python3 >/dev/null 2>&1; then
935
+ echo "Error: python3 is required for the subagent-error scan" >&2
936
+ exit 2
937
+ fi
938
+ if [[ ! -d "$PROJECTS_ROOT" ]]; then
939
+ echo "Error: Claude project tree not found at $PROJECTS_ROOT" >&2
940
+ exit 1
941
+ fi
942
+ python3 -c "$JSONL_PY" scan "$PROJECTS_ROOT" "${limit:-}"
943
+ exit $?
944
+ }
945
+
553
946
  if [[ $# -eq 0 ]]; then
554
947
  usage
555
948
  fi
@@ -564,6 +957,10 @@ case "$1" in
564
957
  [[ $# -eq 0 ]] && usage
565
958
  grep_mode "$1" "${2:-}"
566
959
  ;;
960
+ --scan-subagent-errors)
961
+ shift
962
+ scan_subagent_errors_mode "${1:-}"
963
+ ;;
567
964
  --help|-h)
568
965
  usage
569
966
  ;;
@@ -572,6 +969,12 @@ case "$1" in
572
969
  usage
573
970
  ;;
574
971
  *)
575
- per_conversation_mode "$1" "${2:-}"
972
+ # Bare <key> resolves the configDir JSONL timeline (Task 585). An explicit
973
+ # <key> <type> keeps the legacy per-account stream-log read unchanged.
974
+ if [[ -n "${2:-}" ]]; then
975
+ per_conversation_mode "$1" "$2"
976
+ else
977
+ jsonl_timeline_mode "$1"
978
+ fi
576
979
  ;;
577
980
  esac
@@ -44,15 +44,22 @@ mkdir -p "$ACCOUNT_DIR/agents/admin" "$ACCOUNT_DIR/agents/public" "$ACCOUNT_DIR/
44
44
  [ -d "$ACCOUNT_DIR/.git" ] || git init -q "$ACCOUNT_DIR"
45
45
 
46
46
  # ------------------------------------------------------------------
47
- # 2. Write project-level .claude/settings.json (HOOKS ONLY)
47
+ # 2. Write project-level .claude/settings.json (permissions + hooks)
48
48
  # ------------------------------------------------------------------
49
- # Task 453: no `permissions.allow` block. The previous short-prefix
50
- # allowlist (mcp__memory__*, mcp__contacts__*, …) never matched the
51
- # plugin-manifest long-prefix tool names Claude Code actually emits
52
- # (mcp__plugin_memory_memory__…), so every Task-subagent plugin call
53
- # was denied at use-time. Permission enforcement comes from each agent's
54
- # frontmatter `tools:` list (native CC permission surface) and from the
55
- # PreToolUse approval hooks below for review-gated tools.
49
+ # Task 453 removed `permissions.allow` because its short-prefix entries
50
+ # (mcp__memory__*, mcp__contacts__*, …) never matched the plugin-manifest
51
+ # long-prefix tool names Claude Code actually emits (mcp__plugin_memory_memory__…),
52
+ # so every Task-subagent plugin call was denied at use-time.
53
+ #
54
+ # Task 583 reinstates a `permissions` block but as wildcard
55
+ # `allow: ["*"]` plus `defaultMode: "bypassPermissions"`. The wildcard
56
+ # bypasses the prefix-matching defect Task 453 was avoiding (a list of one
57
+ # entry that matches everything cannot mismatch), and `bypassPermissions`
58
+ # short-circuits Stage 1 of the local matcher so claude.ai/code sessions
59
+ # never fall through to the Anthropic remote auto-classifier (which routes
60
+ # Agent dispatch to `ask` and surfaces a permission prompt no unattended
61
+ # operator clicks). Per-agent frontmatter `tools:` lists remain the
62
+ # permission surface for what each subagent is allowed to invoke.
56
63
  #
57
64
  # Hook commands use $PLATFORM_ROOT (set by claude-agent.ts at spawn time)
58
65
  # to locate scripts relative to the platform installation, not the account dir.
@@ -61,6 +68,11 @@ HOOKS_PATH="\$PLATFORM_ROOT/plugins/admin/hooks"
61
68
  WRITER_CRAFT_HOOKS_PATH="\$PLATFORM_ROOT/../premium-plugins/writer-craft/hooks"
62
69
  cat > "$ACCOUNT_SETTINGS" << SETTINGS_EOF
63
70
  {
71
+ "permissions": {
72
+ "defaultMode": "bypassPermissions",
73
+ "allow": ["*"],
74
+ "deny": []
75
+ },
64
76
  "hooks": {
65
77
  "PreToolUse": [
66
78
  {
@@ -99,7 +99,6 @@ export type AppendBlockResult = {
99
99
  reason: 'host-unresolved' | 'identity-unresolved';
100
100
  detail: string;
101
101
  };
102
- export declare const ATTACHMENT_CEILING_BYTES = 31457280;
103
102
  export declare function composeAppendSystemPrompt(host: HostContext, sources: IdentitySources, extras?: ComposeExtras): AppendBlockResult;
104
103
  export type IdentityForbiddenViolation = {
105
104
  class: string;
@@ -1 +1 @@
1
- {"version":3,"file":"system-prompt.d.ts","sourceRoot":"","sources":["../src/system-prompt.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"system-prompt.d.ts","sourceRoot":"","sources":["../src/system-prompt.ts"],"names":[],"mappings":"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"}
@@ -4,10 +4,12 @@
4
4
  //
5
5
  // <host> ... </host> — hostname, LAN IPv4, admin URL,
6
6
  // tunnel URL (or "none")
7
- // <attachment-ceiling> ... </attachment-ceiling>
8
- // — 30 MiB SendUserFile ceiling and
9
- // the tunnel-link workaround for
10
- // oversize files
7
+ // <file-delivery> ... </file-delivery>
8
+ // — how a produced file reaches the
9
+ // operator: write under output/,
10
+ // then state its location (Artefacts
11
+ // panel / operator file share). There
12
+ // is no send-to-chat tool.
11
13
  // <identity> ... </identity> — verbatim contents of
12
14
  // <accountDir>/agents/<role>/IDENTITY.md
13
15
  // <soul> ... </soul> — verbatim contents of
@@ -57,7 +59,6 @@
57
59
  // inline-coded so an operator can grep for it.
58
60
  import { readFileSync } from 'node:fs';
59
61
  import { join } from 'node:path';
60
- export const ATTACHMENT_CEILING_BYTES = 31_457_280; // 30 MiB, mirrors the SDK uploader
61
62
  function readIdentityFile(path) {
62
63
  let content;
63
64
  try {
@@ -152,9 +153,9 @@ function renderAppendBlock(host, identityContent, soulContent, extras) {
152
153
  `admin-url: ${host.adminUrl}`,
153
154
  `tunnel-url: ${tunnelLine}`,
154
155
  '</host>',
155
- '<attachment-ceiling>',
156
- 'SendUserFile and any inline attachment surface accept at most 30 MiB per file. Never call SendUserFile with a file whose on-disk size exceeds 30 MiB; there is no oversize delivery route, so tell the operator the file is too large to deliver and offer to split or compress it under the ceiling. The tool returns "N files delivered to user" even when the byte upload silently fails, so that string is not evidence of delivery the operator\'s visible download chip is.',
157
- '</attachment-ceiling>',
156
+ '<file-delivery>',
157
+ 'Files you produce are not auto-delivered into the conversation there is no working send-to-chat tool on this surface, and a bare on-disk path is unreachable from the chat. To deliver a file: write it under the output/ directory in your working directory (any depth), then in your reply tell the operator its exact location — it appears in the Artefacts panel (open the maxy sidebar to download it) and lives at the full output/ path, reachable on the operator file share (e.g. Samba) if one is set up. Always state the complete path, never a vague "in the output directory".',
158
+ '</file-delivery>',
158
159
  '<identity>',
159
160
  identityContent,
160
161
  '</identity>',
@@ -382,8 +383,8 @@ export function runSystemPromptSelfTest() {
382
383
  const required = [
383
384
  '<host>',
384
385
  '</host>',
385
- '<attachment-ceiling>',
386
- '</attachment-ceiling>',
386
+ '<file-delivery>',
387
+ '</file-delivery>',
387
388
  '<identity>',
388
389
  '</identity>',
389
390
  '<soul>',