@rubytech/create-maxy-code 0.1.214 → 0.1.219
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.
- package/package.json +1 -1
- package/payload/platform/lib/mcp-eager/dist/index.d.ts +0 -55
- package/payload/platform/lib/mcp-eager/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/mcp-eager/dist/index.js +0 -76
- package/payload/platform/lib/mcp-eager/dist/index.js.map +1 -1
- package/payload/platform/lib/mcp-eager/src/index.ts +0 -101
- package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +4 -1
- package/payload/platform/plugins/admin/hooks/__tests__/hook-emit-stale-lock-ttl.test.sh +102 -0
- package/payload/platform/plugins/admin/hooks/__tests__/post-tool-use-agent.test.sh +97 -1
- package/payload/platform/plugins/admin/hooks/__tests__/pre-tool-use-admin-tool-gate.test.sh +55 -74
- package/payload/platform/plugins/admin/hooks/lib/hook-emit.sh +58 -0
- package/payload/platform/plugins/admin/hooks/post-tool-use-agent.sh +100 -29
- package/payload/platform/plugins/admin/hooks/pre-tool-use.sh +0 -55
- package/payload/platform/plugins/admin/mcp/dist/__tests__/capabilities-here.test.d.ts +2 -0
- package/payload/platform/plugins/admin/mcp/dist/__tests__/capabilities-here.test.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/__tests__/capabilities-here.test.js +130 -0
- package/payload/platform/plugins/admin/mcp/dist/__tests__/capabilities-here.test.js.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js +63 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/lib/catalogue-census.d.ts +14 -0
- package/payload/platform/plugins/admin/mcp/dist/lib/catalogue-census.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/lib/catalogue-census.js +50 -0
- package/payload/platform/plugins/admin/mcp/dist/lib/catalogue-census.js.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/capabilities-here.d.ts +28 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/capabilities-here.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/capabilities-here.js +68 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/capabilities-here.js.map +1 -0
- package/payload/platform/plugins/admin/skills/capabilities-here/SKILL.md +31 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +39 -30
- package/payload/platform/plugins/browser/mcp/dist/index.js +19 -19
- package/payload/platform/plugins/browser/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/docs/references/platform.md +28 -25
- package/payload/platform/plugins/docs/references/plugins-guide.md +2 -0
- package/payload/platform/plugins/email/mcp/dist/index.js +12 -12
- package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/index.js +8 -8
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/url-get/mcp/dist/index.js +2 -2
- package/payload/platform/plugins/url-get/mcp/dist/index.js.map +1 -1
- package/payload/platform/scripts/check-architecture-skill-no-drift.mjs +89 -42
- package/payload/platform/scripts/check-roles-doc-completeness.mjs +96 -0
- package/payload/platform/scripts/check-specialist-tool-surface.mjs +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +1 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +0 -14
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +54 -21
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +20 -9
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.d.ts +21 -0
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js +118 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts +28 -17
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +110 -60
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-life.d.ts +4 -0
- package/payload/platform/services/claude-session-manager/dist/rc-life.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/rc-life.js +16 -0
- package/payload/platform/services/claude-session-manager/dist/rc-life.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/rc-script-spawn.d.ts +80 -0
- package/payload/platform/services/claude-session-manager/dist/rc-script-spawn.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/rc-script-spawn.js +306 -0
- package/payload/platform/services/claude-session-manager/dist/rc-script-spawn.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/specialist-drift.d.ts +9 -0
- package/payload/platform/services/claude-session-manager/dist/specialist-drift.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/specialist-drift.js +6 -2
- package/payload/platform/services/claude-session-manager/dist/specialist-drift.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/systemd-scope.d.ts +12 -46
- package/payload/platform/services/claude-session-manager/dist/systemd-scope.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/systemd-scope.js +24 -73
- package/payload/platform/services/claude-session-manager/dist/systemd-scope.js.map +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +6 -4
- package/payload/platform/templates/specialists/agents/content-producer.md +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/index.d.ts.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/index.js +4 -5
- package/payload/premium-plugins/writer-craft/mcp/dist/index.js.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/src/index.ts +4 -5
- package/payload/premium-plugins/writer-craft/lib/mcp-eager/dist/index.d.ts +0 -116
- package/payload/premium-plugins/writer-craft/lib/mcp-eager/dist/index.d.ts.map +0 -1
- package/payload/premium-plugins/writer-craft/lib/mcp-eager/dist/index.js +0 -125
- package/payload/premium-plugins/writer-craft/lib/mcp-eager/dist/index.js.map +0 -1
- package/payload/premium-plugins/writer-craft/lib/mcp-eager/package.json +0 -7
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Task
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
2
|
+
# Task 571 — the orchestration-only block has been removed. Admin's standard
|
|
3
|
+
# tool surface (Write/Edit/MultiEdit/Bash/WebFetch/WebSearch) is callable
|
|
4
|
+
# directly from the admin seat; delegation is a doctrine bias enforced in
|
|
5
|
+
# IDENTITY.md, not a platform refusal. This test pins the post-571 contract:
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
7
|
+
# - Admin-direct Bash/Edit/Write/WebFetch all return exit 0 with no
|
|
8
|
+
# `reason=orchestration-only` or `reason=specialist-owned` line.
|
|
9
|
+
# - The resource-specific hook blocks (code-dir, entitlement, account.json,
|
|
10
|
+
# pending-actions, base64-guard, approval-queue) are unaffected and live
|
|
11
|
+
# in their own dedicated tests.
|
|
12
|
+
#
|
|
13
|
+
# Reintroducing the orchestration-only block requires a doctrine flip — this
|
|
14
|
+
# test fails first and surfaces the change loudly.
|
|
12
15
|
|
|
13
16
|
set -u
|
|
14
17
|
|
|
@@ -30,97 +33,75 @@ PASS=0; FAIL=0
|
|
|
30
33
|
pass() { PASS=$((PASS+1)); echo "PASS: $1"; }
|
|
31
34
|
fail() { FAIL=$((FAIL+1)); echo "FAIL: $1" >&2; }
|
|
32
35
|
|
|
33
|
-
# The hook receives the parent session's transcript_path even on subagent
|
|
34
|
-
# calls in Claude Code 2.1.x. The fixture mirrors production.
|
|
35
36
|
SESSION_TRANSCRIPT="/tmp/sess/44283ae1-2b82-4bd7-b8af-99a11141ed4b.jsonl"
|
|
36
|
-
SUBAGENT_PARENT_ID="toolu_01QFC4W32wFUyHy6WJMNjHf6"
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
# run_admin_direct <tool> <expected_rc> <label>
|
|
39
|
+
# Pipes a minimal PreToolUse payload to the hook with MAXY_SESSION_ROLE=admin
|
|
40
|
+
# (the realistic admin spawn shape) and asserts the rc and that no
|
|
41
|
+
# orchestration-only / specialist-owned refusal line appears on stderr.
|
|
42
|
+
run_admin_direct() {
|
|
43
|
+
local tool="$1" expected_rc="$2" label="$3"
|
|
40
44
|
local input_json
|
|
41
45
|
input_json=$(python3 -c '
|
|
42
46
|
import json, sys
|
|
43
|
-
|
|
47
|
+
print(json.dumps({
|
|
44
48
|
"hook_event_name": "PreToolUse",
|
|
45
49
|
"tool_name": sys.argv[1],
|
|
46
|
-
"tool_input": {"file_path": "/tmp/x", "content": "x", "command": "echo hi"},
|
|
50
|
+
"tool_input": {"file_path": "/tmp/x", "content": "x", "command": "echo hi", "url": "https://example.com"},
|
|
47
51
|
"transcript_path": sys.argv[2],
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if parent:
|
|
51
|
-
payload["parent_tool_use_id"] = parent
|
|
52
|
-
print(json.dumps(payload, separators=(",", ":")))
|
|
53
|
-
' "$tool" "$SESSION_TRANSCRIPT" "$parent_id")
|
|
52
|
+
}, separators=(",", ":")))
|
|
53
|
+
' "$tool" "$SESSION_TRANSCRIPT")
|
|
54
54
|
local stdout_file; stdout_file=$(mktemp); TMPFILES+=("$stdout_file")
|
|
55
55
|
local stderr_file; stderr_file=$(mktemp); TMPFILES+=("$stderr_file")
|
|
56
|
-
printf '%s' "$input_json" |
|
|
56
|
+
printf '%s' "$input_json" | \
|
|
57
|
+
env -u MAXY_SPECIALIST MAXY_SESSION_ROLE=admin \
|
|
58
|
+
bash "$HOOK" admin >"$stdout_file" 2>"$stderr_file"
|
|
57
59
|
local rc=$?
|
|
58
60
|
if [[ "$rc" -ne "$expected_rc" ]]; then
|
|
59
61
|
fail "$label: expected exit $expected_rc, got $rc. Stderr: $(cat "$stderr_file")"
|
|
60
62
|
return
|
|
61
63
|
fi
|
|
62
|
-
if
|
|
63
|
-
fail "$label: stderr
|
|
64
|
+
if grep -qE 'reason=orchestration-only|reason=specialist-owned|reason=missing-marker' "$stderr_file"; then
|
|
65
|
+
fail "$label: stderr carries a removed-gate reason line. Got: $(cat "$stderr_file")"
|
|
64
66
|
return
|
|
65
67
|
fi
|
|
66
68
|
pass "$label"
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
# ── Admin-direct
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"Test 4: admin-direct Bash rejected"
|
|
78
|
-
run WebFetch "" 2 '\[admin-tool-gate\] role=admin .*tool=WebFetch decision=block.*owner=research-assistant' \
|
|
79
|
-
"Test 5: admin-direct WebFetch rejected"
|
|
80
|
-
run WebSearch "" 2 '\[admin-tool-gate\] role=admin .*tool=WebSearch decision=block.*owner=research-assistant' \
|
|
81
|
-
"Test 6: admin-direct WebSearch rejected"
|
|
82
|
-
|
|
83
|
-
# ── In-window subagent passthrough (parent_tool_use_id non-empty) ───────
|
|
84
|
-
run Write "$SUBAGENT_PARENT_ID" 0 "\[admin-tool-gate\] role=subagent parent_tool_use_id=${SUBAGENT_PARENT_ID} .*tool=Write decision=allow" \
|
|
85
|
-
"Test 7: subagent Write passes through"
|
|
86
|
-
run Edit "$SUBAGENT_PARENT_ID" 0 '\[admin-tool-gate\] role=subagent .*tool=Edit decision=allow' \
|
|
87
|
-
"Test 8: subagent Edit passes through"
|
|
88
|
-
run Bash "$SUBAGENT_PARENT_ID" 0 '\[admin-tool-gate\] role=subagent .*tool=Bash decision=allow' \
|
|
89
|
-
"Test 9: subagent Bash passes through (Task 559 regression — coding-assistant git pull)"
|
|
90
|
-
run WebFetch "$SUBAGENT_PARENT_ID" 0 '\[admin-tool-gate\] role=subagent .*tool=WebFetch decision=allow' \
|
|
91
|
-
"Test 10: subagent WebFetch passes through"
|
|
92
|
-
run MultiEdit "$SUBAGENT_PARENT_ID" 0 '\[admin-tool-gate\] role=subagent .*tool=MultiEdit decision=allow' \
|
|
93
|
-
"Test 11: subagent MultiEdit passes through"
|
|
71
|
+
# ── Admin-direct passthrough (the Task 571 regression boundary) ──────────
|
|
72
|
+
run_admin_direct Bash 0 "Test 1: admin-direct Bash passes through"
|
|
73
|
+
run_admin_direct Edit 0 "Test 2: admin-direct Edit passes through (path /tmp/x is outside protected dirs)"
|
|
74
|
+
run_admin_direct Write 0 "Test 3: admin-direct Write passes through"
|
|
75
|
+
run_admin_direct MultiEdit 0 "Test 4: admin-direct MultiEdit passes through"
|
|
76
|
+
run_admin_direct WebFetch 0 "Test 5: admin-direct WebFetch passes through"
|
|
77
|
+
run_admin_direct WebSearch 0 "Test 6: admin-direct WebSearch passes through"
|
|
78
|
+
run_admin_direct Read 0 "Test 7: admin-direct Read passes through"
|
|
94
79
|
|
|
95
|
-
# ──
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
# ── Task 560: admin-direct block emits propagation record ──────────────
|
|
100
|
-
# Uses the Task 559 classifier (no parent_tool_use_id → admin-direct).
|
|
101
|
-
PROP_TMPDIR=$(mktemp -d); TMPFILES+=("$PROP_TMPDIR")
|
|
102
|
-
export MAXY_HOOK_BUFFER_DIR="$PROP_TMPDIR/buffers"
|
|
103
|
-
export MAXY_HOOK_SERVER_LOG="$PROP_TMPDIR/server.log"
|
|
104
|
-
SESSION="task560-test-sess"
|
|
105
|
-
PROP_INPUT=$(python3 -c '
|
|
80
|
+
# ── Resource-specific blocks unaffected: protected code-dir Write still refuses ──
|
|
81
|
+
# Verifies the deletion was surgical — code-dir-protection-ui still fires.
|
|
82
|
+
PROTECTED_INPUT=$(python3 -c '
|
|
106
83
|
import json, sys
|
|
107
|
-
print(json.dumps({
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
84
|
+
print(json.dumps({
|
|
85
|
+
"hook_event_name": "PreToolUse",
|
|
86
|
+
"tool_name": "Write",
|
|
87
|
+
"tool_input": {"file_path": "/srv/maxy/platform/ui/app/page.tsx", "content": "x"},
|
|
88
|
+
"transcript_path": sys.argv[1],
|
|
89
|
+
}, separators=(",", ":")))
|
|
90
|
+
' "$SESSION_TRANSCRIPT")
|
|
91
|
+
PROT_STDOUT=$(mktemp); TMPFILES+=("$PROT_STDOUT")
|
|
92
|
+
PROT_STDERR=$(mktemp); TMPFILES+=("$PROT_STDERR")
|
|
93
|
+
printf '%s' "$PROTECTED_INPUT" | \
|
|
94
|
+
env -u MAXY_SPECIALIST MAXY_SESSION_ROLE=admin \
|
|
95
|
+
bash "$HOOK" admin >"$PROT_STDOUT" 2>"$PROT_STDERR"
|
|
96
|
+
PROT_RC=$?
|
|
97
|
+
if [[ "$PROT_RC" -eq 2 ]] && grep -q 'cannot modify platform UI source' "$PROT_STDERR"; then
|
|
98
|
+
pass "Test 8: admin-direct Write to platform/ui/ still blocked (resource-specific block intact)"
|
|
117
99
|
else
|
|
118
|
-
fail "Test
|
|
100
|
+
fail "Test 8: expected platform/ui code-dir block, got rc=$PROT_RC stdout=$(cat "$PROT_STDOUT") stderr=$(cat "$PROT_STDERR")"
|
|
119
101
|
fi
|
|
120
|
-
unset MAXY_HOOK_BUFFER_DIR MAXY_HOOK_SERVER_LOG
|
|
121
102
|
|
|
122
103
|
echo
|
|
123
|
-
echo "──────── pre-tool-use admin-tool-gate test summary ────────"
|
|
104
|
+
echo "──────── pre-tool-use admin-tool-gate test summary (post-Task 571) ────────"
|
|
124
105
|
echo "PASS: $PASS"
|
|
125
106
|
echo "FAIL: $FAIL"
|
|
126
107
|
|
|
@@ -9,6 +9,55 @@
|
|
|
9
9
|
: "${MAXY_HOOK_BUFFER_DIR:=$HOME/.maxy-code/logs/hook-decisions}"
|
|
10
10
|
: "${MAXY_HOOK_SERVER_LOG:=$HOME/.maxy-code/logs/server.log}"
|
|
11
11
|
: "${MAXY_HOOK_STDERR_CAP:=4096}"
|
|
12
|
+
# Task 563 — mkdir-based mutex serialises emitter append vs drainer rename.
|
|
13
|
+
# 100 attempts × 10ms = ~1s ceiling. Portable across Linux (Pi) and macOS
|
|
14
|
+
# (tests) without flock(1).
|
|
15
|
+
: "${MAXY_HOOK_LOCK_ATTEMPTS:=100}"
|
|
16
|
+
: "${MAXY_HOOK_LOCK_SLEEP:=0.01}"
|
|
17
|
+
# Task 569 — TTL for orphaned lockdirs left behind when a holder dies
|
|
18
|
+
# between `mkdir lock` and `rmdir lock` (SIGKILL, OOM, systemd restart).
|
|
19
|
+
# Default 5s is far longer than the legitimate hold (microseconds for a
|
|
20
|
+
# rename or append) and shorter than operator-noticeable propagation lag.
|
|
21
|
+
: "${MAXY_HOOK_LOCK_TTL_SECONDS:=5}"
|
|
22
|
+
|
|
23
|
+
# _hook_lockdir_age_seconds <lockdir> — prints integer age in seconds, or
|
|
24
|
+
# -1 if the lockdir cannot be stat'd. Portable across Linux (`stat -c`)
|
|
25
|
+
# and macOS (`stat -f`).
|
|
26
|
+
_hook_lockdir_age_seconds() {
|
|
27
|
+
local lockdir="$1" mtime now
|
|
28
|
+
mtime=$(stat -c %Y "$lockdir" 2>/dev/null || stat -f %m "$lockdir" 2>/dev/null)
|
|
29
|
+
[[ -z "$mtime" ]] && { echo -1; return; }
|
|
30
|
+
now=$(date +%s)
|
|
31
|
+
echo $((now - mtime))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# _hook_acquire_lock <lockdir> — returns 0 on success, 1 on timeout.
|
|
35
|
+
# On each mkdir failure, if the lockdir is older than
|
|
36
|
+
# MAXY_HOOK_LOCK_TTL_SECONDS, the orphaned holder is presumed dead: the
|
|
37
|
+
# lockdir is force-removed, a stale-lock-reclaimed observability line is
|
|
38
|
+
# emitted, and one mkdir retry is attempted before resuming the spin.
|
|
39
|
+
_hook_acquire_lock() {
|
|
40
|
+
local lockdir="$1" i=0 age
|
|
41
|
+
while ! mkdir "$lockdir" 2>/dev/null; do
|
|
42
|
+
age=$(_hook_lockdir_age_seconds "$lockdir")
|
|
43
|
+
if [[ "$age" -ge "$MAXY_HOOK_LOCK_TTL_SECONDS" ]]; then
|
|
44
|
+
rmdir "$lockdir" 2>/dev/null || rm -rf "$lockdir" 2>/dev/null
|
|
45
|
+
mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
|
|
46
|
+
printf '[hook-propagate-error] reason=stale-lock-reclaimed lockdir=%s ageSeconds=%s\n' \
|
|
47
|
+
"$lockdir" "$age" >>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null || true
|
|
48
|
+
mkdir "$lockdir" 2>/dev/null && return 0
|
|
49
|
+
fi
|
|
50
|
+
i=$((i+1))
|
|
51
|
+
[[ "$i" -ge "$MAXY_HOOK_LOCK_ATTEMPTS" ]] && return 1
|
|
52
|
+
sleep "$MAXY_HOOK_LOCK_SLEEP" 2>/dev/null || true
|
|
53
|
+
done
|
|
54
|
+
return 0
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# _hook_release_lock <lockdir>
|
|
58
|
+
_hook_release_lock() {
|
|
59
|
+
rmdir "$1" 2>/dev/null || true
|
|
60
|
+
}
|
|
12
61
|
|
|
13
62
|
# hook_emit_decision <agentId> <hookName> <toolName> <decision> <reason>
|
|
14
63
|
# <stderrPayload> <exitCode> <durationMs>
|
|
@@ -52,12 +101,21 @@ PY
|
|
|
52
101
|
|
|
53
102
|
mkdir -p "$MAXY_HOOK_BUFFER_DIR" 2>/dev/null
|
|
54
103
|
local buf="$MAXY_HOOK_BUFFER_DIR/$agentId.jsonl"
|
|
104
|
+
local lock="${buf}.lock"
|
|
105
|
+
if ! _hook_acquire_lock "$lock"; then
|
|
106
|
+
mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
|
|
107
|
+
printf '[hook-propagate-error] reason=lock-timeout agentId=%s hook=%s\n' \
|
|
108
|
+
"$agentId" "$hookName" >>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null
|
|
109
|
+
return 0
|
|
110
|
+
fi
|
|
55
111
|
if ! printf '%s\n' "$rec" >>"$buf" 2>/dev/null; then
|
|
112
|
+
_hook_release_lock "$lock"
|
|
56
113
|
mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
|
|
57
114
|
printf '[hook-propagate-error] reason=buffer-write agentId=%s hook=%s\n' \
|
|
58
115
|
"$agentId" "$hookName" >>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null
|
|
59
116
|
return 0
|
|
60
117
|
fi
|
|
118
|
+
_hook_release_lock "$lock"
|
|
61
119
|
|
|
62
120
|
local bytesIn="${#stderrPayload}"
|
|
63
121
|
local bytesOut="${#rec}"
|
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Task 560 — PostToolUse hook scoped to tool_name=Agent in the admin role.
|
|
3
|
-
# Drains
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
3
|
+
# Drains every subagent hook-decision buffer in the dir, prints each record
|
|
4
|
+
# as a [hook-propagate] line to stdout (Claude Code attaches the stdout to
|
|
5
|
+
# the parent JSONL as a hook_success attachment), and emits one
|
|
6
|
+
# [hook-propagate-census] line. N != M in the census is the propagation
|
|
7
|
+
# regression signal.
|
|
8
|
+
#
|
|
9
|
+
# Task 563 — concurrency hardening:
|
|
10
|
+
# - Atomic claim before read. Each buffer is renamed under a mkdir mutex
|
|
11
|
+
# to a per-fire `claimed/<basename>-<epoch>-<pid>.jsonl` path. The lock
|
|
12
|
+
# is held only for the rename syscall; reading happens against the
|
|
13
|
+
# renamed inode, which no emitter can address. The race window between
|
|
14
|
+
# the drainer's read and the buffer rotation is closed.
|
|
15
|
+
# - No cursor. The cursor file was load-bearing only because consumed
|
|
16
|
+
# buffers could be re-seen; with atomic claim the file's presence in
|
|
17
|
+
# the directory is the unambiguous signal. Dropping the cursor closes
|
|
18
|
+
# two further races (concurrent-drainer cursor write, shared
|
|
19
|
+
# `.unknown.cursor` across broken-stdin fires).
|
|
20
|
+
# - Unknown-parent observability. When stdin's `session_id` is missing
|
|
21
|
+
# or unparseable, an explicit `[hook-propagate] parentSession=unknown
|
|
22
|
+
# reason=session-id-missing` line is emitted to both stdout and
|
|
23
|
+
# server.log so the broken-stdin path is no longer silent.
|
|
8
24
|
|
|
9
25
|
set -uo pipefail
|
|
10
26
|
|
|
11
27
|
: "${MAXY_HOOK_BUFFER_DIR:=$HOME/.maxy-code/logs/hook-decisions}"
|
|
12
28
|
: "${MAXY_HOOK_SERVER_LOG:=$HOME/.maxy-code/logs/server.log}"
|
|
29
|
+
: "${MAXY_HOOK_LOCK_ATTEMPTS:=100}"
|
|
30
|
+
: "${MAXY_HOOK_LOCK_SLEEP:=0.01}"
|
|
31
|
+
# Task 569 — TTL for orphaned lockdirs (see hook-emit.sh for rationale).
|
|
32
|
+
: "${MAXY_HOOK_LOCK_TTL_SECONDS:=5}"
|
|
13
33
|
|
|
14
34
|
if [ -t 0 ]; then
|
|
15
35
|
# No stdin — no correlation possible. Fail-open.
|
|
@@ -19,61 +39,112 @@ INPUT=$(cat)
|
|
|
19
39
|
PARENT_SESSION=$(printf '%s' "$INPUT" | python3 -c '
|
|
20
40
|
import sys, json
|
|
21
41
|
try:
|
|
22
|
-
|
|
42
|
+
sid = json.load(sys.stdin).get("session_id") or ""
|
|
43
|
+
print(sid if sid else "unknown")
|
|
23
44
|
except Exception:
|
|
24
45
|
print("unknown")
|
|
25
46
|
' 2>/dev/null || echo "unknown")
|
|
26
47
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
48
|
+
if [[ "$PARENT_SESSION" == "unknown" ]]; then
|
|
49
|
+
# Conservative play (Task 563 brief option C): emit observability and
|
|
50
|
+
# exit without draining. A broken-stdin fire has no business processing
|
|
51
|
+
# buffers — the next valid parent fire will pick them up.
|
|
52
|
+
mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
|
|
53
|
+
printf '[hook-propagate] parentSession=unknown reason=session-id-missing\n'
|
|
54
|
+
printf '[hook-propagate] parentSession=unknown reason=session-id-missing\n' \
|
|
55
|
+
>>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null || true
|
|
56
|
+
exit 0
|
|
34
57
|
fi
|
|
35
|
-
NOW_EPOCH=$(date +%s)
|
|
36
58
|
|
|
59
|
+
mkdir -p "$MAXY_HOOK_BUFFER_DIR/claimed" "$MAXY_HOOK_BUFFER_DIR/consumed" 2>/dev/null
|
|
60
|
+
|
|
61
|
+
# Sweep orphaned `.<session>.cursor` files left behind by an older
|
|
62
|
+
# version of this script. The cursor mechanism is gone; without this
|
|
63
|
+
# they would persist forever as dir noise.
|
|
64
|
+
rm -f "$MAXY_HOOK_BUFFER_DIR"/.*.cursor 2>/dev/null || true
|
|
65
|
+
|
|
66
|
+
NOW_EPOCH=$(date +%s)
|
|
37
67
|
N_OBSERVED=0
|
|
38
68
|
N_EMITTED=0
|
|
69
|
+
|
|
70
|
+
# Task 569 — same stale-lockdir TTL reclamation as hook-emit.sh.
|
|
71
|
+
_drain_lockdir_age_seconds() {
|
|
72
|
+
local lockdir="$1" mtime now
|
|
73
|
+
mtime=$(stat -c %Y "$lockdir" 2>/dev/null || stat -f %m "$lockdir" 2>/dev/null)
|
|
74
|
+
[[ -z "$mtime" ]] && { echo -1; return; }
|
|
75
|
+
now=$(date +%s)
|
|
76
|
+
echo $((now - mtime))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_drain_acquire_lock() {
|
|
80
|
+
local lockdir="$1" i=0 age
|
|
81
|
+
while ! mkdir "$lockdir" 2>/dev/null; do
|
|
82
|
+
age=$(_drain_lockdir_age_seconds "$lockdir")
|
|
83
|
+
if [[ "$age" -ge "$MAXY_HOOK_LOCK_TTL_SECONDS" ]]; then
|
|
84
|
+
rmdir "$lockdir" 2>/dev/null || rm -rf "$lockdir" 2>/dev/null
|
|
85
|
+
mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
|
|
86
|
+
printf '[hook-propagate-error] reason=stale-lock-reclaimed lockdir=%s ageSeconds=%s\n' \
|
|
87
|
+
"$lockdir" "$age" >>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null || true
|
|
88
|
+
mkdir "$lockdir" 2>/dev/null && return 0
|
|
89
|
+
fi
|
|
90
|
+
i=$((i+1))
|
|
91
|
+
[[ "$i" -ge "$MAXY_HOOK_LOCK_ATTEMPTS" ]] && return 1
|
|
92
|
+
sleep "$MAXY_HOOK_LOCK_SLEEP" 2>/dev/null || true
|
|
93
|
+
done
|
|
94
|
+
return 0
|
|
95
|
+
}
|
|
96
|
+
|
|
39
97
|
shopt -s nullglob
|
|
40
98
|
for buf in "$MAXY_HOOK_BUFFER_DIR"/*.jsonl; do
|
|
41
99
|
[[ -f "$buf" ]] || continue
|
|
42
100
|
case "$(basename "$buf")" in
|
|
43
101
|
.*) continue;;
|
|
44
102
|
esac
|
|
45
|
-
|
|
46
|
-
if
|
|
103
|
+
lock="${buf}.lock"
|
|
104
|
+
if ! _drain_acquire_lock "$lock"; then
|
|
105
|
+
mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
|
|
106
|
+
printf '[hook-propagate-error] reason=lock-timeout parentSession=%s buffer=%s\n' \
|
|
107
|
+
"$PARENT_SESSION" "$(basename "$buf")" >>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null || true
|
|
108
|
+
continue
|
|
109
|
+
fi
|
|
110
|
+
# Re-check existence under lock — a peer drainer may have claimed it
|
|
111
|
+
# between our `for` iteration and our lock acquisition.
|
|
112
|
+
if [[ ! -f "$buf" ]]; then
|
|
113
|
+
rmdir "$lock" 2>/dev/null || true
|
|
114
|
+
continue
|
|
115
|
+
fi
|
|
116
|
+
CLAIMED="$MAXY_HOOK_BUFFER_DIR/claimed/$(basename "$buf" .jsonl)-${NOW_EPOCH}-$$.jsonl"
|
|
117
|
+
if ! mv "$buf" "$CLAIMED" 2>/dev/null; then
|
|
118
|
+
rmdir "$lock" 2>/dev/null || true
|
|
119
|
+
mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
|
|
120
|
+
printf '[hook-propagate-error] reason=claim-mv parentSession=%s buffer=%s\n' \
|
|
121
|
+
"$PARENT_SESSION" "$(basename "$buf")" >>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null || true
|
|
47
122
|
continue
|
|
48
123
|
fi
|
|
49
|
-
|
|
50
|
-
|
|
124
|
+
rmdir "$lock" 2>/dev/null || true
|
|
125
|
+
|
|
126
|
+
# Ground-truth observed count: non-empty lines in the claimed file. The
|
|
127
|
+
# printf-success N_EMITTED is incremented independently below, so a
|
|
51
128
|
# printf failure (SIGPIPE on a closed downstream pipe, EIO on stdout)
|
|
52
129
|
# produces N_OBSERVED > N_EMITTED — the propagation regression signal
|
|
53
130
|
# the census promises. The two counters must not be bumped together.
|
|
54
|
-
BUF_LINES=$(grep -c . "$
|
|
131
|
+
BUF_LINES=$(grep -c . "$CLAIMED" 2>/dev/null || echo 0)
|
|
55
132
|
N_OBSERVED=$((N_OBSERVED + BUF_LINES))
|
|
56
133
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
57
134
|
[[ -z "$line" ]] && continue
|
|
58
135
|
if printf '[hook-propagate] %s\n' "$line" 2>/dev/null; then
|
|
59
136
|
N_EMITTED=$((N_EMITTED+1))
|
|
60
137
|
fi
|
|
61
|
-
done <"$
|
|
62
|
-
TARGET="$MAXY_HOOK_BUFFER_DIR/consumed/$(basename "$
|
|
63
|
-
if ! mv "$
|
|
138
|
+
done <"$CLAIMED"
|
|
139
|
+
TARGET="$MAXY_HOOK_BUFFER_DIR/consumed/$(basename "$CLAIMED")"
|
|
140
|
+
if ! mv "$CLAIMED" "$TARGET" 2>/dev/null; then
|
|
64
141
|
mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
|
|
65
142
|
printf '[hook-propagate-error] reason=consumed-mv parentSession=%s buffer=%s\n' \
|
|
66
|
-
"$PARENT_SESSION" "$(basename "$
|
|
143
|
+
"$PARENT_SESSION" "$(basename "$CLAIMED")" >>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null || true
|
|
67
144
|
fi
|
|
68
145
|
done
|
|
69
146
|
shopt -u nullglob
|
|
70
147
|
|
|
71
|
-
if ! printf '%s' "$NOW_EPOCH" >"$CURSOR_FILE" 2>/dev/null; then
|
|
72
|
-
mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
|
|
73
|
-
printf '[hook-propagate-error] reason=cursor-write parentSession=%s cursor=%s\n' \
|
|
74
|
-
"$PARENT_SESSION" "$CURSOR_FILE" >>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null || true
|
|
75
|
-
fi
|
|
76
|
-
|
|
77
148
|
printf '[hook-propagate-census] parentSession=%s subagentHooksObserved=%d attachmentsEmitted=%d\n' \
|
|
78
149
|
"$PARENT_SESSION" "$N_OBSERVED" "$N_EMITTED"
|
|
79
150
|
|
|
@@ -43,61 +43,6 @@ except Exception:
|
|
|
43
43
|
# ---------------------------------------------------------------------------
|
|
44
44
|
if [ "$AGENT_TYPE" = "admin" ]; then
|
|
45
45
|
|
|
46
|
-
# ── Orchestration-only authoring + research block (Task 529, 559) ────────
|
|
47
|
-
# The admin seat is orchestration + clarification + delivery only.
|
|
48
|
-
# Write/Edit/MultiEdit/Bash/WebFetch/WebSearch are specialist-owned —
|
|
49
|
-
# content-producer for authoring, research-assistant for research, the
|
|
50
|
-
# shell tools for whichever specialist owns the workflow that needs them.
|
|
51
|
-
# In-window Task subagents must keep these tools (Task 222 regression
|
|
52
|
-
# class).
|
|
53
|
-
#
|
|
54
|
-
# Discriminator (Task 559): Claude Code passes `parent_tool_use_id` in
|
|
55
|
-
# the hook input JSON when the call originates from a subagent (the id
|
|
56
|
-
# of the parent's Task tool_use). Admin-direct calls have it absent or
|
|
57
|
-
# empty. Earlier versions used the `transcript_path` glob, but Claude
|
|
58
|
-
# Code 2.1.x passes the parent session's transcript_path to subagent
|
|
59
|
-
# hooks, so the glob never matched and every specialist subagent Bash
|
|
60
|
-
# was blocked with the admin orchestration-only wording.
|
|
61
|
-
#
|
|
62
|
-
# Admin direct (no parent_tool_use_id) → exit 2 with dispatch hint.
|
|
63
|
-
# Subagent (parent_tool_use_id non-empty) → exit 0 (passthrough).
|
|
64
|
-
case "$TOOL_NAME" in
|
|
65
|
-
Write|Edit|MultiEdit|Bash|WebFetch|WebSearch)
|
|
66
|
-
PARENT_TOOL_USE_ID=$(echo "$INPUT" | python3 -c '
|
|
67
|
-
import sys, json
|
|
68
|
-
try:
|
|
69
|
-
v = json.load(sys.stdin).get("parent_tool_use_id", "")
|
|
70
|
-
print(v if isinstance(v, str) else "")
|
|
71
|
-
except Exception:
|
|
72
|
-
print("")
|
|
73
|
-
' 2>/dev/null)
|
|
74
|
-
SPECIALIST="${MAXY_SPECIALIST:-}"
|
|
75
|
-
if [ -n "$PARENT_TOOL_USE_ID" ]; then
|
|
76
|
-
# In-window subagent — passthrough. Tool name resolves on the
|
|
77
|
-
# subagent's own frontmatter `tools:` allowlist downstream.
|
|
78
|
-
echo "[admin-tool-gate] role=subagent parent_tool_use_id=${PARENT_TOOL_USE_ID} specialist=${SPECIALIST:--} tool=${TOOL_NAME} decision=allow source=hook-passthrough" >&2
|
|
79
|
-
else
|
|
80
|
-
# Admin direct invocation — refuse with dispatch hint.
|
|
81
|
-
case "$TOOL_NAME" in
|
|
82
|
-
Write|Edit|MultiEdit)
|
|
83
|
-
OWNER="content-producer"
|
|
84
|
-
;;
|
|
85
|
-
Bash)
|
|
86
|
-
OWNER="the specialist that owns the workflow needing shell access"
|
|
87
|
-
;;
|
|
88
|
-
WebFetch|WebSearch)
|
|
89
|
-
OWNER="research-assistant"
|
|
90
|
-
;;
|
|
91
|
-
esac
|
|
92
|
-
echo "[admin-tool-gate] role=admin parent_tool_use_id=- specialist=${SPECIALIST:--} tool=${TOOL_NAME} decision=block reason=orchestration-only owner=${OWNER}" >&2
|
|
93
|
-
hook_block_with_emit "$SESSION_ID" "admin-tool-gate" "${TOOL_NAME}" \
|
|
94
|
-
"orchestration-only" \
|
|
95
|
-
"Blocked: \`${TOOL_NAME}\` is not callable from the admin seat. The admin seat is orchestration + clarification + delivery only — authoring, shell work, and web research are owned by specialists.
|
|
96
|
-
Dispatch the work to ${OWNER} via the Agent tool."
|
|
97
|
-
fi
|
|
98
|
-
;;
|
|
99
|
-
esac
|
|
100
|
-
|
|
101
46
|
# ── Code directory protection ────────────────────────────────────────────
|
|
102
47
|
case "$TOOL_NAME" in
|
|
103
48
|
Write|Edit)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capabilities-here.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/capabilities-here.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { resolveCapabilitiesHere } from "../tools/capabilities-here.js";
|
|
6
|
+
function writeFile(path, body) {
|
|
7
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
8
|
+
writeFileSync(path, body);
|
|
9
|
+
}
|
|
10
|
+
function seedTree(root, spec) {
|
|
11
|
+
const accountConfigPath = join(root, "accounts", spec.accountId, "account.json");
|
|
12
|
+
const brandConfigPath = join(root, "platform", "config", "brand.json");
|
|
13
|
+
const platformRoot = join(root, "platform");
|
|
14
|
+
const premiumPluginsRoot = join(root, "premium-plugins");
|
|
15
|
+
const claudeConfigDir = join(root, "claude-config");
|
|
16
|
+
writeFile(accountConfigPath, JSON.stringify({
|
|
17
|
+
accountId: spec.accountId,
|
|
18
|
+
enabledPlugins: spec.enabledPlugins ?? [],
|
|
19
|
+
tier: spec.tier,
|
|
20
|
+
adminModel: spec.adminModel,
|
|
21
|
+
publicModel: spec.publicModel,
|
|
22
|
+
}));
|
|
23
|
+
writeFile(brandConfigPath, JSON.stringify({
|
|
24
|
+
productName: spec.productName,
|
|
25
|
+
shipsPremiumBundles: spec.shipsPremiumBundles ?? [],
|
|
26
|
+
}));
|
|
27
|
+
const specialistsDir = join(platformRoot, "templates", "specialists", "agents");
|
|
28
|
+
mkdirSync(specialistsDir, { recursive: true });
|
|
29
|
+
for (const name of spec.platformSpecialists) {
|
|
30
|
+
writeFileSync(join(specialistsDir, `${name}.md`), `---\nname: ${name}\n---\n`);
|
|
31
|
+
}
|
|
32
|
+
for (const [bundle, names] of Object.entries(spec.premiumSpecialists ?? {})) {
|
|
33
|
+
const dir = join(premiumPluginsRoot, bundle, "agents");
|
|
34
|
+
mkdirSync(dir, { recursive: true });
|
|
35
|
+
for (const name of names) {
|
|
36
|
+
writeFileSync(join(dir, `${name}.md`), `---\nname: ${name}\n---\n`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const agentsDir = join(claudeConfigDir, "agents");
|
|
40
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
41
|
+
for (const name of spec.registeredAgents ?? []) {
|
|
42
|
+
writeFileSync(join(agentsDir, `${name}.md`), `---\nname: ${name}\n---\n`);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
input: {
|
|
46
|
+
accountId: spec.accountId,
|
|
47
|
+
accountConfigPath,
|
|
48
|
+
brandConfigPath,
|
|
49
|
+
platformRoot,
|
|
50
|
+
premiumPluginsRoot,
|
|
51
|
+
claudeConfigDir,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
describe("resolveCapabilitiesHere", () => {
|
|
56
|
+
let root;
|
|
57
|
+
beforeEach(() => { root = mkdtempSync(join(tmpdir(), "cap-here-")); });
|
|
58
|
+
afterEach(() => { rmSync(root, { recursive: true, force: true }); });
|
|
59
|
+
it("returns Maxy install set with no premium bundles", async () => {
|
|
60
|
+
const { input } = seedTree(root, {
|
|
61
|
+
accountId: "acct-maxy",
|
|
62
|
+
enabledPlugins: ["business-assistant", "sales", "teaching"],
|
|
63
|
+
tier: "pro",
|
|
64
|
+
adminModel: "claude-opus-4-7",
|
|
65
|
+
publicModel: "claude-haiku-4-5",
|
|
66
|
+
productName: "Maxy",
|
|
67
|
+
shipsPremiumBundles: [],
|
|
68
|
+
platformSpecialists: ["personal-assistant", "project-manager", "librarian"],
|
|
69
|
+
registeredAgents: ["plugin:admin:librarian", "plugin:admin:project-manager"],
|
|
70
|
+
});
|
|
71
|
+
const result = await resolveCapabilitiesHere(input, "2026-05-31T07:00:00Z");
|
|
72
|
+
expect(result.accountId).toBe("acct-maxy");
|
|
73
|
+
expect(result.tier).toBe("pro");
|
|
74
|
+
expect(result.enabledPlugins).toEqual(["business-assistant", "sales", "teaching"]);
|
|
75
|
+
expect(result.installedPlatformSpecialists).toEqual(["librarian", "personal-assistant", "project-manager"]);
|
|
76
|
+
expect(result.installedPremiumSpecialists).toEqual([]);
|
|
77
|
+
expect(result.registeredAgentTypes).toEqual(["plugin:admin:librarian", "plugin:admin:project-manager"]);
|
|
78
|
+
expect(result.brand.productName).toBe("Maxy");
|
|
79
|
+
expect(result.brand.shipsPremiumBundles).toEqual([]);
|
|
80
|
+
expect(result.capturedAt).toBe("2026-05-31T07:00:00Z");
|
|
81
|
+
});
|
|
82
|
+
it("returns Real Agent install set including premium specialists from each shipped bundle", async () => {
|
|
83
|
+
const { input } = seedTree(root, {
|
|
84
|
+
accountId: "acct-ra",
|
|
85
|
+
enabledPlugins: ["loop", "property-data", "estate-sales"],
|
|
86
|
+
tier: "agency",
|
|
87
|
+
productName: "Real Agent",
|
|
88
|
+
shipsPremiumBundles: ["real-agent"],
|
|
89
|
+
platformSpecialists: ["coding-assistant", "database-operator"],
|
|
90
|
+
premiumSpecialists: {
|
|
91
|
+
"real-agent": ["negotiator", "valuer", "compliance", "listing-curator"],
|
|
92
|
+
},
|
|
93
|
+
registeredAgents: ["plugin:real-agent:negotiator"],
|
|
94
|
+
});
|
|
95
|
+
const result = await resolveCapabilitiesHere(input, "2026-05-31T07:00:00Z");
|
|
96
|
+
expect(result.brand.productName).toBe("Real Agent");
|
|
97
|
+
expect(result.brand.shipsPremiumBundles).toEqual(["real-agent"]);
|
|
98
|
+
expect(result.installedPlatformSpecialists).toEqual(["coding-assistant", "database-operator"]);
|
|
99
|
+
expect(result.installedPremiumSpecialists).toEqual([
|
|
100
|
+
{ bundle: "real-agent", name: "compliance" },
|
|
101
|
+
{ bundle: "real-agent", name: "listing-curator" },
|
|
102
|
+
{ bundle: "real-agent", name: "negotiator" },
|
|
103
|
+
{ bundle: "real-agent", name: "valuer" },
|
|
104
|
+
]);
|
|
105
|
+
expect(result.registeredAgentTypes).toEqual(["plugin:real-agent:negotiator"]);
|
|
106
|
+
});
|
|
107
|
+
it("handles missing fields and missing directories gracefully", async () => {
|
|
108
|
+
const accountConfigPath = join(root, "account.json");
|
|
109
|
+
const brandConfigPath = join(root, "brand.json");
|
|
110
|
+
writeFileSync(accountConfigPath, JSON.stringify({ accountId: "x" }));
|
|
111
|
+
writeFileSync(brandConfigPath, JSON.stringify({}));
|
|
112
|
+
const result = await resolveCapabilitiesHere({
|
|
113
|
+
accountId: "x",
|
|
114
|
+
accountConfigPath,
|
|
115
|
+
brandConfigPath,
|
|
116
|
+
platformRoot: join(root, "absent-platform"),
|
|
117
|
+
premiumPluginsRoot: join(root, "absent-premium"),
|
|
118
|
+
claudeConfigDir: join(root, "absent-claude"),
|
|
119
|
+
}, "2026-05-31T07:00:00Z");
|
|
120
|
+
expect(result.tier).toBeNull();
|
|
121
|
+
expect(result.adminModel).toBeNull();
|
|
122
|
+
expect(result.publicModel).toBeNull();
|
|
123
|
+
expect(result.enabledPlugins).toEqual([]);
|
|
124
|
+
expect(result.installedPlatformSpecialists).toEqual([]);
|
|
125
|
+
expect(result.installedPremiumSpecialists).toEqual([]);
|
|
126
|
+
expect(result.registeredAgentTypes).toEqual([]);
|
|
127
|
+
expect(result.brand.productName).toBeNull();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
//# sourceMappingURL=capabilities-here.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capabilities-here.test.js","sourceRoot":"","sources":["../../src/__tests__/capabilities-here.test.ts"],"names":[],"mappings":"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"}
|